diff --git a/(K+U+B)^3=KUB v2.py b/(K+U+B)^3=KUB v2.py new file mode 100644 index 000000000..5bfa792ba --- /dev/null +++ b/(K+U+B)^3=KUB v2.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/issues/13#issuecomment-943412289 + + +""" +Напишите программу, которая выводит решение ребуса (K+U+B)^3=KUB, где знак ^ означает возведение в степень. +Одинаковым буквам соответствуют одинаковые цифры. Разным буквам соответствуют разные цифры. +Выведите решение в формате (K+U+B)^3=KUB, где вместо букв подставлены цифры. +""" + +for num in range(100, 1000): + kub = set(str(num)) + if len(kub) == 3 and sum(map(int, kub)) ** 3 == num: + print(f'({"+".join(str(num))})^3={num}') diff --git a/(K+U+B)^3=KUB.py b/(K+U+B)^3=KUB.py new file mode 100644 index 000000000..20618a4b5 --- /dev/null +++ b/(K+U+B)^3=KUB.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stepik.org/lesson/360560/step/13?unit=345000 + + +""" +Напишите программу, которая выводит решение ребуса (K+U+B)^3=KUB, где знак ^ означает возведение в степень. +Одинаковым буквам соответствуют одинаковые цифры. Разным буквам соответствуют разные цифры. +Выведите решение в формате (K+U+B)^3=KUB, где вместо букв подставлены цифры. +""" + +for k in range(1, 10): + for u in range(10): + for b in range(10): + # Разным буквам соответствуют разные цифры + if len({k, u, b}) != 3: + continue + + kub = k * 100 + u * 10 + b + + # Проверка, что число трехзначное или не совпадает условие + if kub <= 999 and kub == (k + u + b) ** 3: + print(f"({k}+{u}+{b})^3={kub}") diff --git a/.gitignore b/.gitignore index b888a3818..ac96cbc34 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,14 @@ docs/_build/ # PyBuilder target/ +# Current project +.gigaide/ +backup/ +database/ +TOKEN.txt +LOGIN_PASSWORD.txt +geckodriver.log +*.sqlite* +*.db +log.txt +settings.* diff --git a/AnalysisOfChatLog/AnalysisOfChatLog.py b/AnalysisOfChatLog/AnalysisOfChatLog.py index da11354b3..02cb13777 100644 --- a/AnalysisOfChatLog/AnalysisOfChatLog.py +++ b/AnalysisOfChatLog/AnalysisOfChatLog.py @@ -1,25 +1,27 @@ # coding=utf-8 -__author__ = 'ipetrash' +__author__ = "ipetrash" # Есть лог-файл какого-то чата. Посчитать «разговорчивость» пользователей в нем в виде ник — количество фраз. # Посчитать среднее число букв на участника чата. -if __name__ == '__main__': - #file_name = raw_input("Chat log file: ") - file_name = "C:\Users\ipetrash\Desktop\chat log.txt" - user_count = {} - for line in file(file_name): - line = line.decode("windows-1251").replace("\n", "") - user, count = line.split(":")[0], len(line.split(":")[1].split(' ')) - 1 - if user in user_count: - user_count[user] += count - else: - user_count[user] = count - - sum = 0.0 - for user, count in user_count.items(): - sum += count - print(user + " = %d" % count) - - print("Average = %s" % (sum / len(user_count)) ) \ No newline at end of file + +file_name = r"chat log.txt" + +user_count: dict[str, int] = dict() +for line in open(file_name, encoding="utf-8"): + line = line.strip() + + user, text = line.split(":", maxsplit=1) + count = len(text.strip().split(" ")) - 1 + if user in user_count: + user_count[user] += count + else: + user_count[user] = count + +total = 0 +for user, count in user_count.items(): + total += count + print(f"{user} = {count}") + +print(f"Average = {total // len(user_count)}") diff --git a/AnalysisOfChatLog/chat log.txt b/AnalysisOfChatLog/chat log.txt new file mode 100644 index 000000000..800b904d7 --- /dev/null +++ b/AnalysisOfChatLog/chat log.txt @@ -0,0 +1,14 @@ +xxx: Многомного зарядный револьвер конечно жесть. Зачем оно надо? Ведь мы прекресно знаем из американских фильмов, что в обычном шестизарядном барабане пули не кончяются никогда, машина взрывается от одного выстрела девятым калибром, а стоит тебе спасти девушку от смерти, как она тут же с радостью даёт. +yyy: Чувак, в 1855 году не было автомобилей. От одного выстрела взрывалась лошадь. +zzz: Да-да, у ковбоев были специальные отравленные пули - с ДВУМЯ каплями никотина. +xxx: интересно, сколько интересно щас емкость всего инета? +xxx: нашел: Объем общемировых данных вырастет с 33 зеттабайт в 2018 году до 175 в 2025. +xxx: я таких приставок даже не знаю +yyy: да бля +yyy: 99 процентов это порно +yyy: остальные 99 процентов это видео с котиками +yyy: еще 99 процентов это порно с котиками. +yyy: и того 297 процентов говна +xxx: Пойду заниматься похоронным бизнесом. Кто со мной жмите класс. +yyy: Супер мега идея, я щас умру от восторга! +ххх: Будешь первым клиентом) \ No newline at end of file diff --git a/BFS__breadth_first_search.py b/BFS__breadth_first_search.py new file mode 100644 index 000000000..ff69f3612 --- /dev/null +++ b/BFS__breadth_first_search.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + +# SOURCE: https://stackoverflow.com/a/47902476/5909792 + + +from collections import deque +from typing import Any + + +# Breadth-first search +def bfs( + grid: list[list[Any]], start: tuple[Any, Any], goal: Any, wall: Any +) -> list[tuple[Any, Any]]: + width, height = len(grid[0]), len(grid) + queue = deque([[start]]) + seen = {start} + + while queue: + path = queue.popleft() + x, y = path[-1] + if grid[y][x] == goal: + return path + + for x2, y2 in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + if ( + 0 <= x2 < width + and 0 <= y2 < height + and grid[y2][x2] != wall + and (x2, y2) not in seen + ): + queue.append(path + [(x2, y2)]) + seen.add((x2, y2)) + + +if __name__ == "__main__": + start = 5, 2 + wall, goal = "#", "*" + grid = [ + list(".........."), + list("..*#...##."), + list("..##...#*."), + list(".....###.."), + list("......*..."), + ] + x, y = start + grid[y][x] = "@" # Start + print("\n".join("".join(row) for row in grid)) + # .......... + # ..*#...##. + # ..##.@.#*. + # .....###.. + # ......*... + + print() + + path = bfs(grid, start, goal, wall) + print(path) + # [(5, 2), (4, 2), (4, 3), (4, 4), (5, 4), (6, 4)] + + for x, y in path[1:]: + grid[y][x] = "x" + print("\n".join("".join(row) for row in grid)) + # .......... + # ..*#...##. + # ..##x@.#*. + # ....x###.. + # ....xxx... diff --git a/Base64_examples/decode_to_file.py b/Base64_examples/decode_to_file.py index cfbdff5ce..a2ce75c7e 100644 --- a/Base64_examples/decode_to_file.py +++ b/Base64_examples/decode_to_file.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from base64 import b64decode -def decode_base64_to_file(file_name: str, text_base64: str): - with open(file_name, 'wb') as f: +def decode_base64_to_file(file_name: str, text_base64: str) -> None: + with open(file_name, "wb") as f: data = b64decode(text_base64) f.write(data) -if __name__ == '__main__': +if __name__ == "__main__": # Hello World! - text = 'SGVsbG8gV29ybGQh' + text = "SGVsbG8gV29ybGQh" - decode_base64_to_file('result.txt', text) + decode_base64_to_file("result.txt", text) diff --git a/Base64_examples/decode_to_file_certificate__and_open.py b/Base64_examples/decode_to_file_certificate__and_open.py index 58ab98547..b2375126c 100644 --- a/Base64_examples/decode_to_file_certificate__and_open.py +++ b/Base64_examples/decode_to_file_certificate__and_open.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -FILE_NAME = 'cert.cer' +FILE_NAME = "cert.cer" TEXT = """ MIIBeTCCASigAwIBAgIEBoLBizAIBgYqhQMCAgMwMTELMAkGA1UEBhMCUlUxEjAQBgNVBAoMCUNy eXB0b1BybzEOMAwGA1UEAwwFQWxpYXMwHhcNMTcwNjA3MDk1MzM5WhcNMTgwNjA3MDk1MzM5WjAx @@ -16,8 +16,10 @@ """ from decode_to_file import decode_base64_to_file + decode_base64_to_file(FILE_NAME, TEXT) # Open file import os + os.startfile(FILE_NAME) diff --git a/Base64_examples/example_encode_and_decode.py b/Base64_examples/example_encode_and_decode.py index cae57b246..ba4960a1c 100644 --- a/Base64_examples/example_encode_and_decode.py +++ b/Base64_examples/example_encode_and_decode.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 text = "Hello py! Привет py!" -print("Text: {}".format(text)) +print(f"Text: {text}") b16 = base64.b16encode(text.encode()) b32 = base64.b32encode(text.encode()) diff --git a/Base64_examples/gui_base64.py b/Base64_examples/gui_base64.py index 251a9a1e8..ba49bbf97 100644 --- a/Base64_examples/gui_base64.py +++ b/Base64_examples/gui_base64.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 import traceback +import sys + +from os.path import split as path_split try: from PyQt5.QtWidgets import * @@ -21,17 +24,15 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions @@ -130,15 +131,14 @@ def log_uncaught_exceptions(ex_cls, ex, tb): "utf_16_le", "utf_7", "utf_8", - "utf_8_sig" + "utf_8_sig", ] class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - from os.path import split as path_split self.setWindowTitle(path_split(__file__)[1]) self.button_direct = QPushButton() @@ -148,10 +148,10 @@ def __init__(self): self.cb_encoding.addItems(STANDART_ENCODINGS) self.cb_encoding.setFixedWidth(100) - index = self.cb_encoding.findText('utf_8') + index = self.cb_encoding.findText("utf_8") self.cb_encoding.setCurrentIndex(index) - self.cb_raw = QCheckBox('raw') + self.cb_raw = QCheckBox("raw") button_layout = QHBoxLayout() button_layout.addWidget(self.button_direct) @@ -171,9 +171,9 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.clicked.connect(self.show_detail_error_massage) self.last_error_message = None @@ -205,11 +205,11 @@ def __init__(self): self.setLayout(layout) - def show_detail_error_massage(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_massage(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -217,7 +217,7 @@ def show_detail_error_massage(self): mb.exec_() - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() @@ -239,7 +239,7 @@ def input_text_changed(self): else: # Параметр errors='replace' нужен для того, чтобы при декодировании в строку проблемные символы # заменялись символами-заменителями (�) - text = text.decode(encoding=codec_name, errors='replace') + text = text.decode(encoding=codec_name, errors="replace") self.text_edit_output.setPlainText(text) @@ -254,16 +254,18 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def change_convert_direct(self): + def change_convert_direct(self) -> None: self.direct_encode_text = not self.direct_encode_text - self.button_direct.setText('text -> base64' if self.direct_encode_text else 'base64 -> text') + self.button_direct.setText( + "text -> base64" if self.direct_encode_text else "base64 -> text" + ) self.input_text_changed() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/Base64_examples/screenshot.jpg b/Base64_examples/screenshot.jpg index f6aaa0f37..7421471cc 100644 Binary files a/Base64_examples/screenshot.jpg and b/Base64_examples/screenshot.jpg differ diff --git a/Bot Buff Knight Advanced/__foo_test.py b/Bot Buff Knight Advanced/__foo_test.py deleted file mode 100644 index b2ad97263..000000000 --- a/Bot Buff Knight Advanced/__foo_test.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import cv2 -import numpy as np -from timeit import default_timer as timer -from datetime import datetime - -from main import find_rect_contours, filter_button, filter_fairy, filter_fairy_and_button - - -def draw_rects(img, contours_rects, color=(0, 255, 0)): - for x, y, w, h in contours_rects: - cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness=5) - # cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness=-1) - - for i in range(h // 10): - cv2.line(img, (x, y + i * 10 + 5), (x + w, y + i * 10 + 5), color, thickness=2) - - cv2.line(img, (0, y), (img.shape[1], y), color, thickness=1) - - -BLUE_HSV_MIN = 105, 175, 182 -BLUE_HSV_MAX = 121, 255, 255 - -ORANGE_HSV_MIN = 7, 200, 200 -ORANGE_HSV_MAX = 20, 255, 255 - -FAIRY_HSV_MIN = 73, 101, 101 -FAIRY_HSV_MAX = 95, 143, 255 - -import glob - -for file_name in glob.glob('screenshots__Buff Knight Advanced/*.png'): - print(file_name) - - img = cv2.imread(file_name) - - # # Resize - # img = cv2.resize(img, None, fx=0.7, fy=0.7, interpolation=cv2.INTER_CUBIC) - - img_screenshot = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - image_source_hsv = cv2.cvtColor(np.array(img_screenshot), cv2.COLOR_RGB2HSV) - # cv2.imshow('img', img) - print(img.shape[:2]) - - img_with_rect = img.copy() - - t = timer() - - try: - rects_blue = find_rect_contours(image_source_hsv, BLUE_HSV_MIN, BLUE_HSV_MAX) - rects_orange = find_rect_contours(image_source_hsv, ORANGE_HSV_MIN, ORANGE_HSV_MAX) - rects_fairy = find_rect_contours(image_source_hsv, FAIRY_HSV_MIN, FAIRY_HSV_MAX) - - # print('rects_fairy:', rects_fairy) - - # Фильтрование оставшихся объектов - rects_blue = list(filter(filter_button, rects_blue)) - rects_orange = list(filter(filter_button, rects_orange)) - rects_fairy = list(filter(filter_fairy, rects_fairy)) - - # print('rects_fairy1:', rects_fairy) - - if rects_blue or rects_orange: - new_rects_fairy = [] - - # Фея и кнопки находятся рядом, поэтому имеет смысл убрать те "феи", что не имеют рядом синих или оранжевых - for rect_fairy in rects_fairy: - x, y = rect_fairy[:2] - - found = False - - found_blue = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue))) - found_orange = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange))) - # print(x, y, found_blue or found_orange) - - if found_blue or found_orange: - new_rects_fairy.append(rect_fairy) - - rects_fairy = new_rects_fairy - - print('rects_fairy2:', rects_fairy) - - if not rects_fairy: - continue - - if len(rects_fairy) > 1: - file_name = 'many_fairy__{}.png'.format(datetime.now().strftime('%d%m%y %H%M%S.%f')) - print(file_name) - cv2.imwrite(file_name, img_with_rect) - continue - - # Фильтр кнопок. Нужно оставить только те кнопки, что рядом с феей - rect_fairy = rects_fairy[0] - rects_blue = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue)) - rects_orange = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange)) - - # Если одновременно обе кнопки - if rects_blue and rects_orange: - file_name = 'many_buttons__{}.png'.format(datetime.now().strftime('%d%m%y %H%M%S.%f')) - print(file_name) - cv2.imwrite(file_name, img_with_rect) - continue - - if not rects_blue and not rects_orange: - continue - - print('rects_blue({}): {}'.format(len(rects_blue), rects_blue)) - print('rects_orange({}): {}'.format(len(rects_orange), rects_orange)) - print('rects_fairy({}): {}'.format(len(rects_fairy), rects_fairy)) - - draw_rects(img_with_rect, rects_blue, (255, 0, 0)) - draw_rects(img_with_rect, rects_orange, (0, 0, 255)) - draw_rects(img_with_rect, rects_fairy, (255, 255, 255)) - - cv2.putText(img_with_rect, 'blue ' + str(rects_blue), (10, 500 + 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - cv2.putText(img_with_rect, 'orange ' + str(rects_orange), (10, 500 + 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - cv2.putText(img_with_rect, 'fairy ' + str(rects_fairy), (10, 500 + 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - - finally: - print('Elapsed: {} secs'.format(timer() - t)) - print() - - cv2.imshow('img_with_rect ' + file_name, img_with_rect) - - # cv2.imwrite(new_file_name, img_with_rect) - # cv2.imwrite('img_with_rect.png', img_with_rect) - -cv2.waitKey() diff --git a/Bot Buff Knight Advanced/common.py b/Bot Buff Knight Advanced/common.py deleted file mode 100644 index 7965874d2..000000000 --- a/Bot Buff Knight Advanced/common.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from datetime import datetime -import cv2 -import numpy as np - - -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - if log_file: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - if log_stdout: - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -def get_current_datetime_str(): - return datetime.now().strftime('%d%m%y %H%M%S.%f') - - -def find_contours(image_source_hsv, hsv_min, hsv_max): - thresholded_image = image_source_hsv - - # Отфильтровываем только то, что нужно, по диапазону цветов - thresholded_image = cv2.inRange( - thresholded_image, - np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) - ) - - # Убираем шум - thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - - # Замыкаем оставшиеся крупные объекты - thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - - # Находим контуры - contours, _ = cv2.findContours( - thresholded_image, - cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE - ) - - return contours - - -def find_rect_contours(image_source_hsv, hsv_min, hsv_max): - return [cv2.boundingRect(c) for c in find_contours(image_source_hsv, hsv_min, hsv_max)] diff --git a/Bot Buff Knight Advanced/main.py b/Bot Buff Knight Advanced/main.py deleted file mode 100644 index 806780202..000000000 --- a/Bot Buff Knight Advanced/main.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import threading -import os -from timeit import default_timer as timer -import time - -# pip install opencv-python -import cv2 -import numpy as np -import pyautogui - -from common import get_logger, get_current_datetime_str, find_rect_contours - - -log = get_logger('Bot Buff Knight Advanced') - - -def filter_button(rect): - x, y, w, h = rect - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - - rule_1 = w > 30 and h > 30 - rule_2 = w > h - - # У кнопок ширина больше высоты - return rule_1 and rule_2 - - -def filter_fairy(rect): - x, y, w, h = rect - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - - # У феи ширина и высота больше определенной цифры - rule_1 = w > 20 and h > 20 - - # У феи высота больше ширины - rule_2 = h > w - - # Фея приблизительно по центру экрана летает - rule_3 = y > 300 and y < 900 - - return rule_1 and rule_2 and rule_3 - - -def filter_fairy_and_button(rect_fairy, rect_button): - x, y = rect_fairy[:2] - x2, y2, _, h2 = rect_button - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - return abs(x2 - x) <= 50 and abs(y2 + h2 - y) <= 50 - - -DIR = 'saved_screenshots' - - -def save_screenshot(prefix, img_hsv): - file_name = DIR + '/{}__{}.png'.format(prefix, get_current_datetime_str()) - log.debug(file_name) - cv2.imwrite(file_name, cv2.cvtColor(np.array(img_hsv), cv2.COLOR_HSV2BGR)) - - -BLUE_HSV_MIN = 105, 175, 182 -BLUE_HSV_MAX = 121, 255, 255 - -ORANGE_HSV_MIN = 7, 200, 200 -ORANGE_HSV_MAX = 20, 255, 255 - -FAIRY_HSV_MIN = 73, 101, 101 -FAIRY_HSV_MAX = 95, 143, 255 - - -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+Q' -AUTO_ATTACK_COMBINATION = 'Space' - -BOT_DATA = { - 'START': False, - 'AUTO_ATTACK': False, -} - - -def change_start(): - BOT_DATA['START'] = not BOT_DATA['START'] - log.debug('START: %s', BOT_DATA['START']) - - -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - log.debug('AUTO_ATTACK: %s', BOT_DATA['AUTO_ATTACK']) - - -log.debug('Press "%s" for RUN / PAUSE', RUN_COMBINATION) -log.debug('Press "%s" for QUIT', QUIT_COMBINATION) -log.debug('Press "%s" for AUTO_ATTACK', AUTO_ATTACK_COMBINATION) - - -if not os.path.exists(DIR): - os.mkdir(DIR) - - -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: log.debug('Quit by Escape') or os._exit(0)) -keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) -keyboard.add_hotkey(RUN_COMBINATION, change_start) - - -def process_auto_attack(): - while True: - if not BOT_DATA['START']: - time.sleep(0.01) - continue - - # Симуляция атаки - if BOT_DATA['AUTO_ATTACK']: - pyautogui.typewrite('C') - - time.sleep(0.01) - -# Запуск потока для автоатаки -thread_auto_attack = threading.Thread(target=process_auto_attack) -thread_auto_attack.start() - - -def process_find_fairy(img_hsv): - rects_blue = find_rect_contours(img_hsv, BLUE_HSV_MIN, BLUE_HSV_MAX) - rects_orange = find_rect_contours(img_hsv, ORANGE_HSV_MIN, ORANGE_HSV_MAX) - rects_fairy = find_rect_contours(img_hsv, FAIRY_HSV_MIN, FAIRY_HSV_MAX) - - # Фильтрование оставшихся объектов - rects_blue = list(filter(filter_button, rects_blue)) - rects_orange = list(filter(filter_button, rects_orange)) - rects_fairy = list(filter(filter_fairy, rects_fairy)) - - # Фильтр объектов, похожих на фею - if rects_blue or rects_orange: - new_rects_fairy = [] - - # Фея и кнопки находятся рядом, поэтому имеет смысл убрать те "феи", что не имеют рядом синих или оранжевых - for rect_fairy in rects_fairy: - found_blue = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue))) - found_orange = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange))) - - # Если возле феи что-то нашлось - if found_blue or found_orange: - new_rects_fairy.append(rect_fairy) - - rects_fairy = new_rects_fairy - - if not rects_fairy: - return - - if len(rects_fairy) > 1: - save_screenshot('many_fairy', img_hsv) - return - - # Фильтр кнопок. Нужно оставить только те кнопки, что рядом с феей - rect_fairy = rects_fairy[0] - rects_blue = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue)) - rects_orange = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange)) - - # Если одновременно обе кнопки - if rects_blue and rects_orange: - save_screenshot('many_buttons', img_hsv) - return - - if not rects_blue and not rects_orange: - return - - # Найдена синяя кнопка - if rects_blue: - log.debug('FOUND BLUE') - save_screenshot('found_blue', img) - - pyautogui.typewrite('D') - - # Найдена оранжевая кнопка - if rects_orange: - log.debug('FOUND ORANGE') - save_screenshot('found_orange', img) - - pyautogui.typewrite('A') - - -while True: - if not BOT_DATA['START']: - time.sleep(0.01) - continue - - t = timer() - - try: - img_screenshot = pyautogui.screenshot() - log.debug('img_screenshot: %s', img_screenshot) - - img = cv2.cvtColor(np.array(img_screenshot), cv2.COLOR_RGB2HSV) - - # Поиск феи - process_find_fairy(img) - - # TODO: возможность автоматического использования хилок и восстановления маны - - finally: - log.debug('Elapsed: {} secs'.format(timer() - t)) - - time.sleep(0.01) diff --git a/Builder__example.py b/Builder__example.py deleted file mode 100644 index 85b352bd9..000000000 --- a/Builder__example.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -class Foo: - def __init__(self): - self.items = [] - self.key_by_value = dict() - - def add_item(self, value): - self.items.append(value) - return self - - def add_items(self, values): - self.items += values - return self - - def set_value(self, key, value): - self.key_by_value[key] = value - return self - - def get_value(self, key): - return self.key_by_value[key] - - def __repr__(self): - return 'Foo'.format(len(self.items), len(self.key_by_value)) - - -foo = Foo()\ - .add_item(1)\ - .add_items('abc')\ - .set_value('x', 1)\ - .set_value('x[]', [1, 2, 3]) - -print(foo) -print(foo.items) -print(foo.key_by_value) -print(foo.get_value('x')) -print(foo.get_value('x[]')) diff --git a/CONTACT__examples/config.py b/CONTACT__examples/config.py index 6b2320374..d7d312554 100644 --- a/CONTACT__examples/config.py +++ b/CONTACT__examples/config.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -URL_NG_SERVER = '' +URL_NG_SERVER = "" # Идентификатор программного обеспечения интегратора -INT_SOFT_ID = '' +INT_SOFT_ID = "" # Код точки обслуживания -POINT_CODE = '' +POINT_CODE = "" diff --git a/CONTACT__examples/connect_to_ng_server_via_socket_.py b/CONTACT__examples/connect_to_ng_server_via_socket_.py index b7392a73f..eb4407d9d 100644 --- a/CONTACT__examples/connect_to_ng_server_via_socket_.py +++ b/CONTACT__examples/connect_to_ng_server_via_socket_.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import socket from config import * + # URL_NG_SERVER = 'http://10.7.8.31:12000' -HOST = URL_NG_SERVER.replace('https://', '').replace('http://', '') +HOST = URL_NG_SERVER.replace("https://", "").replace("http://", "") # HOST = 10.7.8.31, PORT = 12000 -HOST, PORT = HOST.split(':') +HOST, PORT = HOST.split(":") PORT = int(PORT) -post_data = """ +post_data = f""" -""".format(INT_SOFT_ID=INT_SOFT_ID, POINT_CODE=POINT_CODE) +""" http_request = ( - 'POST / HTTP/1.1\r\n', - 'Host: {host}:{port}\r\n', - 'Accept-Encoding: gzip, deflate\r\n', - 'User-Agent: {user_agent}\r\n', - 'Connection: close\r\n', - 'Accept: */*\r\n', - 'Content-Length: {content_length}\r\n', - '\r\n', - '{body}' + "POST / HTTP/1.1\r\n", + "Host: {host}:{port}\r\n", + "Accept-Encoding: gzip, deflate\r\n", + "User-Agent: {user_agent}\r\n", + "Connection: close\r\n", + "Accept: */*\r\n", + "Content-Length: {content_length}\r\n", + "\r\n", + "{body}", ) -http_request = ''.join(http_request) +http_request = "".join(http_request) http_request = http_request.format( host=HOST, port=PORT, - user_agent='iHuman', + user_agent="iHuman", content_length=len(post_data), body=post_data, ) @@ -48,13 +50,12 @@ print(repr(http_request)) -import socket sock = socket.socket() sock.connect((HOST, PORT)) sock.send(http_request.encode()) -print('Socket name: {}'.format(sock.getsockname())) -print('\nResponse:') +print(f"Socket name: {sock.getsockname()}") +print("\nResponse:") while True: data = sock.recv(1024) diff --git a/CONTACT__examples/create_and_fill_database_from_dictionary.py b/CONTACT__examples/create_and_fill_database_from_dictionary.py index 2936bc3c3..4537d3050 100644 --- a/CONTACT__examples/create_and_fill_database_from_dictionary.py +++ b/CONTACT__examples/create_and_fill_database_from_dictionary.py @@ -1,7 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import glob +import sqlite3 +import os + +from bs4 import BeautifulSoup def build_sql_table(table_name: str, format_fields_of_table: [dict]) -> str: @@ -51,25 +58,29 @@ def build_sql_table(table_name: str, format_fields_of_table: [dict]) -> str: """ - def build_field_table(format_field: dict, indent=' ' * 4) -> str: - field_name = format_field['attrname'] + def build_field_table(format_field: dict, indent=" " * 4) -> str: + field_name = format_field["attrname"] - field_type = 'TEXT' - if format_field['fieldtype'] == 'i4': - field_type = 'INTEGER' + field_type = "TEXT" + if format_field["fieldtype"] == "i4": + field_type = "INTEGER" - if field_name.upper() == 'ID': - field_type += ' PRIMARY KEY' + if field_name.upper() == "ID": + field_type += " PRIMARY KEY" - return indent + '{} {}'.format(field_name, field_type) + return f"{indent}{field_name} {field_type}" - fields_list = [build_field_table(format_field) for format_field in format_fields_of_table] + fields_list = [ + build_field_table(format_field) for format_field in format_fields_of_table + ] table = """\ CREATE TABLE IF NOT EXISTS {table_name} ( {fields_list} ); -""".format(table_name=table_name.upper(), fields_list=',\n'.join(fields_list)) +""".format( + table_name=table_name.upper(), fields_list=",\n".join(fields_list) + ) return table @@ -89,26 +100,32 @@ def build_insert(row_of_table: dict) -> str: keys = sorted(key for key in row_of_table.keys()) # return "INSERT OR REPLACE INTO {table_name} ({fields}) VALUES ({values});".format( - return "INSERT OR IGNORE INTO {table_name} ({fields}) VALUES ({values});".format( - table_name=table_name.upper(), - fields=','.join(key.upper() for key in keys), - values=','.join(repr(row_of_table[key]).replace('\\"', '').replace("\\'", '') for key in keys), + return ( + "INSERT OR IGNORE INTO {table_name} ({fields}) VALUES ({values});".format( + table_name=table_name.upper(), + fields=",".join(key.upper() for key in keys), + values=",".join( + repr(row_of_table[key]).replace('\\"', "").replace("\\'", "") + for key in keys + ), + ) ) - return '\n'.join(build_insert(row_data.attrs) for row_data in rows_of_table) + return "\n".join(build_insert(row_data.attrs) for row_data in rows_of_table) def create_connect(): - import sqlite3 - return sqlite3.connect('database.sqlite') + return sqlite3.connect("database.sqlite") -def create_table(table_name: str, sql_table: str, sql_table_data_rows: str, drop_table=False): +def create_table( + table_name: str, sql_table: str, sql_table_data_rows: str, drop_table=False +) -> None: # Создание таблицы connect = create_connect() try: if drop_table: - connect.execute('DROP TABLE IF EXISTS ?;', (table_name,)) + connect.execute("DROP TABLE IF EXISTS ?;", (table_name,)) connect.execute(sql_table) connect.executescript(sql_table_data_rows) @@ -119,25 +136,24 @@ def create_table(table_name: str, sql_table: str, sql_table_data_rows: str, drop connect.close() -if __name__ == '__main__': - import glob - for file_name in glob.glob('contact_dicts/*.xml'): - import os +if __name__ == "__main__": + for file_name in glob.glob("contact_dicts/*.xml"): table_name = os.path.splitext(os.path.basename(file_name))[0] - print('Append {} from {}'.format(table_name, file_name)) + print(f"Append {table_name} from {file_name}") - from bs4 import BeautifulSoup - root = BeautifulSoup(open(file_name, 'rb'), 'lxml') + root = BeautifulSoup(open(file_name, "rb"), "lxml") - format_fields_of_dict = [row for row in root.select('metadata > fields > field')] - rows_of_dict = [row for row in root.select('rowdata > row')] + format_fields_of_dict = [ + row for row in root.select("metadata > fields > field") + ] + rows_of_dict = [row for row in root.select("rowdata > row")] sql_table = build_sql_table(table_name, format_fields_of_dict) sql_table_data_rows = build_sql_rows_data_table(table_name, rows_of_dict) # print(sql_table + "\n\n" + sql_table_data_rows) - print(' Append {} rows\n'.format(len(rows_of_dict))) + print(f" Append {len(rows_of_dict)} rows\n") create_table(table_name, sql_table, sql_table_data_rows) # print() diff --git a/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py b/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py index d70931aa5..97cab3430 100644 --- a/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py +++ b/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,6 +10,11 @@ """ +import base64 +import glob +import os +import zlib + def bytes_to_compress_to_base64(bytes_text: bytes) -> str: """ @@ -17,17 +22,14 @@ def bytes_to_compress_to_base64(bytes_text: bytes) -> str: """ - import zlib compress_bytes_xml = zlib.compress(bytes_text) - - import base64 base64_compress = base64.b64encode(compress_bytes_xml) - return base64_compress.decode('utf-8') + return base64_compress.decode("utf-8") -if __name__ == '__main__': - DIR_DICT_CONTACT = 'mini_full_dict__CONTACT' - FILE_NAME_FULL_DICT = 'new_mini_full_dict__CONTACT.xml' +if __name__ == "__main__": + DIR_DICT_CONTACT = "mini_full_dict__CONTACT" + FILE_NAME_FULL_DICT = "new_mini_full_dict__CONTACT.xml" HTML_PATTERN_RESPONSE__GET_CHANGES = """\ str: child_list = list() - import glob - for file_name in glob.glob(DIR_DICT_CONTACT + '/*.xml'): + for file_name in glob.glob(DIR_DICT_CONTACT + "/*.xml"): print(file_name) - import os dict_name = os.path.splitext(os.path.basename(file_name))[0].upper() - bytes_xml = open(file_name, 'rb').read() + bytes_xml = open(file_name, "rb").read() base64_str = bytes_to_compress_to_base64(bytes_xml) - child_list.append("<{0}>{1}".format(dict_name, base64_str)) + child_list.append(f"<{dict_name}>{base64_str}") - with open(FILE_NAME_FULL_DICT, 'w', encoding='utf-8') as f: - f.write(HTML_PATTERN_RESPONSE__GET_CHANGES.format(''.join(child_list))) + with open(FILE_NAME_FULL_DICT, "w", encoding="utf-8") as f: + f.write(HTML_PATTERN_RESPONSE__GET_CHANGES.format("".join(child_list))) print() - print('Write to {}'.format(FILE_NAME_FULL_DICT)) + print(f"Write to {FILE_NAME_FULL_DICT}") diff --git a/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py b/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py index af8d9bc99..7167ee2f7 100644 --- a/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py +++ b/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py @@ -1,49 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def get_bytes_from_base64_zlib(text_or_tag_or_bytes) -> bytes: +import base64 +import zlib +import sys +import os + +from typing import Union + +from bs4 import BeautifulSoup, Tag + + +def get_bytes_from_base64_zlib(text_or_tag_or_bytes: Union[str, bytes, Tag]) -> bytes: """ Декодирует данные из текста или элемента XML из формата BASE64, после разжимает их алгоритмом zlib, парсит и возвращает как объект XML. """ - if not isinstance(text_or_tag_or_bytes, str) and not isinstance(text_or_tag_or_bytes, bytes): + if not isinstance(text_or_tag_or_bytes, str) and not isinstance( + text_or_tag_or_bytes, bytes + ): text = text_or_tag_or_bytes.text else: text = text_or_tag_or_bytes - import base64 compress_data = base64.b64decode(text) - - import zlib return zlib.decompress(compress_data) -if __name__ == '__main__': - file_name_full_dict = 'mini_full_dict__CONTACT.xml' - export_dir = 'mini_full_dict__CONTACT' +if __name__ == "__main__": + file_name_full_dict = "mini_full_dict__CONTACT.xml" + export_dir = "mini_full_dict__CONTACT" - import os os.makedirs(export_dir, exist_ok=True) # Parsing - from bs4 import BeautifulSoup - root = BeautifulSoup(open(file_name_full_dict, 'rb'), 'lxml') - # print(root) - - response = root.select_one('response') + root = BeautifulSoup(open(file_name_full_dict, "rb"), "lxml") + response = root.select_one("response") # Если ошибка - if response['re'] != '0': - print('Error text: "{}"'.format(response['err_text'])) - quit() + if response["re"] != "0": + print(f'Error text: "{response["err_text"]}"') + sys.exit() - print('Справочник полный?:', response['full'] == '1') - print('Версия справочника:', response['version']) + print("Справочник полный?:", response["full"] == "1") + print("Версия справочника:", response["version"]) print() for child in response.children: @@ -51,9 +56,9 @@ def get_bytes_from_base64_zlib(text_or_tag_or_bytes) -> bytes: if child.name is None: continue - file_name = os.path.join(export_dir, child.name) + '.xml' + file_name = os.path.join(export_dir, child.name) + ".xml" print(file_name) - with open(file_name, 'w', encoding='cp1251') as f: + with open(file_name, "w", encoding="cp1251") as f: data = get_bytes_from_base64_zlib(child) - f.write(data.decode(encoding='cp1251')) + f.write(data.decode(encoding="cp1251")) diff --git a/CONTACT__examples/export_logo_from_dict.py b/CONTACT__examples/export_logo_from_dict.py index e1b4112fc..99830a0ff 100644 --- a/CONTACT__examples/export_logo_from_dict.py +++ b/CONTACT__examples/export_logo_from_dict.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -9,26 +9,27 @@ """ +import base64 +import os -if __name__ == '__main__': - FILE_NAME_DICT_LOGO = 'mini_full_dict__CONTACT/logo.xml' - DIR_LOGO_IMAGES = 'logo_images' +from bs4 import BeautifulSoup - import os - os.makedirs(DIR_LOGO_IMAGES, exist_ok=True) - from bs4 import BeautifulSoup - root = BeautifulSoup(open(FILE_NAME_DICT_LOGO, 'rb'), 'lxml') +FILE_NAME_DICT_LOGO = "mini_full_dict__CONTACT/logo.xml" - for row in root.select('rowdata > row'): - logo_name = row['logo_name'] - print(logo_name) +DIR_LOGO_IMAGES = "logo_images" +os.makedirs(DIR_LOGO_IMAGES, exist_ok=True) - logo_data = row['logo_data'] - import base64 - img_data = base64.b64decode(logo_data) +root = BeautifulSoup(open(FILE_NAME_DICT_LOGO, "rb"), "lxml") - file_name = os.path.join(DIR_LOGO_IMAGES, logo_name) +for row in root.select("rowdata > row"): + logo_name = row["logo_name"] + print(logo_name) - with open(file_name, 'wb') as f: - f.write(img_data) + logo_data = row["logo_data"] + img_data = base64.b64decode(logo_data) + + file_name = os.path.join(DIR_LOGO_IMAGES, logo_name) + + with open(file_name, "wb") as f: + f.write(img_data) diff --git a/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py b/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py index 66ebc276b..eb41f8024 100644 --- a/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py +++ b/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -12,12 +12,14 @@ from config import * +import requests -FILE_NAME_FULL_DICT = 'full_dict__CONTACT.xml' +FILE_NAME_FULL_DICT = "full_dict__CONTACT.xml" -if __name__ == '__main__': - post_data = """ + +if __name__ == "__main__": + post_data = f""" - """.format(INT_SOFT_ID=INT_SOFT_ID, POINT_CODE=POINT_CODE) + """ - import requests rs = requests.post(URL_NG_SERVER, data=post_data) print(rs) print(len(rs.text)) - with open(FILE_NAME_FULL_DICT, 'wb') as f: + with open(FILE_NAME_FULL_DICT, "wb") as f: f.write(rs.content) - print('Write to: {}'.format(FILE_NAME_FULL_DICT)) + print(f"Write to: {FILE_NAME_FULL_DICT}") diff --git a/CONTACT__examples/reducing number row in rows dicts.py b/CONTACT__examples/reducing number row in rows dicts.py index 29234d602..8b83ff852 100644 --- a/CONTACT__examples/reducing number row in rows dicts.py +++ b/CONTACT__examples/reducing number row in rows dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -12,30 +12,31 @@ """ -if __name__ == '__main__': - # NOTE: справочники CONTACT хранятся в кодировке cp1251 (windows-1251) +import glob +import os - import glob - for file_name in glob.glob('mini_contact_dicts/*.xml'): - print(file_name) +from bs4 import BeautifulSoup - from bs4 import BeautifulSoup - root = BeautifulSoup(open(file_name, 'rb'), 'lxml', from_encoding='cp1251') - rows = root.select('row') - old_len = len(rows) +# NOTE: справочники CONTACT хранятся в кодировке cp1251 (windows-1251) - for row in rows[20:]: - row.decompose() +for file_name in glob.glob("mini_contact_dicts/*.xml"): + print(file_name) - rows = root.select('row') - print(' len {} -> {}'.format(old_len, len(rows))) + root = BeautifulSoup(open(file_name, "rb"), "lxml", from_encoding="cp1251") - import os - file_name = 'mini_contact_dicts/' + os.path.basename(file_name) + rows = root.select("row") + old_len = len(rows) - with open(file_name, 'w', encoding='cp1251') as f: - f.write(str(root)) + for row in rows[20:]: + row.decompose() - print(' Write to {}'.format(file_name)) - print() + rows = root.select("row") + print(f" len {old_len} -> {len(rows)}") + + file_name = "mini_contact_dicts/" + os.path.basename(file_name) + with open(file_name, "w", encoding="cp1251") as f: + f.write(str(root)) + + print(f" Write to {file_name}") + print() diff --git a/Callable modules/main.py b/Callable modules/main.py index 4f5582299..107b5729d 100644 --- a/Callable modules/main.py +++ b/Callable modules/main.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import print_this -print_this('1232') + + +print_this("1232") print_this(32) diff --git a/Callable modules/print_this.py b/Callable modules/print_this.py index 239db0df5..6f8f95e67 100644 --- a/Callable modules/print_this.py +++ b/Callable modules/print_this.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import sys # SOURCE: https://stackoverflow.com/a/1060872/5909792 class mod_call(object): - def __call__(self, text): + def __call__(self, text) -> None: print(text) -import sys sys.modules[__name__] = mod_call() diff --git a/CamelCase_to_snake_case.py b/CamelCase_to_snake_case.py index cdae0607c..2ac5445ea 100644 --- a/CamelCase_to_snake_case.py +++ b/CamelCase_to_snake_case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,15 +9,15 @@ # SOURCE: https://stackoverflow.com/a/1176023/5909792 def convert(text: str) -> str: - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -if __name__ == '__main__': - assert convert('CamelCase') == 'camel_case' - assert convert('CamelCamelCase') == 'camel_camel_case' - assert convert('Camel2Camel2Case') == 'camel2_camel2_case' - assert convert('getHTTPResponseCode') == 'get_http_response_code' - assert convert('get2HTTPResponseCode') == 'get2_http_response_code' - assert convert('HTTPResponseCode') == 'http_response_code' - assert convert('HTTPResponseCodeXYZ') == 'http_response_code_xyz' +if __name__ == "__main__": + assert convert("CamelCase") == "camel_case" + assert convert("CamelCamelCase") == "camel_camel_case" + assert convert("Camel2Camel2Case") == "camel2_camel2_case" + assert convert("getHTTPResponseCode") == "get_http_response_code" + assert convert("get2HTTPResponseCode") == "get2_http_response_code" + assert convert("HTTPResponseCode") == "http_response_code" + assert convert("HTTPResponseCodeXYZ") == "http_response_code_xyz" diff --git a/Check with notification/Check changes in torrent/check_changes_in_torrent.py b/Check with notification/Check changes in torrent/check_changes_in_torrent.py deleted file mode 100644 index 127d0a44f..000000000 --- a/Check with notification/Check changes in torrent/check_changes_in_torrent.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Script for download serial torrent by qbittorrent. -When serial torrent modify (example: append new series), script download new files. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - - -def get_rutor_torrent_download_info(torrent_url): - """ - Parse torrent url and return tuple: (torrent_file_url, magnet_url, info_hash) - - """ - - import requests - rs = requests.get(torrent_url) - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - magnet_url = root.select_one('#download > a[href^="magnet"]')['href'] - - # For get info hash from magnet url - import re - match = re.compile(r'btih:([abcdef\d]+?)&', flags=re.IGNORECASE).search(magnet_url) - if match: - info_hash = match.group(1) - - return torrent_url.replace('/torrent/', '/download/'), magnet_url, info_hash - - -def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): - info_hash_by_name_dict = {torrent['hash']: torrent['name'] for torrent in qb.torrents()} - - from collections import defaultdict - name_by_info_hash_list_dict = defaultdict(list) - - for info_hash, name in info_hash_by_name_dict.items(): - name_by_info_hash_list_dict[name].append(info_hash) - - # If info_hash already in torrent list - if new_info_hash in info_hash_by_name_dict: - # Get torrent name - name = info_hash_by_name_dict[new_info_hash] - - # Get torrents info hash with - info_hash_list = name_by_info_hash_list_dict[name] - - # Remove new (current) info hash - info_hash_list.remove(new_info_hash) - - # Remove previous torrents - if info_hash_list: - print('Удаление предыдущих раздач этого торрента: {}'.format(info_hash_list)) - qb.delete(info_hash_list) - - else: - print("Предыдущие закачки не найдены") - - -if __name__ == '__main__': - IP_HOST = 'http://127.0.0.1:8080/' - USER = 'admin' - PASSWORD = '' - - from all_common import wait, simple_send_sms - from qbittorrent import Client - - qb = Client(IP_HOST) - qb.login(USER, PASSWORD) - - torrent_url = 'http://anti-tor.org/torrent/544942' - - last_info_hash = None - last_torrent_files = list() - - while True: - try: - from datetime import datetime - - today = datetime.today() - - torrent_file_url, _, info_hash = get_rutor_torrent_download_info(torrent_url) - print('{}: Проверка {}: {} / {}'.format(today, torrent_url, torrent_file_url, info_hash)) - - if qb.get_torrent(info_hash): - print('Торрент {} уже есть в списке раздачи'.format(info_hash)) - - else: - if info_hash != last_info_hash: - import requests - - data = requests.get(torrent_file_url).content.decode('latin1') - - import effbot_bencode - - torrent = effbot_bencode.decode(data) - files = ["/".join(file["path"]) for file in torrent["info"]["files"]] - - # Использование множеств, чтобы узнать разницу списков, т.е. какие файлы были добавлены - # А чтобы узнать какие были удалены: list(set(last_torrent_files) - set(files)) - new_files = list(set(files) - set(last_torrent_files)) - - if last_info_hash is None: - print("Добавление торрента с {} файлами: {}".format(len(new_files), new_files)) - else: - print('Торрент изменился, пора его перекачивать') - print("Добавлено {} файлов: {}".format(len(new_files), new_files)) - - last_info_hash = info_hash - last_torrent_files = files - - # Say qbittorrent client download torrent file - # OR: qb.download_from_file - qb.download_from_link(torrent_file_url) - - # Отправляю смс на номер - text = "Вышла новая серия '{}'".format(torrent['info']['name']) - simple_send_sms(text) - - # Даем 5 секунд на добавление торрента в клиент - import time - - time.sleep(5) - - remove_previous_torrent_from_qbittorrent(qb, info_hash) - - else: - print('Изменений нет') - - print() - - # Every 3 hours - wait(hours=3) - - except Exception: - import traceback - - print('Ошибка:') - print(traceback.format_exc()) - - print('Через 5 минут попробую снова...') - - # Wait 1 minute before next attempt - import time - - time.sleep(60) diff --git a/Check with notification/Check for new chapters of the manga/last_feed b/Check with notification/Check for new chapters of the manga/last_feed deleted file mode 100644 index adf54ba39..000000000 --- a/Check with notification/Check for new chapters of the manga/last_feed +++ /dev/null @@ -1 +0,0 @@ -Башня Бога 3 - 27 52F Кель Хелм (01) \ No newline at end of file diff --git a/Check with notification/Check for new chapters of the manga/main.py b/Check with notification/Check for new chapters of the manga/main.py deleted file mode 100644 index fc087d67a..000000000 --- a/Check with notification/Check for new chapters of the manga/main.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -from pathlib import Path - -import feedparser -import requests - -from all_common import make_backslashreplace_console, get_logger, simple_send_sms, wait - - -make_backslashreplace_console() - - -log = get_logger('new_chapters_manga') - - -def get_feeds_by_manga_chapters(url_rss: str) -> list: - rss_text = requests.get(url_rss).text - feed = feedparser.parse(rss_text) - - feeds = list() - - for entry in feed.entries: - title = entry.title - - if title.startswith('Манга '): - title = title[len('Манга '):] - - elif title.startswith('Взрослая манга '): - title = title[len('Взрослая манга '):] - - feeds.append(title) - - return feeds - - -URL_USER_RSS = 'https://grouple.co/user/rss/315828?filter=' - -DIR = Path(__file__).resolve().parent -FILE_NAME_LAST_FEED = DIR / 'last_feed' -FILE_NAME_SKIP = DIR / 'skip' - - -def save_last_feed(feed): - open(FILE_NAME_LAST_FEED, 'w', encoding='utf-8').write(feed) - - -if __name__ == '__main__': - notified_by_sms = True - - # Загрузка последней новости - try: - last_feed = open(FILE_NAME_LAST_FEED, encoding='utf-8').read() - except: - last_feed = "" - - while True: - if FILE_NAME_SKIP.exists(): - log.info('Обнаружен файл "%s", пропускаю проверку.', FILE_NAME_SKIP.name) - wait(days=7) - continue - - try: - log.debug('get_feeds_by_manga_chapters') - log.debug('Last feed: "%s"', last_feed) - - current_feeds = get_feeds_by_manga_chapters(URL_USER_RSS) - log.debug('current_feeds: %s', current_feeds) - - if not last_feed or last_feed not in current_feeds: - # Считаем что это первый запуск - last_feed = current_feeds[0] - log.debug('Первый запуск, запоминаю последнюю главу: "{}"'.format(last_feed)) - - save_last_feed(last_feed) - - # Если последняя новость есть в списке текущих новостей - else: - index = current_feeds.index(last_feed) - - # Получаем список новостей после последней новости - new_feeds = current_feeds[:index] - if new_feeds: - last_feed = new_feeds[0] - save_last_feed(last_feed) - - log.debug('Вышло:') - for manga in new_feeds: - log.debug(' ' + manga) - - if notified_by_sms: - text = 'Новые главы: {}'.format(len(new_feeds)) - simple_send_sms(text, log) - - else: - log.debug('Новых глав нет') - - wait(days=7) - - except requests.exceptions.ConnectionError as e: - log.warning('Ошибка подключения к сети: %s', e) - log.debug('Через минуту попробую снова...') - - wait(minutes=1) - - except: - log.exception('Непредвиденная ошибка:') - log.debug('Через 5 минут попробую снова...') - - wait(minutes=5) diff --git a/Check with notification/Check new books vitaly-zykov/books b/Check with notification/Check new books vitaly-zykov/books deleted file mode 100644 index b7684b3c0..000000000 --- a/Check with notification/Check new books vitaly-zykov/books +++ /dev/null @@ -1,26 +0,0 @@ -[ - "Безымянный раб", - "Наёмник его величества", - "Под знаменем пророчества", - "Владыка Сардуора", - "Власть силы, том 1", - "Власть силы, том 2", - "Великие Спящие, том 1", - "Великие Спящие, том 2", - "Безымянный раб (аудиоверсия)", - "Наёмник его величества (аудиоверсия)", - "Под знаменем пророчества (аудиоверсия)", - "Владыка Сардуора (аудиоверсия)", - "Власть силы, том 1 (аудиоверсия)", - "Власть силы, том 2 (аудиоверсия)", - "Великие Спящие (аудиоверсия), том 1", - "Великие Спящие (аудиоверсия), том 2", - "Конклав Бессмертных. В краю далёком", - "Конклав Бессмертных. Проба сил", - "Во имя потерянных душ", - "Конклав Бессмертных. В краю далёком (аудиоверсия)", - "Конклав Бессмертных. Проба сил (аудиоверсия)", - "Во имя потерянных душ (аудиоверсия)", - "Малк", - "Праймлорд или Хозяин Одинокой Башни" -] \ No newline at end of file diff --git a/Check with notification/Check new books vitaly-zykov/main.py b/Check with notification/Check new books vitaly-zykov/main.py deleted file mode 100644 index 6500a9d3e..000000000 --- a/Check with notification/Check new books vitaly-zykov/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых книг Зыкова. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка книг -sys.path.append('../../html_parsing') - -from all_common import make_backslashreplace_console, run_notification_job -from vitaly_zykov_ru_knigi__get_books import get_books - - -make_backslashreplace_console() - - -run_notification_job( - 'vitaly-zykov new books', - 'books', - get_books, - notified_by_sms=True, - format_current_items='Текущий список книг (%s): %s', - format_get_items='Запрос списка книг', - format_items='Список книг (%s): %s', - format_new_item='Появилась новая книга Зыкова: "%s"', - format_no_new_items='Новых книг нет', -) diff --git a/Check with notification/Check new chapters The Legendary Moonlight Sculptor/chapters b/Check with notification/Check new chapters The Legendary Moonlight Sculptor/chapters deleted file mode 100644 index 54e18dd17..000000000 --- a/Check with notification/Check new chapters The Legendary Moonlight Sculptor/chapters +++ /dev/null @@ -1,632 +0,0 @@ -[ - "Том 1 - Предисловие.", - "Том 1 - Глава 1: Рождение темного геймера.", - "Том 1 - Глава 2: Появление дикого зверя.", - "Том 1 - Глава 3: Просьба инструктора.", - "Том 1 - Глава 4: Виид Ужасный.", - "Том 1 - Глава 5: Девочка, забывшая слова.", - "Том 1 - Глава 6: За барбекю.", - "Том 1 - Глава 7: Маэстро сражений.", - "Том 1 - Глава 8: Профессия, что предначертана судьбой.", - "Том 1 - Глава 9: Небесный город.", - "Том 1 - Глава 10: Роль Виида в карательном отряде.", - "Том 1 - Глава 11: Легендарная земля.", - "Том 1 - Глава 12: Статуя Фреи.", - "Том 1 - Глава 13: Утерянное Сокровище Храма.", - "Том 1 - Глава 14: Небесный город Лавиас.", - "Том 2 - Глава 15: Не больше, не меньше.", - "Том 2 - Глава 16: Значение Королевской дороги.", - "Том 2 - Глава 17: Безымянные статуи Виида и Дайни.", - "Том 2 - Глава 18: Потерянное сокровище Фреи.", - "Том 2 - Глава 19: Рыцарь принцессы.", - "Том 2 - Глава 20: Горы барахла.", - "Том 3 - Глава 1: Профессия: Легендарный Скульптор Лунного Света.", - "Том 3 - Глава 2: Камень, вбирающий молнии.", - "Том 3 - Глава 3: Заговор и инструменты.", - "Том 3 - Глава 4: Унижение Ван Хона.", - "Том 3 - Глава 5: Возвращение святыни.", - "Том 3 - Глава 6: Простуда.", - "Том 3 - Глава 7: Невежество новичков.", - "Том 3 - Глава 8: Король вампиров.", - "Том 3 - Глава 9: Прекрасные скульптуры.", - "Том 3 - Глава 10: Битва в Черном замке.", - "Том 3 - Глава 11: Странное выступление на ТВ.", - "Том 4 - Глава 1: Земли отчаяния.", - "Том 4 - Глава 2: Мастер на все руки.", - "Том 4 - Глава 3: Аукцион в свободном городе Сомурен.", - "Том 4 - Глава 4: Скульптурное мастерство Дарона.", - "Том 4 - Глава 5: Скульптурное мастерство метаморфоз.", - "Том 4 - Глава 6: Осада крепости Одеин.", - "Том 4 - Глава 7: Охота в подземелье.", - "Том 4 - Глава 8: Охота Виида.", - "Том 4 - Глава 9: Ассоциация темных геймеров.", - "Том 4 - Глава 10: Самая большая гробница.", - "Том 5 - Глава 1: Усыпальница Великого Короля.", - "Том 5 - Глава 2: Возведение королевской усыпальницы.", - "Том 5 - Глава 3: Сила алкоголя.", - "Том 5 - Глава 4: Оценки.", - "Том 5 - Глава 5: Деревня изгнанных.", - "Том 5 - Глава 6: Изменение.", - "Том 5 - Глава 7: Простой и невежественный орк Каричи!", - "Том 5 - Глава 8: Война орков.", - "Том 5 - Глава 9: Зал славы.", - "Том 5 - Глава 10: Выбор.", - "Том 6 - Глава 1: Восторг.", - "Том 6 - Глава 2: Собеседование.", - "Том 6 - Глава 3: Первый раз в кинотеатре.", - "Том 6 - Глава 4: Никому нельзя доверять.", - "Том 6 - Глава 5: Быстрый Каричи.", - "Том 6 - Глава 6: Доспехи Даллока.", - "Том 6 - Глава 7: Прошлые связи.", - "Том 6 - Глава 8: Странный спутник.", - "Том 6 - Глава 9: Ее скульптуры.", - "Том 7 - Глава 1: Жажда битвы.", - "Том 7 - Глава 2: Армия нежити.", - "Том 7 - Глава 3: Богатое королевство.", - "Том 7 - Глава 4: Квест.", - "Том 7 - Глава 5: Сила, способная обмануть смерть.", - "Том 7 - Глава 6: К центру мира.", - "Том 7 - Глава 7: Деньги приходят и уходят.", - "Том 7 - Глава 8: Горная дорога.", - "Том 7 - Глава 9: Поход за славой.", - "Том 8 - Глава 1: Родиум.", - "Том 8 - Глава 2: Золотая статуя.", - "Том 8 - Глава 3: Силуэт золотой статуи.", - "Том 8 - Глава 4: Северная экспедиция.", - "Том 8 - Глава 5: Техника лунного света.", - "Том 8 - Глава 6: Подземелье.", - "Том 8 - Глава 7: Концерт классической музыки.", - "Том 8 - Глава 8: Принудительное задание.", - "Том 8 - Глава 9: Долина смерти.", - "Том 9 - Глава 1: Ночная Мора.", - "Том 9 - Глава 2: Сбор продовольствия.", - "Том 9 - Глава 3: Северное Чудо!", - "Том 9 - Глава 4: Сражение.", - "Том 9 - Глава 5: В пещере.", - "Том 9 - Глава 6: Мужской роман.", - "Том 9 - Глава 7: Скульптура лунного света.", - "Том 9 - Глава 8: Университет.", - "Том 9 - Глава 9: Что посеешь…", - "Том 9 - Глава 10: Рейд.", - "Том 9 - Глава 11: Бог войны Виид.", - "Том 10 - Глава 1: Виид – воин скелет.", - "Том 10 - Глава 2: Возрожденный.", - "Том 10 - Глава 3: Призыв нежити.", - "Том 10 - Глава 4: Преимущество искусства нападения.", - "Том 10 - Глава 5: Сокровище империи Нифльхейм.", - "Том 10 - Глава 6: День рождения.", - "Том 10 - Глава 7: Лук высшего эльфа.", - "Том 10 - Глава 8: Башня света.", - "Том 10 - Глава 9: Сбор.", - "Том 10 - Глава 10: Тодум.", - "Том 11 - Глава 1: Земля вампиров.", - "Том 11 - Глава 2: Сейлун.", - "Том 11 - Глава 3: Задание.", - "Том 11 - Глава 4: Спасение Розерин.", - "Том 11 - Глава 5: Первый урок Ли Хэна.", - "Том 11 - Глава 6: Дорога.", - "Том 11 - Глава 7: Письмо вампиров.", - "Том 11 - Глава 8: Теплящаяся надежда.", - "Том 11 - Глава 9: Программа 'Виид'. Начало регулярного эфира!", - "Том 11 - Глава 10: Выездное мероприятие.", - "Том 12 - Глава 1: День решающей битвы.", - "Том 12 - Глава 2: Битва в Тодуме.", - "Том 12 - Глава 3: Ад Сильмидо.", - "Том 12 - Глава 4: Дикие условия и курс ада.", - "Том 12 - Глава 5: Раскрытие личности Ли Хэна.", - "Том 12 - Глава 6: Тень трофеев.", - "Том 12 - Глава 7: Победа в небе и на земле.", - "Том 12 - Глава 8: Перехвативший военное знамя Кольдрим.", - "Том 12 - Глава 9: Дух скульптурного мастерства.", - "Том 12 - Глава 10: Башня Героев.", - "Том 13 - Предисловие", - "Том 13 - Глава 1. Скелет-рыцарь", - "Том 13 - Глава 2. Рыцарь принцессы", - "Том 13 - Глава 3. Историческая битва", - "Том 13 - Глава 4. Возвращение Виида", - "Том 13 - Глава 5. Символ процветания Моры", - "Том 13 - Глава 6. Скульптура дочери", - "Том 13 - Глава 7. Порочный правитель Моры", - "Том 13 - Глава 8. Королевство гномов", - "Том 13 - Глава 9. Гном Артхэнд", - "Том 13 - Глава 10. Дьявольский дракон Кэйберн", - "Том 14 - Глава 1. Деревня Железная рука", - "Том 14 - Глава 2. Курысо", - "Том 14 - Глава 3. Тайна скульпторского задания", - "Том 14 - Глава 4. Содрогающий командующий", - "Том 14 - Глава 5. Битва с Рукой смерти", - "Том 14 - Глава 6. Мечта континента", - "Том 14 - Глава 7. Водные игры маленьких гномов", - "Том 14 - Глава 8. Создание духов", - "Том 14 - Глава 9. Разговор тёмных геймеров", - "Том 14 - Глава 10. Гильдия 'Пустынные странники'", - "Том 14 - Глава 11. Ожидания Дайни", - "Том 15 - Глава 1. Подарки гномов", - "Том 15 - Глава 2. Неисследованное подземелье", - "Том 15 - Глава 3. Странная брошюра Гилдраса", - "Том 15 - Глава 4. Проявленные способности", - "Том 15 - Глава 5. Дева чистоты", - "Том 15 - Глава 6. Свидание раба", - "Том 15 - Глава 7. Любопытство Смита", - "Том 15 - Глава 8. Восстановление империи Нифльхейм", - "Том 15 - Глава 9. Скульптор Ада", - "Том 15 - Глава 10. Скульптор на быке", - "Том 15 - Глава 11. Скульптуры, возведенные на реке Плача", - "Том 16 - Глава 1. Разыскивается", - "Том 16 - Глава 2. Альянс освободителей", - "Том 16 - Глава 3. Власть вождя", - "Том 16 - Глава 4. Божественный кодекс военных тактик и стратегий", - "Том 16 - Глава 5. Стратегия Чжугэ Ляна", - "Том 16 - Глава 6. Чёрный дракон", - "Том 16 - Глава 7. Девушка мечты Виида", - "Том 16 - Глава 8. Главнокомандующий", - "Том 16 - Глава 9. Возвращение тирана", - "Том 17 - Глава 1. Дурная репутация Виида", - "Том 17 - Глава 2. Кузнец магических мечей", - "Том 17 - Глава 3. Правитель Моры", - "Том 17 - Глава 4. Прелесть медной монеты", - "Том 17 - Глава 5. Визит Союн", - "Том 17 - Глава 6. Скульптура ребёнка", - "Том 17 - Глава 7. Жизнь одной девочки", - "Том 17 - Глава 8. Встреча с Дайни", - "Том 17 - Глава 9. Скиталец", - "Том 17 - Глава 10. Злополучные отношения с Бадыреем", - "Том 18 - Глава 1. Запечатанные воспоминания императора Арпена", - "Том 18 - Глава 2. Путешествие Человека", - "Том 18 - Глава 3. Крылья", - "Том 18 - Глава 4. Архитектура империи Арпен", - "Том 18 - Глава 5. Создание культа травяной каши", - "Том 18 - Глава 6. Песнь Виида.", - "Том 18 - Глава 7. Оборона Моры", - "Том 18 - Глава 8. Переговоры о мире", - "Том 18 - Глава 9. История скульптуры", - "Том 18 - Глава 10. Корабль-призрак", - "Том 18 - Глава 11. Капитан Деорол", - "Том 19 - Глава 1. Капитан корабля-призрака", - "Том 19 - Глава 2. Ужин на острове Ипия", - "Том 19 - Глава 3. Ходовые качества корабля", - "Том 19 - Глава 4. Подводное сражение Виида", - "Том 19 - Глава 5. Потерянный Пиратский Флот", - "Том 19 - Глава 6. Следуя за Авророй", - "Том 19 - Глава 7. Авантюрист Джиголаса", - "Том 19 - Глава 8. Наследие Скульптора", - "Том 19 - Глава 9. Прибытие Союн", - "Том 19 - Глава 10. Пределы Некроманта", - "Том 20 - Глава 1: Трогательная встреча.", - "Том 20 - Глава 2: Появление незваных гостей.", - "Том 20 - Глава 3: Ночь Нежити.", - "Том 20 - Глава 4: Битва во время извержения вулкана.", - "Том 20 - Глава 5: История Слоуа.", - "Том 20 - Глава 6: Подземелье Инферно.", - "Том 20 - Глава 7: Огненные Гиганты.", - "Том 20 - Глава 8: Война племен Джиголаса.", - "Том 20 - Глава 9: Виид снова выкрутился.", - "Том 20 - Глава 10: Свадьба Слоуа.", - "Том 21 - Глава 1: Тайна основания Империи.", - "Том 21 - Глава 2: Шахта Скульпторов.", - "Том 21 - Глава 3: Кофейное свидание.", - "Том 21 - Глава 4: Мифриловые Ангелы.", - "Том 21 - Глава 5: Послание в бутылке.", - "Том 21 - Глава 6: Чудо дарования жизни.", - "Том 21 - Глава 7: Аура смерти.", - "Том 21 - Глава 8: Контратака во время дождя, ветра и тумана.", - "Том 21 - Глава 9: Битва в открытом море.", - "Том 21 - Глава 10: Друзья.", - "Том 22 - Предисловие", - "Том 22 - Глава 1: Сумка для русалки.", - "Том 22 - Глава 2: Грандиозные Здания.", - "Том 22 - Глава 3: Объявляющий Победу Рог Тиерезиса.", - "Том 22 - Глава 4: Визит в главный офис.", - "Том 22 - Глава 5: Ваяние Природы.", - "Том 22 - Глава 6: Скульптор, странствующий по континенту.", - "Том 22 - Глава 7: Собор Фреи.", - "Том 22 - Глава 8: Наперекор судьбе.", - "Том 23 - Глава 1: Низкоранговый Скелет.", - "Том 23 - Глава 2: Мастер Меча Эш.", - "Том 23 - Глава 3: Призрак Капуи.", - "Том 23 - Глава 4: Неразрушимый песочный замок.", - "Том 23 - Глава 5: Каньон Рыцаря Смерти.", - "Том 23 - Глава 6: Крепость Фургалов.", - "Том 23 - Глава 7: Песнь Рыцаря Смерти.", - "Том 23 - Глава 8: Ваяние Великого Природного Бедствия.", - "Том 23 - Глава 9: Нападение Гильдии Гермес.", - "Том 23 - Глава 10: Великий Король Эваннов.", - "Том 24 - Глава 1: Мятежный план Рыцаря Возмездия.", - "Том 24 - Глава 2: Столкновение с Гильдией Гермес.", - "Том 24 - Глава 3: Пугающая идентификация.", - "Том 24 - Глава 4: Скульптура из гелия.", - "Том 24 - Глава 5: Изгнание Нежити.", - "Том 24 - Глава 6: Три Костяных Дракона.", - "Том 24 - Глава 7: Вызов шторма.", - "Том 24 - Глава 8: Хозяин Крепости Варго.", - "Том 24 - Глава 9: Снегопад в Рождественскую ночь.", - "Том 24 - Глава 10: Просьба старой служанки.", - "Том 24 - Глава 11: Вызов Виида.", - "Том 25 - Глава 1: Вознесение Бадырея.", - "Том 25 - Глава 2: Грэйпасс.", - "Том 25 - Глава 3: Произведения искусства Захаба.", - "Том 25 - Глава 4: Скульптурное наследие Захаба.", - "Том 25 - Глава 5: Секретный навык Владения Мечом.", - "Том 25 - Глава 6: Сияющий Меч, оставленный в скульптуре.", - "Том 25 - Глава 7: Старая горничная из Королевства Розенхайм.", - "Том 25 - Глава 8: Дневник Королевы Ивэйн.", - "Том 25 - Глава 9: Вторжение Церкви Эмбиню.", - "Том 25 - Глава 10: Побег из Крепости Серабург.", - "Том 26 - Глава 1: Подземный проход.", - "Том 26 - Глава 2: Беженцы Крепости Серабург.", - "Том 26 - Глава 3: Решение Виида.", - "Том 26 - Глава 4: Потоп и Сфинкс.", - "Том 26 - Глава 5: Церковь Лю.", - "Том 26 - Глава 6: Происхождение Скульптурного Ремесла.", - "Том 26 - Глава 7: История четырех рас.", - "Том 26 - Глава 8: Месторасположение Ратцебурга.", - "Том 26 - Глава 9: Открытие первого города.", - "Том 26 - Глава 10: Былая слава орков.", - "Том 26 - Глава 11: История орков.", - "Том 27 - Глава 1: Земляные скульптуры.", - "Том 27 - Глава 2: Гончарное дело.", - "Том 27 - Глава 3: Задание орков.", - "Том 27 - Глава 4: Скульптуры птичьих яиц.", - "Том 27 - Глава 5: Рождение керамической профессии.", - "Том 27 - Глава 6: Кузница Гестии.", - "Том 27 - Глава 7: Правитель королевства.", - "Том 27 - Глава 8: Предложение работы от гномов.", - "Том 27 - Глава 9: Встреча с расой оживлённых скульптур.", - "Том 28 - Глава 1: Рудники Мельборна", - "Том 28 - Глава 2: Бадырей и Королевская Гвардия", - "Том 28 - Глава 3: Вумба Белкаин", - "Том 28 - Глава 4: Надвигающаяся опасность", - "Том 28 - Глава 5: Выкопанная Ледяная Красотка", - "Том 28 - Глава 6: Специальный Реюньон", - "Том 28 - Глава 7: Адская дуэль", - "Том 28 - Глава 8: Воровство Брони", - "Том 28 - Глава 9: Вторая секретная техника фехтования", - "Том 28 - Глава 10: Перерождение Гелия", - "Том 28 - Глава 11: Событие на 30 Золотых", - "Том 29 - Глава 1: Неудавшаяся скульптура.", - "Том 29 - Глава 2: Сад богов.", - "Том 29 - Глава 3: Выбор орков.", - "Том 29 - Глава 4: Худшая судьба.", - "Том 29 - Глава 5: Руины гробницы короля Белсоса.", - "Том 29 - Глава 6: Последний Мастер скульптуры.", - "Том 29 - Глава 7: Скульптура короля духов.", - "Том 29 - Глава 8: Выбор северных правителей.", - "Том 29 - Глава 9: Рабочие будни оживленных скульптур.", - "Том 29 - Глава 10: Ловушка племени Саллионов.", - "Том 30 - Глава 1: Ужасная катастрофа в скалистом каньоне.", - "Том 30 - Глава 2: Огненный воитель.", - "Том 30 - Глава 3: Гора Тубкал.", - "Том 30 - Глава 4: Новая статуя в городе.", - "Том 30 - Глава 5: Тернистый путь черного рыцаря.", - "Том 30 - Глава 6: Статуя на площади.", - "Том 30 - Глава 7: Ненасытная жажда власти гильдии Гермес.", - "Том 30 - Глава 8: В поисках утраченного Бахармонга.", - "Том 30 - Глава 9: Ужасные монстры Антарозы.", - "Том 30 - Глава 10: Путь одинокого воина.", - "Том 31 - Глава 1: Время новых секретных техник.", - "Том 31 - Глава 2: Неукротимая мощь Бахаморга.", - "Том 31 - Глава 3: Загадочная улыбка Союн.", - "Том 31 - Глава 4: Битва за замок Сислей.", - "Том 31 - Глава 5: Тяжелая доля темного игрока.", - "Том 31 - Глава 6: Будучи калибри.", - "Том 31 - Глава 7: Кругосветное путешествие маленькой птички.", - "Том 31 - Глава 8: Бог любит троицу.", - "Том 31 - Глава 9: Мучения культа Эмбрью.", - "Том 31 - Глава 10: Кстати, о героях.", - "Том 31 - Глава 11: Белая взрослая черепаха.", - "Том 32 - Глава 1: Расширение королевства Арпен.", - "Том 32 - Глава 2: Трясина.", - "Том 32 - Глава 3: Общий сбор всех паладинов.", - "Том 32 - Глава 4: Бесконечный лабиринт Родерика.", - "Том 32 - Глава 5: Самоотверженность паладинов.", - "Том 32 - Глава 6: Хозяин снова дома.", - "Том 32 - Глава 7: Убийца демонов.", - "Том 32 - Глава 8: Мясорубка.", - "Том 32 - Глава 9: Самая невероятная победа.", - "Том 32 - Глава 10: Шокирующие исследования Родерика.", - "Том 33 - Глава 1: Морское сражение культа Травяной каши.", - "Том 33 - Глава 2: Наземное сражение культа Травяной каши.", - "Том 33 - Глава 3: Верхом на Желтом.", - "Том 33 - Глава 4: Победитель сражения на полях Рупи.", - "Том 33 - Глава 5: Нодюлль и Хильдеран.", - "Том 33 - Глава 6: Время прошлого.", - "Том 33 - Глава 7: Версальский континент в прошлом.", - "Том 33 - Глава 8: Пламенное Бедствие.", - "Том 33 - Глава 9: Пойманная в прошлом Союн.", - "Том 33 - Глава 10: Перекресток жизни и смерти.", - "Том 34 - Глава 1: Страдания Нодюлля.", - "Том 34 - Глава 2: Захаб и Королева Ивэйн.", - "Том 34 - Глава 3: Битва в Королевском Замке Порту.", - "Том 34 - Глава 4: Замок Муноджи.", - "Том 34 - Глава 5: Скульптура, творящая чудеса.", - "Том 34 - Глава 6: Неожиданная измена.", - "Том 34 - Глава 7: Пустыня Спокойствия.", - "Том 34 - Глава 8: Человек, вызывающий дождь.", - "Том 34 - Глава 9: Герой прошлого.", - "Том 35 - Глава 1: Победа Империи Хэйвен.", - "Том 35 - Глава 2: Покорение Метапеи.", - "Том 35 - Глава 3: Непревзойдённый воин.", - "Том 35 - Глава 4: Во времена войны.", - "Том 35 - Глава 5: Воин, спасающий мир.", - "Том 35 - Глава 6: Исчезающий город.", - "Том 35 - Глава 7: Война в прошлом.", - "Том 35 - Глава 8: Становление тирана.", - "Том 36 - Глава 1: Командующий Армией Тьмы, Ван Хок.", - "Том 36 - Глава 2: Задание на последнюю секретную скульптурную технику.", - "Том 36 - Глава 3: Прекрасная жертва.", - "Том 36 - Глава 4: Сильнейший Воин.", - "Том 36 - Глава 5: Тьма, страх и чума.", - "Том 36 - Глава 6: Могущество Ван Хока.", - "Том 36 - Глава 7: Вечный напарник по имени Захаб.", - "Том 36 - Глава 8: Питон идёт на север.", - "Том 36 - Глава 9: Армия Эмбиню.", - "Том 37 - Глава 1: Невезение Крепости Дулмор.", - "Том 37 - Глава 2: Подготовка к превосходству.", - "Том 37 - Глава 3: Пришествие Злого Воина.", - "Том 37 - Глава 4: Глазами Бури.", - "Том 37 - Глава 5: Конец Виида.", - "Том 37 - Глава 6: Мощь Эмбиню.", - "Том 37 - Глава 7: Пункт назначения.", - "Том 37 - Глава 8: След в истории.", - "Том 37 - Глава 9: Мощь Пэллос.", - "Том 37 - Глава 10: Вторжение Империи Хэйвен.", - "Том 38 - Глава 1: Разбросанные подчиненные.", - "Том 38 - Глава 2: Рабочие Небесной Башни.", - "Том 38 - Глава 3: Глаза Арендодателя.", - "Том 38 - Глава 4: Люди севера.", - "Том 38 - Глава 5: Воля Великого Императора Виида.", - "Том 38 - Глава 6: Обрушение Башни.", - "Том 38 - Глава 7: Дракон Хаоса Асоллет.", - "Том 38 - Глава 8: Сумасшествие Дракона.", - "Том 38 - Глава 9: Бурый Медведь и Черный Дракон.", - "Том 39 - Глава 1: Драконий кризис.", - "Том 39 - Глава 2: Аватар Эмбиню.", - "Том 39 - Глава 3: Тернистый путь.", - "Том 39 - Глава 4: Эра Эрозии.", - "Том 39 - Глава 5: Заключительный этап.", - "Том 39 - Глава 6: Финал Нодюлля.", - "Том 39 - Глава 7: Ваяние Времени.", - "Том 39 - Глава 8: Морские Сокровища.", - "Том 39 - Глава 9: Возвращение Короля.", - "Том 40 - Глава 1: Внутренние дела Королевства Арпен.", - "Том 40 - Глава 2: Отряд Не Задающей Вопросы Убийственной Каши.", - "Том 40 - Глава 3: Рыцарь Бездны и Гильдия Гермес.", - "Том 40 - Глава 4: Правитель Империи Хэйвен.", - "Том 40 - Глава 5: Встреча с подчинёнными.", - "Том 40 - Глава 6: Мост Альказар.", - "Том 40 - Глава 7: Отряд Небесной Каши.", - "Том 40 - Глава 8: Задание Великого Императора.", - "Том 40 - Глава 9: Воскрешение ненавистного подчинённого.", - "Том 41 - Глава 1: Мимолётные терзания героя.", - "Том 41 - Глава 2: Пришествие героя.", - "Том 41 - Глава 3: Встретившиеся бедствия.", - "Том 41 - Глава 4: Лобовая атака.", - "Том 41 - Глава 5: Песнь Виида.", - "Том 41 - Глава 6: Война, которую нельзя проиграть.", - "Том 41 - Глава 7: Изгибы войны.", - "Том 41 - Глава 8: Крах Королевского Дворца.", - "Том 41 - Глава 9: Несчастье Империи Хэйвен.", - "Том 42 - Глава 1. Реабилитация", - "Том 42 - Глава 2. Последняя просьба Хестигера", - "Том 42 - Глава 3. Внутренние дела", - "Том 42 - Глава 4. Непомерная жадность", - "Том 42 - Глава 5. Инцидент у Крепости Варго", - "Том 42 - Глава 6. Инвестиции", - "Том 42 - Глава 7. Вербовка", - "Том 42 - Глава 8. Красивый Мир", - "Том 43 - Глава 1. Суть Ваяния Времени", - "Том 43 - Глава 2. Вызов Тенейдон", - "Том 43 - Глава 3. Квест дракона", - "Том 43 - Глава 4. Мир фей", - "Том 43 - Глава 5. День нападения", - "Том 43 - Глава 6. Чрезвычайная ситуация", - "Том 43 - Глава 7. Бог Войны Виид", - "Том 43 - Глава 8. Сокрушительный удар", - "Том 43 - Глава 9. Набег на Империю Хэйвен", - "Том 43 - Глава 10. Долг Чёрного Рыцаря", - "Том 44 - Глава 1. Пробуждение Бога Войны", - "Том 44 - Глава 2. Отец Союн", - "Том 44 - Глава 3. Особенности Рыцарей на Грифонах Мьюла", - "Том 44 - Глава 4. Упадок империи", - "Том 44 - Глава 5. Благородная скульптура", - "Том 44 - Глава 6. Великий мастер", - "Том 44 - Глава 7. Вознаграждение Ратуаса", - "Том 44 - Глава 8. Набег Гильдии Гермес", - "Том 45 - Глава 1. Славная нажива", - "Том 45 - Глава 2. Грозный враг Империи Хэйвен", - "Том 45 - Глава 3. Прибыль Виида", - "Том 45 - Глава 4. Крепость Пухоль", - "Том 45 - Глава 5. Раскрытый план", - "Том 45 - Глава 6. Присоединение авианов", - "Том 45 - Глава 7. Подлинный герой", - "Том 46 - Глава 1. Предложение капитуляции", - "Том 46 - Глава 2. Последствия для Гильдии Гермес", - "Том 46 - Глава 3. Идея, как заработать целое состояние", - "Том 46 - Глава 4. Крупнейшая постройка севера", - "Том 46 - Глава 5. Пухольский аквапарк", - "Том 46 - Глава 6. День, когда время остановилось", - "Том 46 - Глава 7. Пустыня Спокойствия", - "Том 46 - Глава 8. Безрассудный вызов", - "Том 47 - Глава 1. Зарождение звезды", - "Том 47 - Глава 2. Звезда-ребёнок", - "Том 47 - Глава 3. Самый трудолюбивый ремесленник", - "Том 47 - Глава 4. Вторая профессия", - "Том 47 - Глава 5. Меч Лоа", - "Том 47 - Глава 6. Рекорды охоты.", - "Том 47 - Глава 7. Гнев Джинг Дюк-су.", - "Том 47 - Глава 8. Ловушка Виида.", - "Том 48 - Глава 1. Замок Каллапик.", - "Том 48 - Глава 2. Раскачка общественного мнения.", - "Том 48 - Глава 3. Мечта великого злодея.", - "Том 48 - Глава 4. Семена атаки.", - "Том 48 - Глава 5. Демон Обжорства.", - "Том 48 - Глава 6. Волнения в Форте Один.", - "Том 48 - Глава 7. Осада Форта Один.", - "Том 48 - Глава 8. Жертва Со Юн.", - "Том 49 - Глава 1. Начало похода.", - "Том 49 - Глава 2-1. Начало похода.", - "Том 49 - Глава 2-2. Начало похода.", - "Том 49 - Глава 3-1. Начало похода.", - "Том 49 - Глава 3-2. Начало похода.", - "Том 49 - Глава 4-1. Морское Сражении в Нерии.", - "Том 49 - Глава 4-2. Морское Сражении в Нерии.", - "Том 49 - Глава 5-1. Морское Сражении в Нерии.", - "Том 49 - Глава 5-2. Морское Сражении в Нерии.", - "Том 49 - Глава 6-1. Морское Сражении в Нерии.", - "Том 49 - Глава 6-2. Морское Сражении в Нерии.", - "Том 49 - Глава 7-1. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 7-2. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 8-1. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 8-2. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 9. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 10. Появление Ордена Железа и Стальных Рыцарей.", - "Том 49 - Глава 11. Освобождение Свободного Города Самурен (часть 1).", - "Том 49 - Глава 12. Освобождение Свободного Города Самурен (часть 2)", - "Том 49 - Глава 13. Освобождение Свободного Города Самурен (часть 3)", - "Том 49 - Глава 14. Освобождение свободного города Самурен (часть 4)", - "Том 49 - Глава 15. Око за око (часть 1)", - "Том 49 - Глава 16. Око за око (часть 2).", - "Том 49 - Глава 17. Око за око (часть 3)", - "Том 49 - Глава 18. Око за око (часть 4)", - "Том 49 - Глава 19. Маг Тьмы (часть 1)", - "Том 49 - Глава 20. Маг Тьмы (часть 2)", - "Том 49 - Глава 21. Маг Тьмы (часть 3)", - "Том 49 - Глава 22. Маг Тьмы (часть 4)", - "Том 49 - Глава 23.1. Лобовая атака (часть 1)", - "Том 49 - Глава 23.2. Лобовая атака (часть 1)", - "Том 49 - Глава 24.1. Лобовая атака (часть 2)", - "Том 49 - Глава 24.2. Лобовая атака (часть 2)", - "Том 49 - Глава 25. Герои собираются (часть 1)", - "Том 49 - Глава 26.1. Герои собираются (часть 2)", - "Том 49 - Глава 26.2. Герои собираются (часть 2)", - "Том 49 - Глава 27. Герои Собираются (часть 3)", - "Том 50 - Глава 1. Ваяние Времени", - "Том 50 - Глава 1. Ваяние времени (часть 2)", - "Том 50 - Глава 2. Небольшой отдых (часть 1)", - "Том 50 - Глава 2. Небольшой отдых (часть 2)", - "Том 50 - Глава 3. Изменения на Равнине Гарнав (часть 1)", - "Том 50 - Глава 3. Изменения на Равнине Гарнав (часть 2)", - "Том 50 - Глава 4. Ради этого прекрасного моря", - "Том 50 - Глава 4. Ради этого прекрасного моря (часть 2)", - "Том 50 - Глава 4. Ради этого прекрасного моря (часть 3)", - "Том 50 - Глава 4. Ради этого прекрасного моря (часть 4)", - "Том 50 - Глава 5. Спрятанное оружие (часть 1)", - "Том 50 - Глава 5. Спрятанное оружие (часть 2)", - "Том 50 - Глава 5. Спрятанное оружие (часть 3)", - "Том 50 - Глава 5. Спрятанное оружие (часть 4)", - "Том 50 - Глава 6. Визит Мьюла (часть 1)", - "Том 50 - Глава 6. Визит Мьюла (часть 2)", - "Том 50 - Глава 6. Визит Мьюла (часть 3)", - "Том 50 - Глава 7. Центр подготовки продвинутого уровня (часть 1)", - "Том 50 - Глава 7. Центр подготовки продвинутого уровня (часть 2)", - "Том 50 - Глава 7. Центр подготовки продвинутого уровня (часть 3)", - "Том 50 - Глава 8.1.1 Новое достижение (часть 1.1)", - "Том 50 - Глава 8.1.2 Новое достижение (часть 1.2)", - "Том 50 - Глава 8.2 Новое достижение (часть 2)", - "Том 50 - Глава 8.3.1 Новое достижение (часть 3.1)", - "Том 50 - Глава 8.3.2 Новое достижение (часть 3.2)", - "Том 50 - Глава 9.1. Баттали, Бог Сражений (часть 1)", - "Том 50 - Глава 9.2. Баттали, Бог Сражений (часть 1)", - "Том 50 - Глава 9. Баталли, Бог сражений (часть 2)", - "Том 51 - Глава 1. Звезда Палланка (часть 1)", - "Том 51 - Глава 1. Звезда Палланка (часть 2)", - "Том 51 - Глава 1. Звезда Палланки (часть 3)", - "Том 51 - Глава 1. Звезда Палланка (часть 4)", - "Том 51 - Глава 1. Звезда Палланки (часть 5)", - "Том 51 - Глава 2. Призыв Пылающих Метеоров (часть 1)", - "Том 51 - Глава 2.1. Призыв Пылающих Метеоров (часть 2)", - "Том 51 - Глава 2.2. Призыв Пылающих Метеоров (часть 2)", - "Том 51 - Глава 2. Призыв Пылающих Метеоров (часть 3)", - "Том 51 - Глава 3. Песня Виида (часть 1)", - "Том 51 - Глава 3. Песня Виида (часть 2)", - "Том 51 - Глава 3. Песня Виида (часть 3)", - "Том 51 - Глава 4. Битва Орка Каричи (часть 1)", - "Том 51 - Глава 4. Битва Орка Каричи (часть 2)", - "Том 51 - Глава 4. Битва Орка Каричи (часть 3)", - "Том 51 - Глава 4. Битва Орка Каричи (часть 4)", - "Том 51 - Глава 5. Рыцарь для Третьей Виверны (часть 1)", - "Том 51 - Глава 5. Рыцарь для Третьей Виверны (часть 2)", - "Том 51 - Глава 6.1. Вырваться из ловушки (часть 1)", - "Том 51 - Глава 6.2. Вырваться из ловушки (часть 1)", - "Том 51 - Глава 6. Вырваться из ловушки (часть 2)", - "Том 51 - Глава 7.1. Бог Войны (часть 1)", - "Том 51 - Глава 7.2.1. Бог Войны (часть 2)", - "Том 51 - Глава 7.2.2. Бог Войны (часть 2)", - "Том 51 - Глава 7.3. Бог Войны (часть 3)", - "Том 51 - Глава 8.1. Крайняя мера (часть 1)", - "Том 51 - Глава 8.2. Крайняя мера (часть 2)", - "Том 51 - Глава 8.3. Крайняя мера (часть 3)", - "Том 52 - Глава 1.1. Темное Владычество (часть 1)", - "Том 52 - Глава 1.2. Темное Владычество (часть 1)", - "Том 52 - Глава 2.1. Темное Владычество (часть 2)", - "Том 52 - Глава 2.2. Темное Владычество (часть 2)", - "Том 52 - Глава 3.1. Темное Владычество (часть 3)", - "Том 52 - Глава 3.2. Темное Владычество (часть 3)", - "Том 52 - Глава 4.1. Катастрофическая контратака (часть 1)", - "Том 52 - Глава 4.2. Катастрофическая контратака (часть 1)", - "Том 52 - Глава 5.1. Катастрофическая контратака (часть 2)", - "Том 52 - Глава 5.2. Катастрофическая контратака (часть 2)", - "Том 52 - Глава 6.1. Одиннадцать Виидов (часть 1)", - "Том 52 - Глава 6.2. Одиннадцать Виидов (часть 1)", - "Том 52 - Глава 7.1. Одиннадцать Виидов (часть 2)", - "Том 52 - Глава 7.2. Одиннадцать Виидов (часть 2)", - "Том 52 - Глава 8. Одиннадцать Виидов (часть 3)", - "Том 52 - Глава 9. Конец Императора Гейхара (часть 1)", - "Том 52 - Глава 10.1. Конец Императора Гейхара (часть 2)", - "Том 52 - Глава 10.2. Конец Императора Гейхара (часть 2)", - "Том 52 - Глава 11.1. Конец Императора Гейхара (часть 3)", - "Том 52 - Глава 11.2. Конец Императора Гейхара (часть 3)", - "Том 52 - Глава 12.1. Рушащаяся Империя (часть 1)", - "Том 52 - Глава 12.2. Рушащаяся Империя (часть 2)", - "Том 52 - Глава 13. Рушащаяся Империя (часть 3)", - "Том 52 - Глава 14. Виид и Бадырей (часть 1)", - "Том 52 - Глава 15. Виид и Бадырей (часть 2)", - "Том 52 - Глава 16.1. Виид и Бадырей (часть 3)", - "Том 52 - Глава 16.2. Виид и Бадырей (часть 3)", - "Том 52 - Глава 17. Виид и Бадырей (часть 4)", - "Том 52 - Глава 18.1. Виид и Бадырей (часть 5)", - "Том 52 - Глава 18.2. Виид и Бадырей (часть 5)", - "Том 52 - Глава 19.1. Виид и Бадырей (часть 6)", - "Том 52 - Глава 19.2. Виид и Бадырей (часть 6)", - "Том 52 - Глава 20.1. Ярость Кэйберна (часть 1)", - "Том 52 - Глава 20.2. Ярость Кэйберна (часть 1)", - "Том 52 - Глава 21.1. Ярость Кэйберна (часть 2)", - "Том 52 - Глава 21.2. Ярость Кэйберна (часть 2)", - "Том 52 - Глава 22.1. Ярость Кэйберна (часть 3)", - "Том 52 - Глава 22.2. Ярость Кэйберна (часть 3)", - "Том 52 - Глава 23.1. Два дракона (часть 1)", - "Том 52 - Глава 23.2. Два дракона (часть 1)", - "Том 52 - Глава 24.1. Два дракона (часть 2)", - "Том 52 - Глава 24.2. Два дракона (часть 2)", - "Том 52 - Глава 25.1. Два дракона (часть 3)", - "Том 52 - Глава 25.2. Два дракона (часть 3)", - "Том 53 - Глава 1.1. Черный дракон (часть 1)", - "Том 53 - Глава 1.2. Черный дракон (часть 1)", - "Том 53 - Глава 2.1. Черный дракон (часть 2)", - "Том 53 - Глава 2.2. Черный дракон (часть 2)", - "Том 53 - Глава 3.1. Черный дракон (часть 3)", - "Том 53 - Глава 3.2. Черный дракон (часть 3)", - "Том 53 - Глава 4.1. Охота на дракона (часть 1)", - "Том 53 - Глава 4.2. Охота на дракона (часть 1)", - "Том 53 - Глава 5. Охота на дракона (часть 2)", - "Том 53 - Глава 6.1. Охота на дракона (часть 3)", - "Том 53 - Глава 6.2. Охота на дракона (часть 3)", - "Том 53 - Глава 7.1. Охота на дракона (часть 4)", - "Том 53 - Глава 7.2. Охота на дракона (часть 4)", - "Том 53 - Глава 8.1. Замок Арен в огне (часть 1)", - "Том 53 - Глава 8.2. Замок Арен в огне (часть 1)", - "Том 53 - Глава 9.1. Замок Арен в огне (часть 2)", - "Том 53 - Глава 9.2. Замок Арен в огне (часть 2)", - "Том 53 - Глава 10.1. Континентальное господство (часть 1)", - "Том 53 - Глава 10.2. Континентальное господство (часть 1)", - "Том 53 - Глава 11.1. Континентальное господство (часть 2)", - "Том 53 - Глава 11.2. Континентальное господство (часть 2)", - "Том 53 - Глава 12.1. Континентальное господство (часть 3)", - "Том 53 - Глава 12.2. Континентальное господство (часть 3)", - "Том 53 - Глава 13.1. Стратегия развития Империи Арпен (часть 1)", - "Том 53 - Глава 13.2. Стратегия развития Империи Арпен (часть 1)", - "Том 53 - Глава 14.1. Стратегия развития Империи Арпен (часть 2)", - "Том 53 - Глава 14.2. Стратегия развития Империи Арпен (часть 2)", - "Том 53 - Глава 15.1. Стратегия развития Империи Арпен (часть 3)", - "Том 53 - Глава 15.2. Стратегия развития Империи Арпен (часть 3)" -] \ No newline at end of file diff --git a/Check with notification/Check new chapters The Legendary Moonlight Sculptor/main.py b/Check with notification/Check new chapters The Legendary Moonlight Sculptor/main.py deleted file mode 100644 index f3bacb36d..000000000 --- a/Check with notification/Check new chapters The Legendary Moonlight Sculptor/main.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых главах Легендарного лунного скульптора. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка глав -sys.path.append('../../html_parsing') - -from all_common import make_backslashreplace_console, run_notification_job -from ranobehub_org_api_ranobe_92_contents__The_Legendary_Moonlight_Sculptor import get_chapters - - -make_backslashreplace_console() - - -run_notification_job( - 'New chapters The Legendary Moonlight Sculptor', - 'chapters', - get_chapters, - timeout={'days': 2}, - notified_by_sms=True, - format_current_items='Текущий список глав (%s): %s', - format_get_items='Запрос списка глав', - format_items='Список глав (%s): %s', - format_new_item='Лунный скульптор: "%s"', - format_no_new_items='Новых глав нет', -) diff --git a/Check with notification/Check new game Sable Maze/games b/Check with notification/Check new game Sable Maze/games deleted file mode 100644 index d83529ad4..000000000 --- a/Check with notification/Check new game Sable Maze/games +++ /dev/null @@ -1 +0,0 @@ -["Sable Maze: Forbidden Garden Collector's Edition", "Sable Maze: Nightmare Shadows Collector's Edition", "Sable Maze: Sinister Knowledge Collector's Edition", "Sable Maze: Soul Catcher Collector's Edition", "Sable Maze: Sullivan River Collector's Edition", "Sable Maze: Twelve Fears Collector's Edition"] \ No newline at end of file diff --git a/Check with notification/Check new game Sable Maze/main.py b/Check with notification/Check new game Sable Maze/main.py deleted file mode 100644 index 84021e88a..000000000 --- a/Check with notification/Check new game Sable Maze/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых игр серии Sable Maze. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка игр -sys.path.append('../../bigfishgames_com__hidden_object') - -from all_common import make_backslashreplace_console, run_notification_job -from find__Sable_Maze__CE import get_games - - -make_backslashreplace_console() - - -run_notification_job( - 'new game Sable Maze', - 'games', - get_games, - notified_by_sms=True, - format_current_items='Текущий список игр (%s): %s', - format_get_items='Запрос списка игр', - format_items='Список игр (%s): %s', - format_new_item='Появилась новая игра "%s"', - format_no_new_items='Новых игр нет', -) diff --git a/Check with notification/Check new game Saga of the Nine Worlds/games b/Check with notification/Check new game Saga of the Nine Worlds/games deleted file mode 100644 index c0079172f..000000000 --- a/Check with notification/Check new game Saga of the Nine Worlds/games +++ /dev/null @@ -1 +0,0 @@ -["Saga of the Nine Worlds: The Four Stags Collector's Edition", "Saga of the Nine Worlds: The Gathering Collector's Edition", "Saga of the Nine Worlds: The Hunt Collector's Edition"] \ No newline at end of file diff --git a/Check with notification/Check new game Saga of the Nine Worlds/main.py b/Check with notification/Check new game Saga of the Nine Worlds/main.py deleted file mode 100644 index f14d7bf29..000000000 --- a/Check with notification/Check new game Saga of the Nine Worlds/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых игр серии Saga of the Nine Worlds. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка игр -sys.path.append('../../bigfishgames_com__hidden_object') - -from all_common import make_backslashreplace_console, run_notification_job -from find__Saga_of_the_Nine_Worlds__CE import get_games - - -make_backslashreplace_console() - - -run_notification_job( - 'new game Saga of the Nine Worlds', - 'games', - get_games, - notified_by_sms=True, - format_current_items='Текущий список игр (%s): %s', - format_get_items='Запрос списка игр', - format_items='Список игр (%s): %s', - format_new_item='Появилась новая игра "%s"', - format_no_new_items='Новых игр нет', -) diff --git a/Check with notification/Check new game Spirits of Mystery/games b/Check with notification/Check new game Spirits of Mystery/games deleted file mode 100644 index 17829c452..000000000 --- a/Check with notification/Check new game Spirits of Mystery/games +++ /dev/null @@ -1,14 +0,0 @@ -[ - "Spirits of Mystery: Amber Maiden Collector's Edition", - "Spirits of Mystery: Chains of Promise Collector's Edition", - "Spirits of Mystery: Family Lies Collector's Edition", - "Spirits of Mystery: Illusions Collector's Edition", - "Spirits of Mystery: Song of the Phoenix Collector's Edition", - "Spirits of Mystery: The Dark Minotaur Collector's Edition", - "Spirits of Mystery: The Fifth Kingdom Collector's Edition", - "Spirits of Mystery: The Last Fire Queen Collector's Edition", - "Spirits of Mystery: The Lost Queen Collector's Edition", - "Spirits of Mystery: The Moon Crystal Collector's Edition", - "Spirits of Mystery: The Silver Arrow Collector's Edition", - "Spirits of Mystery: Whisper of the Past Collector's Edition" -] \ No newline at end of file diff --git a/Check with notification/Check new game Spirits of Mystery/main.py b/Check with notification/Check new game Spirits of Mystery/main.py deleted file mode 100644 index 8673af776..000000000 --- a/Check with notification/Check new game Spirits of Mystery/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых игр серии Spirits of Mystery. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка игр -sys.path.append('../../bigfishgames_com__hidden_object') - -from all_common import make_backslashreplace_console, run_notification_job -from find__Spirits_of_Mystery__CE import get_games - - -make_backslashreplace_console() - - -run_notification_job( - 'new game Spirits of Mystery', - 'games', - get_games, - notified_by_sms=True, - format_current_items='Текущий список игр (%s): %s', - format_get_items='Запрос списка игр', - format_items='Список игр (%s): %s', - format_new_item='Появилась новая игра "%s"', - format_no_new_items='Новых игр нет', -) diff --git a/Check with notification/Check new rank ru.stackoverflow/main.py b/Check with notification/Check new rank ru.stackoverflow/main.py deleted file mode 100644 index 8bc7d39a1..000000000 --- a/Check with notification/Check new rank ru.stackoverflow/main.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о изменении ранга на ru.stackoverflow. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - - -from all_common import make_backslashreplace_console, get_logger, simple_send_sms, wait - - -make_backslashreplace_console() - - -import time -import requests - - -log = get_logger('Check new rank ru.stackoverflow') - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/65b8d0c0a18eea726370b5b17e8a84ce00914aeb/stackoverflow/user_rank_and_reputation.py -def get_user_rank_and_reputation() -> (str, str): - url = 'https://stackexchange.com/leagues/filter-users/609/AllTime/2015-03-27/?filter=gil9red&sort=reputationchange' - - import requests - rs = requests.get(url) - text = rs.text - # print(text) - - import re - match = re.search('>#(.+) all time rank', text) - rank = match.group(1) - - match = re.search('>(.+) all time reputation', text) - reputation = match.group(1).replace(',', '') - - return rank, reputation - - -FILE_NAME_LAST_RANK = 'last_rank' - - -def update_file_data(value: str): - open(FILE_NAME_LAST_RANK, mode='w', encoding='utf-8').write(value) - - -if __name__ == '__main__': - notified_by_sms = True - - try: - last_rank = open(FILE_NAME_LAST_RANK, encoding='utf-8').read() - except: - last_rank = '' - - while True: - try: - log.debug('get rank and reputation') - log.debug('last_rank: %s', last_rank if last_rank else '') - - rank, reputation = get_user_rank_and_reputation() - - log.debug('current rank: %s, reputation: %s', rank, reputation) - - # Если предыдущий ранг не был известен, например при первом запуске скрипта - if not last_rank: - log.debug('Обнаружен первый запуск') - - last_rank = rank - update_file_data(last_rank) - - else: - if last_rank != rank: - text = 'Изменился ранг: {} -> {} ({})'.format(last_rank, rank, reputation) - log.debug(text) - - # Обновление последнего ранга - last_rank = rank - update_file_data(last_rank) - - if notified_by_sms: - simple_send_sms(text, log) - - else: - log.debug('Ранг не изменился') - - wait(weeks=1) - - except requests.exceptions.ConnectionError as e: - log.warning('Ошибка подключения к сети: %s', e) - log.debug('Через минуту попробую снова...') - - time.sleep(60) - - except: - log.exception('Непредвиденная ошибка:') - log.debug('Через 5 минут попробую снова...') - - time.sleep(5 * 60) diff --git a/Check with notification/Check new video Final Fantasy Series Story/main.py b/Check with notification/Check new video Final Fantasy Series Story/main.py deleted file mode 100644 index 3cfe3983b..000000000 --- a/Check with notification/Check new video Final Fantasy Series Story/main.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых видео истории серии Final Fantasy. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка видео -sys.path.append('../../html_parsing') - -from all_common import make_backslashreplace_console, run_notification_job -from youtube_com__get_video_list import get_video_list - - -make_backslashreplace_console() - - -def my_get_video_list(): - text = 'История серии Final Fantasy' - url = 'https://www.youtube.com/user/StopGameRu/search?query=' + text - - return get_video_list(url, filter_func=lambda name: text.lower() in name.lower()) - - -if __name__ == '__main__': - run_notification_job( - 'Check new video Final Fantasy Series Story', - 'video', - my_get_video_list, - notified_by_sms=True, - format_current_items='Текущий список видео (%s): %s', - format_get_items='Запрос видео', - format_items='Список видео (%s): %s', - format_new_item='Новое видео "%s"', - format_no_new_items='Изменений нет', - ) diff --git a/Check with notification/Check new video Final Fantasy Series Story/video b/Check with notification/Check new video Final Fantasy Series Story/video deleted file mode 100644 index aad1a2991..000000000 --- a/Check with notification/Check new video Final Fantasy Series Story/video +++ /dev/null @@ -1,4 +0,0 @@ -[ - "История серии Final Fantasy, часть 1. Всё о Final Fantasy I, Dragon Quest, Nintendo и JRPG", - "История серии Final Fantasy, часть 2. Всё о Final Fantasy II, Dragon Quest III и Nintendo" -] \ No newline at end of file diff --git a/Check with notification/Check new video Gorgeous Freeman/main.py b/Check with notification/Check new video Gorgeous Freeman/main.py deleted file mode 100644 index 0ae7e46ff..000000000 --- a/Check with notification/Check new video Gorgeous Freeman/main.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых видео Gorgeous Freeman. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка видео -sys.path.append('../../html_parsing') - -from all_common import make_backslashreplace_console, run_notification_job -from youtube_com__get_video_list import get_video_list - - -make_backslashreplace_console() - - -def my_get_video_list(): - text = 'Gorgeous Freeman -' - url = 'https://www.youtube.com/user/antoine35DeLak/search?query=' + text - - return get_video_list(url, filter_func=lambda name: text in name) - - -if __name__ == '__main__': - run_notification_job( - 'new video Gorgeous Freeman', - 'video', - my_get_video_list, - notified_by_sms=True, - format_current_items='Текущий список видео (%s): %s', - format_get_items='Запрос видео', - format_items='Список видео (%s): %s', - format_new_item='Новое видео "%s"', - format_no_new_items='Изменений нет', - ) diff --git a/Check with notification/Check new video Gorgeous Freeman/video b/Check with notification/Check new video Gorgeous Freeman/video deleted file mode 100644 index 1786aba25..000000000 --- a/Check with notification/Check new video Gorgeous Freeman/video +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Gorgeous Freeman - Episode 1 - The Suit", - "Gorgeous Freeman - Episode 2 - The Crowbar", - "Gorgeous Freeman - Episode 3 - The Part 1" -] \ No newline at end of file diff --git a/Check with notification/Check new video My Hero Academia/main.py b/Check with notification/Check new video My Hero Academia/main.py deleted file mode 100644 index dd998454a..000000000 --- a/Check with notification/Check new video My Hero Academia/main.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых серий аниме My Hero Academia. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка видео -sys.path.append('../../online_anidub_com') - -from all_common import make_backslashreplace_console, run_notification_job -from get_video_list import search_video_list - - -make_backslashreplace_console() - - -run_notification_job( - 'new video My Hero Academia', - 'video', - lambda: search_video_list('Моя геройская академия'), - notified_by_sms=True, - timeout={'weeks': 2}, - format_current_items='Текущий список видео (%s): %s', - format_get_items='Запрос видео', - format_items='Список видео (%s): %s', - format_new_item='Новая серия "%s"', - format_no_new_items='Изменений нет', -) diff --git a/Check with notification/Check new video My Hero Academia/video b/Check with notification/Check new video My Hero Academia/video deleted file mode 100644 index 1fa81c58b..000000000 --- a/Check with notification/Check new video My Hero Academia/video +++ /dev/null @@ -1,7 +0,0 @@ -[ - "Моя геройская академия ТВ-4 [25 из 25]", - "Моя геройская академия: Два героя", - "Моя геройская академия ТВ-3 [25 из 25]", - "Моя геройская академия ТВ-2 [25 из 25]", - "Моя геройская академия [13 из 13]" -] \ No newline at end of file diff --git a/Check with notification/Check new video Sally Face/main.py b/Check with notification/Check new video Sally Face/main.py deleted file mode 100644 index 9ce6bd2b7..000000000 --- a/Check with notification/Check new video Sally Face/main.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых видео Sally Face. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка видео -sys.path.append('../../html_parsing') - -from all_common import make_backslashreplace_console, run_notification_job -from youtube_com__get_video_list import get_video_list - - -make_backslashreplace_console() - - -def my_get_video_list(): - text = 'Sally Face' - url = 'https://www.youtube.com/user/HellYeahPlay/search?query=' + text - - return get_video_list(url, filter_func=lambda name: text in name and 'эпизод' in name.lower()) - - -if __name__ == '__main__': - run_notification_job( - 'new video Sally Face', - 'video', - my_get_video_list, - notified_by_sms=True, - format_current_items='Текущий список видео (%s): %s', - format_get_items='Запрос видео', - format_items='Список видео (%s): %s', - format_new_item='Новое видео "%s"', - format_no_new_items='Изменений нет', - ) diff --git a/Check with notification/Check new video Sally Face/video b/Check with notification/Check new video Sally Face/video deleted file mode 100644 index d3a3bbf0a..000000000 --- a/Check with notification/Check new video Sally Face/video +++ /dev/null @@ -1,14 +0,0 @@ -[ - "ОККУЛЬТНАЯ РАСЧЛЕНЕНКА - Sally Face [ЭПИЗОД 3] #7", - "ЗАГАДОЧНАЯ СМЕРТЬ - Sally Face [ЭПИЗОД 4] #11", - "ЗЛО ВОЗВРАЩАЕТСЯ - Sally Face [ЭПИЗОД 4] #10", - "РАСЧЛЕНЕНКА У СОСЕДЕЙ 🔪 Sally Face [ЭПИЗОД 2] #3", - "ПРИЗРАК В УЖАСНОЙ КВАРТИРЕ - Sally Face [ЭПИЗОД 1] #2", - "БИТВА С БОССОМ - Sally Face [ЭПИЗОД 4] [ФИНАЛ] #12", - "БЕЗУМИЕ - Sally Face [ВСЕ СТРАНИЦЫ ДНЕВНИКА] [ЭПИЗОД 3] #8", - "СТРАШНЫЙ РИТУАЛ ☦️ Sally Face [ЭПИЗОД 2] #5", - "ЗАГАДОЧНАЯ РАСЧЛЕНЕНКА - Sally Face [ЭПИЗОД 1] #1", - "ТВОРЕНИЯ ВЕЛЬЗЕВУЛА - Sally Face [ЭПИЗОД 4] #9", - "СТРАННАЯ КОЛБАСА - Sally Face [ЭПИЗОД 3] #6", - "ПОИСК МЕРТВЫХ ЛЮДЕЙ ☠️ Sally Face [ЭПИЗОД 2] #4" -] \ No newline at end of file diff --git "a/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/main.py" "b/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/main.py" deleted file mode 100644 index b6be17d3c..000000000 --- "a/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/main.py" +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для уведомления о появлении новых серий аниме Богиня благословляет этот прекрасный мир. - -""" - - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - -# Чтобы импортировать функцию для получения списка видео -sys.path.append('../../online_anidub_com') - -from all_common import make_backslashreplace_console, run_notification_job -from get_video_list import search_video_list - - -make_backslashreplace_console() - - -run_notification_job( - 'new video Богиня благословляет этот прекрасный мир', - 'video', - lambda: search_video_list('Богиня благословляет этот прекрасный мир'), - notified_by_sms=True, - timeout={'weeks': 2}, - format_current_items='Текущий список видео (%s): %s', - format_get_items='Запрос видео', - format_items='Список видео (%s): %s', - format_new_item='Новая серия "%s"', - format_no_new_items='Изменений нет', -) diff --git "a/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/video" "b/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/video" deleted file mode 100644 index 0a3856bdb..000000000 --- "a/Check with notification/Check new video \320\221\320\276\320\263\320\270\320\275\321\217 \320\261\320\273\320\260\320\263\320\276\321\201\320\273\320\276\320\262\320\273\321\217\320\265\321\202 \321\215\321\202\320\276\321\202 \320\277\321\200\320\265\320\272\321\200\320\260\321\201\320\275\321\213\320\271 \320\274\320\270\321\200/video" +++ /dev/null @@ -1,7 +0,0 @@ -[ - "Богиня благословляет этот прекрасный мир! Багровая легенда", - "Богиня благословляет этот прекрасный мир", - "Богиня благословляет этот прекрасный мир ТВ-2 [10 из 10]", - "Богиня благословляет этот прекрасный мир OVA", - "Богиня благословляет этот прекрасный мир [10 из 10]" -] \ No newline at end of file diff --git a/Check with notification/Check pwned/main.py b/Check with notification/Check pwned/main.py deleted file mode 100644 index 22eb7dfbc..000000000 --- a/Check with notification/Check pwned/main.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time - -import requests - -import sys -sys.path.append('..') -sys.path.append('../../selenium__examples') - -from all_common import make_backslashreplace_console, get_logger, simple_send_sms, wait -from check__haveibeenpwned_com import do_check - - -make_backslashreplace_console() - - -log = get_logger('Check pwned') - - -FILE_NAME_LAST_VALUE = 'last_value' -DIR_SAVE_PWNED_SCREENSHOTS = 'pwned_screenshots' -CHECK_EMAIL = 'ilya.petrash@inbox.ru' - - -def update_file_data(value: str): - open(FILE_NAME_LAST_VALUE, mode='w', encoding='utf-8').write(value) - - -if __name__ == '__main__': - notified_by_sms = True - - try: - last_value = open(FILE_NAME_LAST_VALUE, encoding='utf-8').read() - except: - last_value = '' - - while True: - try: - log.debug('Check pwned') - log.debug("Last value: %s", repr(last_value) if last_value else '') - - value = do_check(CHECK_EMAIL, DIR_SAVE_PWNED_SCREENSHOTS) - log.debug(f'Current value: {value!r}') - - if not last_value: - log.debug('Обнаружен первый запуск') - - last_value = value - update_file_data(last_value) - - else: - if last_value != value: - text = 'PWNED' - log.debug(text) - - last_value = value - update_file_data(last_value) - - if notified_by_sms: - simple_send_sms(text, log) - - else: - log.debug('Ничего не поменялось...') - - wait(weeks=1) - - except requests.exceptions.ConnectionError as e: - log.warning('Ошибка подключения к сети: %s', e) - log.debug('Через минуту попробую снова...') - - time.sleep(60) - - except: - log.exception('Непредвиденная ошибка:') - log.debug('Через 5 минут попробую снова...') - - time.sleep(5 * 60) diff --git a/Check with notification/all_common.py b/Check with notification/all_common.py deleted file mode 100644 index e3f110891..000000000 --- a/Check with notification/all_common.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import Callable, List, Union - - -def make_backslashreplace_console(): - # При выводе юникодных символов в консоль винды - # Возможно, не только для винды, но и для любой платформы стоит использовать - # эту настройку -- мало какие проблемы могут встретиться - import sys - if sys.platform == 'win32': - import codecs - - try: - sys.stdout = codecs.getwriter(sys.stdout.encoding)(sys.stdout.detach(), 'backslashreplace') - sys.stderr = codecs.getwriter(sys.stderr.encoding)(sys.stderr.detach(), 'backslashreplace') - - except AttributeError: - # ignore "AttributeError: '_io.BufferedWriter' object has no attribute 'encoding'" - pass - - -# Import https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py -# Absolute path. Analog '../wait' -from pathlib import Path -import sys -sys.path.append(str(Path(__file__).resolve().parent.parent / 'wait')) -from wait import wait - - -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True) -> 'logging.Logger': - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - if log_file: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - if log_stdout: - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -def send_sms(api_id: str, to: str, text: str, log): - if not api_id or not to: - log.warning('Параметры api_id или to не указаны, отправка СМС невозможна!') - return - - log.info(f'Отправка sms: {text!r}') - - if len(text) > 70: - text = text[:70-3] + '...' - log.info(f'Текст sms будет сокращено, т.к. слишком длинное (больше 70 символов): {text!r}') - - # Отправляю смс на номер - url = 'https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}'.format( - api_id=api_id, - to=to, - text=text - ) - log.debug(repr(url)) - - while True: - try: - import requests - rs = requests.get(url) - log.debug(repr(rs.text)) - - break - - except: - log.exception("При отправке sms произошла ошибка:") - log.debug('Через 5 минут попробую снова...') - - # Wait 5 minutes before next attempt - import time - time.sleep(5 * 60) - - -def simple_send_sms(text: str, log=None): - # Если логгер не определен, тогда создаем свой, который логирует в консоль - if not log: - log = get_logger('all_common', log_file=False) - - from all_config import API_ID, TO - return send_sms(API_ID, TO, text, log) - - -def run_notification_job( - log__or__log_name: Union['logging.Logger', str], - file_name_items: str, - get_new_items: Callable[[], List[str]], - notified_by_sms=True, - timeout={'weeks': 1}, - timeout_exception_seconds=5 * 60, - format_first_start_detected='Обнаружен первый запуск', - format_current_items='Текущий список (%s): %s', - format_get_items='Запрос списка', - format_items='Список (%s): %s', - format_new_item='Появился новый элемент "%s"', - format_no_new_items='Новых элементов нет', - format_on_exception='Ошибка:', - format_on_exception_next_attempt='Через 5 минут попробую снова...', -): - log = log__or__log_name - if isinstance(log, str): - log = get_logger(log) - - def save_items(items: List[str]): - with open(file_name_items, mode='w', encoding='utf-8') as f: - import json - json.dump(items, f, ensure_ascii=False, indent=4) - - def read_items() -> List[str]: - try: - with open(file_name_items, encoding='utf-8') as f: - import json - obj = json.load(f) - - # Должен быть список, но если в файле будет что-то другое -- это будет неправильно - if not isinstance(obj, list): - return [] - - return obj - - except: - return [] - - FILE_NAME_SKIP = Path('skip') - - # Загрузка текущего списка из файла - current_items = read_items() - log.debug(format_current_items, len(current_items), current_items) - - while True: - if FILE_NAME_SKIP.exists(): - log.info('Обнаружен файл "%s", пропускаю проверку.', FILE_NAME_SKIP.name) - wait(**timeout) - continue - - try: - log.debug(format_get_items) - - items = get_new_items() - log.debug(format_items, len(items), items) - - # Если текущих список пустой - if not current_items: - log.debug(format_first_start_detected) - - current_items = items - save_items(current_items) - - else: - new_items = set(items) - set(current_items) - if new_items: - current_items = items - save_items(current_items) - - for item in new_items: - text = format_new_item % item - log.debug(text) - - if notified_by_sms: - simple_send_sms(text, log) - - else: - log.debug(format_no_new_items) - - wait(**timeout) - - except: - log.exception(format_on_exception) - log.debug(format_on_exception_next_attempt) - - # Wait before next attempt - import time - time.sleep(timeout_exception_seconds) diff --git a/Check with notification/all_config.py b/Check with notification/all_config.py deleted file mode 100644 index d9de506cd..000000000 --- a/Check with notification/all_config.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -API_ID = '' -TO = '' diff --git a/Check with notification/games_with_RTX/db.py b/Check with notification/games_with_RTX/db.py deleted file mode 100644 index 198359c9d..000000000 --- a/Check with notification/games_with_RTX/db.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT - -from peewee import * - -# Absolute file name -import pathlib -DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / 'games.sqlite') - - -def db_create_backup(backup_dir='backup'): - import datetime as DT - import os - import shutil - - os.makedirs(backup_dir, exist_ok=True) - - file_name = str(DT.datetime.today().date()) + '.sqlite' - file_name = os.path.join(backup_dir, file_name) - - shutil.copy(DB_FILE_NAME, file_name) - - -# Ensure foreign-key constraints are enforced. -db = SqliteDatabase(DB_FILE_NAME, pragmas={'foreign_keys': 1}) - - -class BaseModel(Model): - class Meta: - database = db - - -class Game(BaseModel): - name = TextField(unique=True) - url = TextField(unique=True) - img_base64 = TextField() - append_date = DateField(default=DT.date.today) - - def __str__(self): - return f'Game(id={self.id}, title={self.name!r}, ' \ - f'url={self.url!r}, ' \ - f'img_base64=<{len(self.img_base64)} chars>)' - - -db.connect() -db.create_tables([Game]) - - -if __name__ == '__main__': - for game in Game.select(): - print(game) diff --git a/Check with notification/games_with_RTX/main.py b/Check with notification/games_with_RTX/main.py deleted file mode 100644 index 1bc3de0b3..000000000 --- a/Check with notification/games_with_RTX/main.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -sys.path.append('..') - -# Для импортирования kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py -sys.path.append('../../html_parsing') - -from all_common import get_logger, wait, simple_send_sms -from kanobu_ru__games__collections__igry_s_podderzhkoi_rtx import get_games -from db import db_create_backup, Game - - -log = get_logger(__file__) - - -while True: - log.debug('Запуск') - - is_empty = not Game.select().count() - if is_empty: - log.debug('Обнаружен первый запуск') - else: - db_create_backup() - - try: - has_new_game = False - - games = get_games() - log.debug(f'Обработка {len(games)} игр') - - for game in games: - game_db, created = Game.get_or_create( - name=game.name, - url=game.url - ) - if game_db.img_base64 != game.img_base64: - game_db.img_base64 = game.img_base64 - game_db.save() - - if not created: - continue - - has_new_game = True - log.debug(f'Добавление новой игры с RTX: {game.name!r}') - - # При первом запуске не нужно информировать по СМС - if not is_empty: - simple_send_sms(f'RTX: {game.name}', log) - - if not has_new_game: - log.debug(f'Новых игр нет') - - wait(weeks=1) - - except Exception as e: - log.exception('Ошибка:') - - wait(minutes=15) - - print() diff --git a/Check with notification/games_with_RTX/web.py b/Check with notification/games_with_RTX/web.py deleted file mode 100644 index a56544f1e..000000000 --- a/Check with notification/games_with_RTX/web.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from db import Game - -from flask import Flask, render_template_string -app = Flask(__name__) - - -@app.route('/') -def index(): - title = 'Games with RTX' - URL = 'https://kanobu.ru/games/collections/igry-s-podderzhkoi-rtx/' - headers = ['#', 'Игра', 'Дата добавления'] - games = Game.select().order_by(Game.id.desc()) - - return render_template_string(""" - - - - - {{ title }} - - - - - - - - - - - - {% for header in headers %} - - {% endfor %} - - - {% for game in games %} - - - - - - {% endfor %} - -
{{ title }} ({{ games.count() }})
{{ header }}
{{ loop.index }} - - - -
  {{ game.name }}
-
{{ game.append_date }}
- - - """, games=games, headers=headers, title=title, URL=URL) - - -if __name__ == "__main__": - app.debug = True - - # Localhost - app.run( - # Включение поддержки множества подключений - threaded=True, - port=15555, - ) - - # # Public IP - # app.run(host='0.0.0.0') diff --git a/Check with notification/games_with_denuvo/common.py b/Check with notification/games_with_denuvo/common.py deleted file mode 100644 index a5d8db764..000000000 --- a/Check with notification/games_with_denuvo/common.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT - -# Чтобы можно было импортировать all_common.py, находящийся уровнем выше -import sys -sys.path.append('..') - - -from all_common import get_logger, wait, simple_send_sms - - -# make_backslashreplace_console() - - -DEBUG = False -# DEBUG = True - - -if DEBUG: - DB_FILE_NAME = 'test.database.sqlite' - - log = get_logger('test_games_with_denuvo', file='test_log.txt') - log_cracked_games = get_logger('test_cracked_games', file='test_cracked_games.log.txt', log_stdout=False) - -else: - DB_FILE_NAME = 'database.sqlite' - - log = get_logger('games_with_denuvo') - log_cracked_games = get_logger('cracked_games', file='cracked_games.log.txt', log_stdout=False) - - -def create_connect(): - import sqlite3 - return sqlite3.connect(DB_FILE_NAME) - - -def init_db(): - # Создание базы и таблицы - connect = create_connect() - try: - connect.execute(''' - CREATE TABLE IF NOT EXISTS Game ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - is_cracked BOOLEAN NOT NULL, - - append_date TIMESTAMP DEFAULT NULL, - release_date TIMESTAMP DEFAULT NULL, - crack_date TIMESTAMP DEFAULT NULL, - - CONSTRAINT name_unique UNIQUE (name) - ); - ''') - - # # NOTE: Пример, когда нужно в таблице подправить схему: - # connect.executescript(''' - # DROP TABLE IF EXISTS Game2; - # - # CREATE TABLE IF NOT EXISTS Game2 ( - # id INTEGER PRIMARY KEY, - # name TEXT NOT NULL, - # is_cracked BOOLEAN NOT NULL, - # - # append_date TIMESTAMP DEFAULT NULL, - # release_date TIMESTAMP DEFAULT NULL, - # crack_date TIMESTAMP DEFAULT NULL, - # - # CONSTRAINT name_unique UNIQUE (name) - # ); - # - # -- INSERT INTO Game2 SELECT id, name, is_cracked, date('now'), crack_date FROM Game; - # INSERT INTO Game2 (id, name, is_cracked, append_date, crack_date) - # SELECT id, name, is_cracked, append_date, crack_date FROM Game; - # - # DROP TABLE Game; - # ALTER TABLE Game2 RENAME TO Game; - # ''') - - connect.commit() - - finally: - connect.close() - - -def db_create_backup(backup_dir='backup'): - from datetime import datetime - file_name = str(datetime.today().date()) + '.sqlite' - - import os - if not os.path.exists(backup_dir): - os.mkdir(backup_dir) - - file_name = os.path.join(backup_dir, file_name) - - import shutil - shutil.copy(DB_FILE_NAME, file_name) - - -def update_release_date(connect, name: str, release_date: DT.date): - sql = "UPDATE Game SET release_date = ? WHERE name = ? AND release_date IS NULL" - connect.execute(sql, [release_date, name]) - - -def append_list_games(games: [(str, DT.date, bool)], notified_by_sms=True): - connect = create_connect() - - def insert(name: str, release_date: DT.date, is_cracked: bool) -> bool: - # Для отсеивания дубликатов - has = connect.execute("SELECT 1 FROM Game WHERE name = ?", [name]).fetchone() - if has: - return False - - log.debug(f'Добавляю {name!r} ({is_cracked})') - sql = "INSERT OR IGNORE INTO Game (name, is_cracked, append_date, release_date) VALUES (?, ?, date('now'), ?)" - connect.execute(sql, [name, release_date, is_cracked]) - - # Если добавлена уже взломанная игра, указываем дату - if is_cracked: - connect.execute("UPDATE Game SET crack_date = date('now') WHERE name = ?", [name]) - - return True - - try: - for name, release_date, is_cracked in games: - ok = insert(name, release_date, is_cracked) - - update_release_date(connect, name, release_date) - - # Игра уже есть в базе, нужно проверить ее статус is_cracked, возможно, он поменялся и игру взломали - if not ok: - rs_is_cracked = connect.execute('SELECT is_cracked FROM Game where name = ?', [name]).fetchone()[0] - - # Если игра раньше имела статус is_cracked = False, а теперь он поменялся на True: - if not rs_is_cracked and is_cracked: - # Поменяем флаг у игры в базе - connect.execute("UPDATE Game SET is_cracked = 1, crack_date = date('now') WHERE name = ?", [name]) - - text = f'Игру {name!r} взломали' - log.info(text) - log_cracked_games.debug(text) - - # При DEBUG = True, отправки смс не будет - if notified_by_sms and not DEBUG: - simple_send_sms(text, log) - - elif is_cracked: - text = f'Добавлена взломанная игра {name!r}' - log.info(text) - log_cracked_games.debug(text) - - # При DEBUG = True, отправки смс не будет - if notified_by_sms and not DEBUG: - simple_send_sms(text, log) - - connect.commit() - - finally: - connect.close() - - -def append_list_games_which_denuvo_is_removed(games: [str, DT.date], notified_by_sms=True): - connect = create_connect() - - def insert(name: str, release_date: DT.date) -> bool: - # Для отсеивания дубликатов - has = connect.execute("SELECT 1 FROM Game WHERE name = ?", [name]).fetchone() - if has: - return False - - log.debug(f'Добавляю игру с убранной защитой {name!r}') - - sql = "INSERT OR IGNORE INTO Game (name, is_cracked, append_date, crack_date, release_date) " \ - "VALUES (?, 1, date('now'), date('now'), ?)" - connect.execute(sql, [name, release_date]) - return True - - try: - for name, release_date in games: - ok = insert(name, release_date) - - update_release_date(connect, name, release_date) - - # Игра уже есть в базе, нужно проверить ее статус is_cracked, возможно, он поменялся и игру взломали - if ok: - text = f'Добавлена игра с убранной защитой {name!r}' - log.info(text) - log_cracked_games.debug(text) - - # При DEBUG = True, отправки смс не будет - if notified_by_sms and not DEBUG: - simple_send_sms(text, log) - - else: - rs_is_cracked = connect.execute('SELECT is_cracked FROM Game where name = ?', [name]).fetchone()[0] - - # Если игра раньше имела статус is_cracked = False - if not rs_is_cracked: - # Поменяем флаг у игры в базе - connect.execute("UPDATE Game SET is_cracked = 1, crack_date = date('now') WHERE name = ?", [name]) - - text = f'Игре {name!r} убрали защиту' - log.info(text) - log_cracked_games.debug(text) - - # При DEBUG = True, отправки смс не будет - if notified_by_sms and not DEBUG: - simple_send_sms(text, log) - - connect.commit() - - finally: - connect.close() - - -def get_games(filter_by_is_cracked=None, sorted_by_name=True, - sorted_by_crack_date=False, sorted_by_append_date=False) -> [(str, bool, str, str, str)]: - """ - Функция возвращает из базы список вида: - [('Monopoly Plus', 1, '12/09/2017', '07/10/2017 '), ('FIFA 18', 1, '18/09/2017', '03/10/2017 '), ... - - :param filter_by_is_cracked: определяет нужно ли фильтровать по полю is_cracked. Если filter_by_is_cracked = None, - фильтр не используется, иначе фильтрация будет по значению в filter_by_is_cracked - - :param sorted_by_name: использовать ли сортировку по названию игры - :param sorted_by_crack_date: использовать ли сортировку по crack_date - :param sorted_by_append_date: использовать ли сортировку по append_date - :return: - """ - - log.debug('Start get_games: filter_by_is_cracked=%s, sorted_by_name=%s, sorted_by_crack_date=%s', - filter_by_is_cracked, sorted_by_name, sorted_by_crack_date) - - connect = create_connect() - - sort = '' - if sorted_by_name: - sort = ' ORDER BY name' - - if sorted_by_crack_date: - # NOTE: идея такая: сначала сортировка по дате, а после сортировка по имени - # среди тех игр, у которых crack_date одинаковый - sort = ' ORDER BY crack_date DESC, name ASC' - - if sorted_by_append_date: - sort = ' ORDER BY append_date DESC, name ASC' - - try: - sql = """ - SELECT name, is_cracked, - strftime('%d/%m/%Y', append_date), - strftime('%d/%m/%Y', crack_date), - strftime('%d/%m/%Y', release_date) - FROM Game - """ - - if filter_by_is_cracked is not None: - sql += ' WHERE is_cracked = ' + str(int(filter_by_is_cracked)) - - sql += sort - - log.debug('sql: %s', sql) - items = connect.execute(sql).fetchall() - - log.debug('Finish get_games: items[%s]: %s', len(items), items) - - return items - - finally: - connect.close() - - -if __name__ == '__main__': - init_db() - - games = get_games() - print('Games:', len(games), games) - print('\n' + '-' * 100 + '\n') - - games = get_games(filter_by_is_cracked=True) - print('Cracked:', len(games), [name for name, _, _, _, _ in games]) - print('\n' + '-' * 100 + '\n') - - games = get_games(filter_by_is_cracked=False) - print('Not cracked:', len(games), [name for name, _, _, _, _ in games]) - print('\n' + '-' * 100 + '\n') - - games = get_games(filter_by_is_cracked=True, sorted_by_crack_date=True) - print('Cracked and sorted:', len(games), [name for name, _, _, _, _ in games]) - print('\n' + '-' * 100 + '\n') - - games = get_games(filter_by_is_cracked=False, sorted_by_append_date=True) - print('Not cracked and sorted by append:', len(games), [name for name, _, _, _, _ in games]) diff --git a/Check with notification/games_with_denuvo/games_with_denuvo.py b/Check with notification/games_with_denuvo/games_with_denuvo.py deleted file mode 100644 index 2e802ac3d..000000000 --- a/Check with notification/games_with_denuvo/games_with_denuvo.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT - -import requests -from bs4 import BeautifulSoup - - -def get_games_with_denuvo() -> [(str, DT.date, bool)]: - """ - Функция со страницы википедии вытаскивает список игр с Denuvo, с датой выхода и статусом "Взломан". - - """ - - url = 'https://ru.wikipedia.org/wiki/Список_игр,_защищённых_Denuvo' - - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - # Таблица "Список защищённых игр" - table = root.select('.wikitable')[0] - - games = list() - - for tr in table.select('tr'): - td_list = tr.select('td') - - # Например, текущий tr -- заголовок таблицы и не содержит td - if not td_list: - continue - - name = td_list[0].get_text(strip=True) - - # Удаление сносок в названии: "Mad Max[а 1]", "Deus Ex: Mankind Divided[а 3]" - if '[' in name and ']' in name: - name = name[:name.index('[')] - - # Example: '23.09.2014' - try: - date_str = td_list[3].get_text(strip=True) - date = DT.datetime.strptime(date_str, '%d.%m.%Y').date() - except: - date = None - - cracked = td_list[5].text.lower() - is_cracked = 'да' in cracked or 'yes' in cracked - - games.append((name, date, is_cracked)) - - return games - - -def get_games_which_denuvo_is_removed() -> [str, DT.date]: - """ - Функция со страницы википедии вытаскивает список игр в которых убрана защита Denuvo. - - """ - - url = 'https://ru.wikipedia.org/wiki/Список_игр,_защищённых_Denuvo' - - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - # Таблица "Список игр, в которых убрана защита" - table = root.select('.wikitable')[1] - - games = list() - - for tr in table.select('tr'): - td_list = tr.select('td') - - # Например, текущий tr -- заголовок таблицы и не содержит td - if not td_list: - continue - - name = td_list[0].text.strip() - - # Удаление сносок в названии: "Mad Max[а 1]", "Deus Ex: Mankind Divided[а 3]" - if '[' in name and ']' in name: - name = name[:name.index('[')] - - # Example: '23.09.2014' - try: - date_str = td_list[3].get_text(strip=True) - date = DT.datetime.strptime(date_str, '%d.%m.%Y').date() - except: - date = None - - # NOTE: В таблицу добавили пустую из-за чего: 'Добавляю игру с убранной защитой ""' - if name: - games.append((name, date)) - - return games - - -if __name__ == '__main__': - games_with_denuvo = get_games_with_denuvo() - print(f'Всего игр с Denuvo {len(games_with_denuvo)}: {games_with_denuvo}\n') - - cracked_games = list(filter(lambda x: x[-1], games_with_denuvo)) - print(f'Взломанные ({len(cracked_games)}):') - for game in sorted(game for game, date, is_cracked in cracked_games): - print(game) - - print('\n') - - games_without_denuvo = get_games_which_denuvo_is_removed() - print(f'Всего игр в которых убрана защита Denuvo ({len(games_without_denuvo)}):') - for name, _ in games_without_denuvo: - print(name) diff --git a/Check with notification/games_with_denuvo/main.py b/Check with notification/games_with_denuvo/main.py deleted file mode 100644 index cda397fbc..000000000 --- a/Check with notification/games_with_denuvo/main.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для периодического сбора игр с защитой Denuvo и занесения их в базу. - -""" - - -import time - -from common import * -from games_with_denuvo import get_games_with_denuvo, get_games_which_denuvo_is_removed - - -# connect = create_connect() -# connect.execute("DROP TABLE IF EXISTS Game") -# connect.commit() - -init_db() - -# NOTE: С этим флагом нужно быть осторожным при первом запуске, когда база пуста, -# ведь на каждую добавленную взломанную игру отправится уведомление по смс -notified_by_sms = True - -while True: - try: - log.debug('get_games_with_denuvo') - - games = get_games_with_denuvo() - log.debug('games (%s): %s', len(games), games) - - games_without_denuvo = get_games_which_denuvo_is_removed() - log.debug('games_without_denuvo (%s): %s', len(games_without_denuvo), games_without_denuvo) - - append_list_games_which_denuvo_is_removed(games_without_denuvo, notified_by_sms) - append_list_games(games, notified_by_sms) - - db_create_backup() - - wait(days=3) - - except Exception: - log.exception('Ошибка:') - log.debug('Через 5 минут попробую снова...') - - # Wait 5 minutes before next attempt - time.sleep(5 * 60) diff --git a/Check with notification/games_with_denuvo/web.py b/Check with notification/games_with_denuvo/web.py deleted file mode 100644 index 827fc690c..000000000 --- a/Check with notification/games_with_denuvo/web.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT - -from flask import Flask, render_template_string -app = Flask(__name__) - -from common import get_games - - -@app.route('/') -def index(): - cracked_games = get_games(filter_by_is_cracked=True, sorted_by_crack_date=True) - - not_cracked_games = [] - - for name, _, _, _, release_date_str in get_games(filter_by_is_cracked=False, sorted_by_append_date=True): - try: - release_date = DT.datetime.strptime(release_date_str, '%d/%m/%Y').date() - days_passed = (DT.date.today() - release_date).days - except: - days_passed = '-' - - not_cracked_games.append((name, release_date_str, days_passed)) - - return render_template_string(""" - - - - - Denuvo. Список взломанных игр - - - - - - - - - - - - - - -
- - - - - - - - {% for header in cracked_headers %} - - {% endfor %} - - - {% for name, _, _, crack_date, _ in cracked_games %} - - - - - - - {% endfor %} - -
Список взломанных игр
{{ header }}
{{ loop.index }}{{ name }}{{ crack_date }} - - - - - - - - -
-
- - - - - - - {% for header in not_cracked_headers %} - - {% endfor %} - - - {% for name, release_date, days_passed in not_cracked_games %} - - - - - - - {% endfor %} - -
Еще не взломали: -
{{ header }}
{{ loop.index }}{{ name }}{{ release_date }}{{ days_passed }}
- -
- - - - """, cracked_headers=["№", "Название", "Дата взлома", "Поиск"], cracked_games=cracked_games, - not_cracked_headers=["№", "Название", "Дата выхода", "Дней"], not_cracked_games=not_cracked_games - ) - - -if __name__ == "__main__": - app.debug = True - - # Localhost - app.run( - # Включение поддержки множества подключений - threaded=True, - port=5555, - ) - - # # Public IP - # app.run(host='0.0.0.0') diff --git a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py index f2931b079..cb60a4485 100644 --- a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py +++ b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py @@ -1,37 +1,48 @@ # coding=utf-8 -__author__ = 'ipetrash' +__author__ = "ipetrash" + import argparse import sys import os -def main(namespace): +def main(namespace) -> None: # @param namespace argparse.Namespace Содержит переданные в аргументах объекты. - indent = ' ' * 8 + indent = " " * 8 - dirs = filter(lambda x: os.path.isdir(os.path.join(namespace.dir, x)), os.listdir(namespace.dir)) + dirs = filter( + lambda x: os.path.isdir(os.path.join(namespace.dir, x)), + os.listdir(namespace.dir), + ) for dir in dirs: - print(indent + """""" % dir) - - -def create_parser(): - parser = argparse.ArgumentParser(prog="CreateNoteXml", - description=u"Cкрипт генерирует xml-файл программы NotesManager.", - epilog=u"(с) Petrash Ilya 2014. Автор: Илья Петраш.") - parser.add_argument('-dir', - help=u'Путь к директории notes.', - type=str) + print( + indent + + """""" % dir + ) + + +def create_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="CreateNoteXml", + description="Cкрипт генерирует xml-файл программы NotesManager.", + epilog="(с) Petrash Ilya 2014. Автор: Илья Петраш.", + ) + parser.add_argument("-dir", help="Путь к директории notes.", type=str) return parser -if __name__ == '__main__': +if __name__ == "__main__": parser = create_parser() - sys.argv = [sys.argv[0], r'-dir=C:\Users\ipetrash\Desktop\NotesManager.v0.0.3.Windows\notes', ] + # TODO: Remove + # sys.argv = [ + # sys.argv[0], + # r"-dir=C:\Users\ipetrash\Desktop\NotesManager.v0.0.3.Windows\notes", + # ] if len(sys.argv) == 1: parser.print_help() else: namespace = parser.parse_args() - main(namespace) \ No newline at end of file + main(namespace) diff --git a/Crypto Git Repository/api.py b/Crypto Git Repository/api.py index 404062aaf..bbd03226b 100644 --- a/Crypto Git Repository/api.py +++ b/Crypto Git Repository/api.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +# pip install GitPython +import git + +from config import REPO_PATH, URL_GIT repo = None def __get_repo(): - # pip install GitPython - import git - from config import REPO_PATH, URL_GIT - try: return git.Repo(REPO_PATH) @@ -31,9 +33,9 @@ def get_repo(): repo = get_repo() -def print_log(reverse=False): - logs = repo.git.log('--pretty=format:%H%x09%an%x09%ad%x09%s').splitlines() - print('Logs[{}]:'.format(len(logs))) +def print_log(reverse=False) -> None: + logs = repo.git.log("--pretty=format:%H%x09%an%x09%ad%x09%s").splitlines() + print(f"Logs[{len(logs)}]:") if reverse: logs.reverse() @@ -42,28 +44,27 @@ def print_log(reverse=False): print(log) -def append(file_or_list): +def append(file_or_list) -> None: if type(file_or_list) == str: file_or_list = [file_or_list] repo.index.add(file_or_list) -def remove(file_or_list): +def remove(file_or_list) -> None: if type(file_or_list) == str: file_or_list = [file_or_list] repo.index.remove(file_or_list) -def commit(message): +def commit(message) -> None: repo.index.commit(message) -def pull(): +def pull() -> None: repo.remotes.origin.pull() -def push(): +def push() -> None: repo.remotes.origin.push() - diff --git a/Crypto Git Repository/append_to_remote_repo.py b/Crypto Git Repository/append_to_remote_repo.py index afff168a0..6cfcc0d41 100644 --- a/Crypto Git Repository/append_to_remote_repo.py +++ b/Crypto Git Repository/append_to_remote_repo.py @@ -1,27 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import random +import string +import os +import uuid def create_random_file(repo): - import uuid file_name = str(uuid.uuid4()) - - import os full_file_name = os.path.join(repo.working_tree_dir, file_name) - with open(full_file_name, 'w') as f: - import random - import string - text = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(64)) + with open(full_file_name, "w") as f: + text = "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(64) + ) f.write(text) return file_name, file_name -if __name__ == '__main__': +if __name__ == "__main__": import api + repo = api.repo print(repo) print() @@ -30,7 +35,7 @@ def create_random_file(repo): print() file_name = create_random_file(repo)[1] - message = 'Create: ' + file_name + message = "Create: " + file_name print(message) api.append(file_name) diff --git a/Crypto Git Repository/config.py b/Crypto Git Repository/config.py index a48ee6d4f..392ec7dac 100644 --- a/Crypto Git Repository/config.py +++ b/Crypto Git Repository/config.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os LOGIN = None @@ -10,16 +13,13 @@ # http://user:password@proxy_host:proxy_port PROXY = None -NEW_REPO = 'Test-Repo' - -import os +NEW_REPO = "Test-Repo" REPO_PATH = os.path.abspath(NEW_REPO) # How use without input login and password: # git clone https://username:password@github.com/username/repository.git -URL_GIT = 'https://{0}:{1}@github.com/{0}/{2}.git'.format(LOGIN, PASSWORD, NEW_REPO) +URL_GIT = f"https://{LOGIN}:{PASSWORD}@github.com/{LOGIN}/{NEW_REPO}.git" if PROXY: - import os - os.environ['http_proxy'] = PROXY + os.environ["http_proxy"] = PROXY diff --git a/Crypto Git Repository/create_local_repo.py b/Crypto Git Repository/create_local_repo.py index c280d66f4..d936ae89c 100644 --- a/Crypto Git Repository/create_local_repo.py +++ b/Crypto Git Repository/create_local_repo.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - repo = api.repo +import api - print('Repo:', repo) - print() - api.print_log() +repo = api.repo + +print("Repo:", repo) +print() + +api.print_log() diff --git a/Crypto Git Repository/delete_random_file__from__remote_repo.py b/Crypto Git Repository/delete_random_file__from__remote_repo.py index 1f5d731f9..a9edbd6f4 100644 --- a/Crypto Git Repository/delete_random_file__from__remote_repo.py +++ b/Crypto Git Repository/delete_random_file__from__remote_repo.py @@ -1,31 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import random def get_random_file(repo): - file_names = list() + file_names = [] - import os for root, dirs, files in os.walk(repo.working_tree_dir): - if '.git' in root: + if ".git" in root: continue for file in files: file_names.append(os.path.join(root, file)) if file_names: - import random return random.choice(file_names) - else: - return None + return # NOTE: first execute append_to_remote_repo.py -if __name__ == '__main__': +if __name__ == "__main__": import api + repo = api.repo print(repo) print() @@ -35,9 +37,8 @@ def get_random_file(repo): abs_file_name = get_random_file(repo) if abs_file_name: - import os file_name = os.path.basename(abs_file_name) - message = 'Remove: ' + file_name + message = "Remove: " + file_name print(message) api.remove(file_name) diff --git a/Crypto Git Repository/print_repo_log.py b/Crypto Git Repository/print_repo_log.py index db556a910..ca967ec24 100644 --- a/Crypto Git Repository/print_repo_log.py +++ b/Crypto Git Repository/print_repo_log.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - api.print_log(reverse=True) +import api + + +api.print_log(reverse=True) diff --git a/Crypto Git Repository/update_from_remote_repository__pull.py b/Crypto Git Repository/update_from_remote_repository__pull.py index cf97a3538..fb33fb143 100644 --- a/Crypto Git Repository/update_from_remote_repository__pull.py +++ b/Crypto Git Repository/update_from_remote_repository__pull.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - api.pull() +import api + + +api.pull() diff --git a/DNS price tracking/common.py b/DNS price tracking/common.py new file mode 100644 index 000000000..0bbf9589c --- /dev/null +++ b/DNS price tracking/common.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +from typing import List + + +def get_tracked_products() -> List[dict]: + return json.load(open("tracked_products.json", encoding="utf-8")) diff --git a/DNS price tracking/db.py b/DNS price tracking/db.py index c322c9177..91a0635fe 100644 --- a/DNS price tracking/db.py +++ b/DNS price tracking/db.py @@ -1,33 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt +import pathlib +import os +import shutil from peewee import * -# Absolute file name -import pathlib -DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / 'tracked_products.sqlite') +# Absolute file name +DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / "tracked_products.sqlite") -def db_create_backup(backup_dir='backup'): - import datetime as DT - import os - import shutil +def db_create_backup(backup_dir="backup") -> None: os.makedirs(backup_dir, exist_ok=True) - file_name = str(DT.datetime.today().date()) + '.sqlite' + file_name = str(dt.datetime.today().date()) + ".sqlite" file_name = os.path.join(backup_dir, file_name) shutil.copy(DB_FILE_NAME, file_name) # Ensure foreign-key constraints are enforced. -db = SqliteDatabase(DB_FILE_NAME, pragmas={'foreign_keys': 1}) +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -40,7 +39,7 @@ class Product(BaseModel): url = TextField(unique=True) def get_technopoint_url(self) -> str: - return self.url.replace('www.dns-shop.ru', 'technopoint.ru') + return self.url.replace("www.dns-shop.ru", "technopoint.ru") def get_last_price_dns(self, actual_price=True): prices = self.prices @@ -68,43 +67,47 @@ def get_last_price_technopoint(self, actual_price=True): return last_price.value_technopoint - def append_price(self, value_dns, value_technopoint): - Price.create(product=self, value_dns=value_dns, value_technopoint=value_technopoint) + def append_price(self, value_dns, value_technopoint) -> None: + Price.create( + product=self, value_dns=value_dns, value_technopoint=value_technopoint + ) - def __str__(self): + def __str__(self) -> str: # TODO: выводить последнюю цену и актуальную - return f'Product(title={self.title!r}, ' \ - f'last_price_dns={self.get_last_price_dns()}, ' \ - f'last_price_technopoint={self.get_last_price_technopoint()}, ' \ - f'url={self.url!r})' + return ( + f"Product(title={self.title!r}, " + f"last_price_dns={self.get_last_price_dns()}, " + f"last_price_technopoint={self.get_last_price_technopoint()}, " + f"url={self.url!r})" + ) class Price(BaseModel): value_dns = DecimalField(null=True) value_technopoint = DecimalField(null=True) - date = DateTimeField(default=DT.datetime.now) - product = ForeignKeyField(Product, backref='prices') + date = DateTimeField(default=dt.datetime.now) + product = ForeignKeyField(Product, backref="prices") class Meta: - indexes = ( - (("product_id", "date", "value_dns", "value_technopoint"), True), - ) + indexes = ((("product_id", "date", "value_dns", "value_technopoint"), True),) - def __str__(self): - return f'Price(value_dns={self.value_dns}, ' \ - f'value_technopoint={self.value_technopoint}, ' \ - f'date={self.date}, product_id={self.product.id})' + def __str__(self) -> str: + return ( + f"Price(value_dns={self.value_dns}, " + f"value_technopoint={self.value_technopoint}, " + f"date={self.date}, product_id={self.product.id})" + ) db.connect() db.create_tables([Product, Price]) -if __name__ == '__main__': +if __name__ == "__main__": for product in Product.select(): print(product) for p in product.prices.select(): - print(' ', p) + print(" ", p) print() diff --git a/DNS price tracking/main.py b/DNS price tracking/main.py index 1130fb633..c2eae3c79 100644 --- a/DNS price tracking/main.py +++ b/DNS price tracking/main.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -import json import time - -# Import https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py +import traceback import sys -sys.path.append('../wait') -sys.path.append('../html_parsing/www_dns_shop_ru') -from wait import wait +from datetime import datetime + +# pip install simple-wait +from simple_wait import wait + +sys.path.append("../html_parsing/www_dns_shop_ru") from get_price import get_price +from common import get_tracked_products from db import Product, db_create_backup @@ -23,53 +24,57 @@ while True: - print(f'Started at {DT.datetime.now():%d/%m/%Y %H:%M:%S}\n') + print(f"Started at {datetime.now():%d/%m/%Y %H:%M:%S}\n") db_create_backup() checked_products.clear() try: - for product_data in json.load(open('tracked_products.json', encoding='utf-8')): + for product_data in get_tracked_products(): if product_data in checked_products: - print(f"Duplicate: {repr(product_data['title'])}, url: {product_data['url']}\n") + print( + f"Duplicate: {repr(product_data['title'])}, url: {product_data['url']}\n" + ) continue - product, _ = Product.get_or_create(title=product_data['title'], url=product_data['url']) + product, _ = Product.get_or_create( + title=product_data["title"], url=product_data["url"] + ) print(product) - last_price_dns = product.get_last_price_dns(actual_price=False) - print(f'DNS: {last_price_dns} / ' - f'{product.get_last_price_dns(actual_price=True)} (actual)') - - last_price_technopoint = product.get_last_price_technopoint(actual_price=False) - print(f'Technopoint: {last_price_technopoint} / ' - f'{product.get_last_price_technopoint(actual_price=True)} (actual)') + last_price_dns = product.get_last_price_dns() + last_price_technopoint = product.get_last_price_technopoint() + print(f"Last DNS: {last_price_dns}, Technopoint: {last_price_technopoint}") current_url_price_dns = get_price(product.url) - current_url_price_technopoint = get_price(product.get_technopoint_url()) - print(f'Current url price: DNS={current_url_price_dns}, Technopoint={current_url_price_technopoint}') + current_url_price_tp = get_price(product.get_technopoint_url()) + print( + f"Current url price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}" + ) - is_change_dns = current_url_price_dns != last_price_dns - is_change_technopoint = current_url_price_technopoint != last_price_technopoint + is_change_dns = current_url_price_dns and current_url_price_dns != last_price_dns + is_change_technopoint = current_url_price_tp and current_url_price_tp != last_price_technopoint is_first_price = not product.prices.count() # Добавляем новую цену, если цена отличается или у продукта еще нет цен if is_change_dns or is_change_technopoint or is_first_price: - text = f'Append new price: DNS={current_url_price_dns}, Technopoint={current_url_price_technopoint}.' \ - f' Reason: ' + text = ( + f"Append new price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}." + f" Reason: " + ) if is_first_price: - text += 'First price' + text += "First price" else: if is_change_dns and is_change_technopoint: - text += 'DNS and Technopoint' + text += "DNS and Technopoint" elif is_change_dns: - text += 'DNS' + text += "DNS" else: - text += 'Technopoint' + text += "Technopoint" print(text) - product.append_price(current_url_price_dns, current_url_price_technopoint) + product.append_price(current_url_price_dns, current_url_price_tp) print() @@ -81,11 +86,10 @@ except Exception as e: # Выводим ошибку в консоль - import traceback tb = traceback.format_exc() print(tb) - print('Wait 15 minutes') - time.sleep(15 * 60) # 15 minutes + print("Wait 15 minutes") + wait(minutes=15) print() diff --git a/DNS price tracking/migrations/001_auto.py b/DNS price tracking/migrations/001_auto.py index fc9ef4731..a7eddd304 100644 --- a/DNS price tracking/migrations/001_auto.py +++ b/DNS price tracking/migrations/001_auto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations @@ -11,7 +11,7 @@ # TODO: имя в конфиг -DB_NAME = '../tracked_products.sqlite' +DB_NAME = "../tracked_products.sqlite" my_db = SqliteDatabase(DB_NAME) @@ -20,8 +20,12 @@ with my_db.atomic(): migrate( - migrator.rename_column('price', 'value', 'value_dns'), - migrator.add_column('price', 'value_technopoint', DecimalField(null=True)), - migrator.drop_index('price', 'price_product_id_date_value'), - migrator.add_index('price', ('product_id', 'date', 'value_dns', 'value_technopoint'), unique=True), + migrator.rename_column("price", "value", "value_dns"), + migrator.add_column("price", "value_technopoint", DecimalField(null=True)), + migrator.drop_index("price", "price_product_id_date_value"), + migrator.add_index( + "price", + ("product_id", "date", "value_dns", "value_technopoint"), + unique=True, + ), ) diff --git a/DNS price tracking/static/chart_js_2.8.0/Chart.bundle.min.js b/DNS price tracking/static/chart_js_2.8.0/Chart.bundle.min.js deleted file mode 100644 index 0bf9ea9e4..000000000 --- a/DNS price tracking/static/chart_js_2.8.0/Chart.bundle.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Chart.js v2.8.0 - * https://www.chartjs.org - * (c) 2019 Chart.js Contributors - * Released under the MIT License - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Chart=e()}(this,function(){"use strict";var t={rgb2hsl:e,rgb2hsv:i,rgb2hwb:n,rgb2cmyk:a,rgb2keyword:o,rgb2xyz:s,rgb2lab:l,rgb2lch:function(t){return v(l(t))},hsl2rgb:u,hsl2hsv:function(t){var e=t[0],i=t[1]/100,n=t[2]/100;if(0===n)return[0,0,0];return[e,100*(2*(i*=(n*=2)<=1?n:2-n)/(n+i)),100*((n+i)/2)]},hsl2hwb:function(t){return n(u(t))},hsl2cmyk:function(t){return a(u(t))},hsl2keyword:function(t){return o(u(t))},hsv2rgb:d,hsv2hsl:function(t){var e,i,n=t[0],a=t[1]/100,r=t[2]/100;return e=a*r,[n,100*(e=(e/=(i=(2-a)*r)<=1?i:2-i)||0),100*(i/=2)]},hsv2hwb:function(t){return n(d(t))},hsv2cmyk:function(t){return a(d(t))},hsv2keyword:function(t){return o(d(t))},hwb2rgb:h,hwb2hsl:function(t){return e(h(t))},hwb2hsv:function(t){return i(h(t))},hwb2cmyk:function(t){return a(h(t))},hwb2keyword:function(t){return o(h(t))},cmyk2rgb:c,cmyk2hsl:function(t){return e(c(t))},cmyk2hsv:function(t){return i(c(t))},cmyk2hwb:function(t){return n(c(t))},cmyk2keyword:function(t){return o(c(t))},keyword2rgb:_,keyword2hsl:function(t){return e(_(t))},keyword2hsv:function(t){return i(_(t))},keyword2hwb:function(t){return n(_(t))},keyword2cmyk:function(t){return a(_(t))},keyword2lab:function(t){return l(_(t))},keyword2xyz:function(t){return s(_(t))},xyz2rgb:f,xyz2lab:m,xyz2lch:function(t){return v(m(t))},lab2xyz:p,lab2rgb:y,lab2lch:v,lch2lab:x,lch2xyz:function(t){return p(x(t))},lch2rgb:function(t){return y(x(t))}};function e(t){var e,i,n=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(n,a,r),s=Math.max(n,a,r),l=s-o;return s==o?e=0:n==s?e=(a-r)/l:a==s?e=2+(r-n)/l:r==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),i=(o+s)/2,[e,100*(s==o?0:i<=.5?l/(s+o):l/(2-s-o)),100*i]}function i(t){var e,i,n=t[0],a=t[1],r=t[2],o=Math.min(n,a,r),s=Math.max(n,a,r),l=s-o;return i=0==s?0:l/s*1e3/10,s==o?e=0:n==s?e=(a-r)/l:a==s?e=2+(r-n)/l:r==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),[e,i,s/255*1e3/10]}function n(t){var i=t[0],n=t[1],a=t[2];return[e(t)[0],100*(1/255*Math.min(i,Math.min(n,a))),100*(a=1-1/255*Math.max(i,Math.max(n,a)))]}function a(t){var e,i=t[0]/255,n=t[1]/255,a=t[2]/255;return[100*((1-i-(e=Math.min(1-i,1-n,1-a)))/(1-e)||0),100*((1-n-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]}function o(t){return w[JSON.stringify(t)]}function s(t){var e=t[0]/255,i=t[1]/255,n=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)+.1805*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)),100*(.2126*e+.7152*i+.0722*n),100*(.0193*e+.1192*i+.9505*n)]}function l(t){var e=s(t),i=e[0],n=e[1],a=e[2];return n/=100,a/=108.883,i=(i/=95.047)>.008856?Math.pow(i,1/3):7.787*i+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(i-n),200*(n-(a=a>.008856?Math.pow(a,1/3):7.787*a+16/116))]}function u(t){var e,i,n,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return[r=255*l,r,r];e=2*l-(i=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(n=o+1/3*-(u-1))<0&&n++,n>1&&n--,r=6*n<1?e+6*(i-e)*n:2*n<1?i:3*n<2?e+(i-e)*(2/3-n)*6:e,a[u]=255*r;return a}function d(t){var e=t[0]/60,i=t[1]/100,n=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*n*(1-i),s=255*n*(1-i*r),l=255*n*(1-i*(1-r));n*=255;switch(a){case 0:return[n,l,o];case 1:return[s,n,o];case 2:return[o,n,l];case 3:return[o,s,n];case 4:return[l,o,n];case 5:return[n,o,s]}}function h(t){var e,i,n,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,u=s+l;switch(u>1&&(s/=u,l/=u),n=6*o-(e=Math.floor(6*o)),0!=(1&e)&&(n=1-n),a=s+n*((i=1-l)-s),e){default:case 6:case 0:r=i,g=a,b=s;break;case 1:r=a,g=i,b=s;break;case 2:r=s,g=i,b=a;break;case 3:r=s,g=a,b=i;break;case 4:r=a,g=s,b=i;break;case 5:r=i,g=s,b=a}return[255*r,255*g,255*b]}function c(t){var e=t[0]/100,i=t[1]/100,n=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a))]}function f(t){var e,i,n,a=t[0]/100,r=t[1]/100,o=t[2]/100;return i=-.9689*a+1.8758*r+.0415*o,n=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,[255*(e=Math.min(Math.max(0,e),1)),255*(i=Math.min(Math.max(0,i),1)),255*(n=Math.min(Math.max(0,n),1))]}function m(t){var e=t[0],i=t[1],n=t[2];return i/=100,n/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(e-i),200*(i-(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116))]}function p(t){var e,i,n,a,r=t[0],o=t[1],s=t[2];return r<=8?a=(i=100*r/903.3)/100*7.787+16/116:(i=100*Math.pow((r+16)/116,3),a=Math.pow(i/100,1/3)),[e=e/95.047<=.008856?e=95.047*(o/500+a-16/116)/7.787:95.047*Math.pow(o/500+a,3),i,n=n/108.883<=.008859?n=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3)]}function v(t){var e,i=t[0],n=t[1],a=t[2];return(e=360*Math.atan2(a,n)/2/Math.PI)<0&&(e+=360),[i,Math.sqrt(n*n+a*a),e]}function y(t){return f(p(t))}function x(t){var e,i=t[0],n=t[1];return e=t[2]/360*2*Math.PI,[i,n*Math.cos(e),n*Math.sin(e)]}function _(t){return k[t]}var k={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},w={};for(var M in k)w[JSON.stringify(k[M])]=M;var S=function(){return new O};for(var D in t){S[D+"Raw"]=function(e){return function(i){return"number"==typeof i&&(i=Array.prototype.slice.call(arguments)),t[e](i)}}(D);var C=/(\w+)2(\w+)/.exec(D),P=C[1],T=C[2];(S[P]=S[P]||{})[T]=S[D]=function(e){return function(i){"number"==typeof i&&(i=Array.prototype.slice.call(arguments));var n=t[e](i);if("string"==typeof n||void 0===n)return n;for(var a=0;a=0&&e<1?H(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return Y(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:Y,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return N(t,e);var i=Math.round(t[0]/255*100),n=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+i+"%, "+n+"%, "+a+"%)"},percentaString:N,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return z(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:z,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return E[t.slice(0,3)]}};function R(t){if(t){var e=[0,0,0],i=1,n=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(n){a=(n=n[1])[3];for(var r=0;ri?(e+.05)/(i+.05):(i+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,i=(e[0]+t)%360;return e[0]=i<0?360+i:i,this.setValues("hsl",e),this},mix:function(t,e){var i=t,n=void 0===e?.5:e,a=2*n-1,r=this.alpha()-i.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*i.red(),o*this.green()+s*i.green(),o*this.blue()+s*i.blue()).alpha(this.alpha()*n+i.alpha()*(1-n))},toJSON:function(){return this.rgb()},clone:function(){var t,e,i=new j,n=this.values,a=i.values;for(var r in n)n.hasOwnProperty(r)&&(t=n[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return i}},j.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},j.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},j.prototype.getValues=function(t){for(var e=this.values,i={},n=0;n=0;a--)e.call(i,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i))},easeOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/i)+1)},easeInOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:2==(t/=.5)?1:(i||(i=.45),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),t<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-$.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*$.easeInBounce(2*t):.5*$.easeOutBounce(2*t-1)+.5}},X={effects:$};Z.easingEffects=$;var K=Math.PI,J=K/180,Q=2*K,tt=K/2,et=K/4,it=2*K/3,nt={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,i,n,a,r){if(r){var o=Math.min(r,a/2,n/2),s=e+o,l=i+o,u=e+n-o,d=i+a-o;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,i,n,a=this.animations,r=0;r=i?(ut.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},yt=ut.options.resolve,bt=["push","pop","shift","splice","unshift"];function xt(t,e){var i=t._chartjs;if(i){var n=i.listeners,a=n.indexOf(e);-1!==a&&n.splice(a,1),n.length>0||(bt.forEach(function(e){delete t[e]}),delete t._chartjs)}}var _t=function(t,e){this.initialize(t,e)};ut.extend(_t.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){this.chart=t,this.index=e,this.linkScales(),this.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),i=t.getDataset();null!==e.xAxisID&&e.xAxisID in t.chart.scales||(e.xAxisID=i.xAxisID||t.chart.options.scales.xAxes[0].id),null!==e.yAxisID&&e.yAxisID in t.chart.scales||(e.yAxisID=i.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this.update(!0)},destroy:function(){this._data&&xt(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,i=this.getMeta(),n=this.getDataset().data||[],a=i.data;for(t=0,e=n.length;ti&&this.insertElements(i,n-i)},insertElements:function(t,e){for(var i=0;is;)a-=2*Math.PI;for(;a=o&&a<=s,u=r>=i.innerRadius&&r<=i.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,i=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,i=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},draw:function(){var t,e=this._chart.ctx,i=this._view,n=i.startAngle,a=i.endAngle,r="inner"===i.borderAlign?.33:0;e.save(),e.beginPath(),e.arc(i.x,i.y,Math.max(i.outerRadius-r,0),n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.fillStyle=i.backgroundColor,e.fill(),i.borderWidth&&("inner"===i.borderAlign?(e.beginPath(),t=r/i.outerRadius,e.arc(i.x,i.y,i.outerRadius,n-t,a+t),i.innerRadius>r?(t=r/i.innerRadius,e.arc(i.x,i.y,i.innerRadius-r,a+t,n-t,!0)):e.arc(i.x,i.y,r,a+Math.PI/2,n-Math.PI/2),e.closePath(),e.clip(),e.beginPath(),e.arc(i.x,i.y,i.outerRadius,n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.lineWidth=2*i.borderWidth,e.lineJoin="round"):(e.lineWidth=i.borderWidth,e.lineJoin="bevel"),e.strokeStyle=i.borderColor,e.stroke()),e.restore()}}),Mt=ut.valueOrDefault,St=ot.global.defaultColor;ot._set("global",{elements:{line:{tension:.4,backgroundColor:St,borderWidth:3,borderColor:St,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var Dt=gt.extend({draw:function(){var t,e,i,n,a=this._view,r=this._chart.ctx,o=a.spanGaps,s=this._children.slice(),l=ot.global,u=l.elements.line,d=-1;for(this._loop&&s.length&&s.push(s[0]),r.save(),r.lineCap=a.borderCapStyle||u.borderCapStyle,r.setLineDash&&r.setLineDash(a.borderDash||u.borderDash),r.lineDashOffset=Mt(a.borderDashOffset,u.borderDashOffset),r.lineJoin=a.borderJoinStyle||u.borderJoinStyle,r.lineWidth=Mt(a.borderWidth,u.borderWidth),r.strokeStyle=a.borderColor||l.defaultColor,r.beginPath(),d=-1,t=0;tt.x&&(e=Rt(e,"left","right")):t.basei?i:n,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>i?i:r,l:l.left||o<0?0:o>e?e:o}}function Wt(t,e,i){var n=null===e,a=null===i,r=!(!t||n&&a)&&Ft(t);return r&&(n||e>=r.left&&e<=r.right)&&(a||i>=r.top&&i<=r.bottom)}ot._set("global",{elements:{rectangle:{backgroundColor:It,borderColor:It,borderSkipped:"bottom",borderWidth:0}}});var Yt=gt.extend({draw:function(){var t=this._chart.ctx,e=this._view,i=function(t){var e=Ft(t),i=e.right-e.left,n=e.bottom-e.top,a=Lt(t,i/2,n/2);return{outer:{x:e.left,y:e.top,w:i,h:n},inner:{x:e.left+a.l,y:e.top+a.t,w:i-a.l-a.r,h:n-a.t-a.b}}}(e),n=i.outer,a=i.inner;t.fillStyle=e.backgroundColor,t.fillRect(n.x,n.y,n.w,n.h),n.w===a.w&&n.h===a.h||(t.save(),t.beginPath(),t.rect(n.x,n.y,n.w,n.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return Wt(this._view,t,e)},inLabelRange:function(t,e){var i=this._view;return At(i)?Wt(i,t,null):Wt(i,null,e)},inXRange:function(t){return Wt(this._view,t,null)},inYRange:function(t){return Wt(this._view,null,t)},getCenterPoint:function(){var t,e,i=this._view;return At(i)?(t=i.x,e=(i.y+i.base)/2):(t=(i.x+i.base)/2,e=i.y),{x:t,y:e}},getArea:function(){var t=this._view;return At(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),Nt={},zt=wt,Vt=Dt,Ht=Ot,Et=Yt;Nt.Arc=zt,Nt.Line=Vt,Nt.Point=Ht,Nt.Rectangle=Et;var Bt=ut.options.resolve;ot._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}});var jt=kt.extend({dataElementType:Nt.Rectangle,initialize:function(){var t;kt.prototype.initialize.apply(this,arguments),(t=this.getMeta()).stack=this.getDataset().stack,t.bar=!0},update:function(t){var e,i,n=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,i=n.length;e0?Math.min(o,n-i):o,i=n;return o}(i,l):-1,pixels:l,start:o,end:s,stackCount:n,scale:i}},calculateBarValuePixels:function(t,e){var i,n,a,r,o,s,l=this.chart,u=this.getMeta(),d=this._getValueScale(),h=d.isHorizontal(),c=l.data.datasets,f=+d.getRightValue(c[t].data[e]),g=d.options.minBarLength,m=d.options.stacked,p=u.stack,v=0;if(m||void 0===m&&void 0!==p)for(i=0;i=0&&a>0)&&(v+=a));return r=d.getPixelForValue(v),s=(o=d.getPixelForValue(v+f))-r,void 0!==g&&Math.abs(s)=0&&!h||f<0&&h?r-g:r+g),{size:s,base:r,head:o,center:o+s/2}},calculateBarIndexPixels:function(t,e,i){var n=i.scale.options,a="flex"===n.barThickness?function(t,e,i){var n,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],o=a.data[n],s=o&&o.custom||{},l=t.options.elements.arc;return{text:i,fillStyle:Zt([s.backgroundColor,r.backgroundColor,l.backgroundColor],void 0,n),strokeStyle:Zt([s.borderColor,r.borderColor,l.borderColor],void 0,n),lineWidth:Zt([s.borderWidth,r.borderWidth,l.borderWidth],void 0,n),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,r=e.index,o=this.chart;for(i=0,n=(o.data.datasets||[]).length;i=Math.PI?-1:p<-Math.PI?1:0))+g,y={x:Math.cos(p),y:Math.sin(p)},b={x:Math.cos(v),y:Math.sin(v)},x=p<=0&&v>=0||p<=2*Math.PI&&2*Math.PI<=v,_=p<=.5*Math.PI&&.5*Math.PI<=v||p<=2.5*Math.PI&&2.5*Math.PI<=v,k=p<=-Math.PI&&-Math.PI<=v||p<=Math.PI&&Math.PI<=v,w=p<=.5*-Math.PI&&.5*-Math.PI<=v||p<=1.5*Math.PI&&1.5*Math.PI<=v,M=f/100,S={x:k?-1:Math.min(y.x*(y.x<0?1:M),b.x*(b.x<0?1:M)),y:w?-1:Math.min(y.y*(y.y<0?1:M),b.y*(b.y<0?1:M))},D={x:x?1:Math.max(y.x*(y.x>0?1:M),b.x*(b.x>0?1:M)),y:_?1:Math.max(y.y*(y.y>0?1:M),b.y*(b.y>0?1:M))},C={width:.5*(D.x-S.x),height:.5*(D.y-S.y)};u=Math.min(s/C.width,l/C.height),d={x:-.5*(D.x+S.x),y:-.5*(D.y+S.y)}}for(e=0,i=c.length;e0&&!isNaN(t)?2*Math.PI*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,i,n,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,i=d.data.datasets.length;e(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,i=t._options,n=ut.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=$t(i.hoverBackgroundColor,n(i.backgroundColor)),e.borderColor=$t(i.hoverBorderColor,n(i.borderColor)),e.borderWidth=$t(i.hoverBorderWidth,i.borderWidth)},_resolveElementOptions:function(t,e){var i,n,a,r=this.chart,o=this.getDataset(),s=t.custom||{},l=r.options.elements.arc,u={},d={chart:r,dataIndex:e,dataset:o,datasetIndex:this.index},h=["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"];for(i=0,n=h.length;i0&&te(l[t-1]._model,s)&&(i.controlPointPreviousX=u(i.controlPointPreviousX,s.left,s.right),i.controlPointPreviousY=u(i.controlPointPreviousY,s.top,s.bottom)),t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],o=a.data[n].custom||{},s=t.options.elements.arc;return{text:i,fillStyle:ne([o.backgroundColor,r.backgroundColor,s.backgroundColor],void 0,n),strokeStyle:ne([o.borderColor,r.borderColor,s.borderColor],void 0,n),lineWidth:ne([o.borderWidth,r.borderWidth,s.borderWidth],void 0,n),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,r=e.index,o=this.chart;for(i=0,n=(o.data.datasets||[]).length;i0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return me(t,e,{intersect:!1})},point:function(t,e){return ce(t,de(e,t))},nearest:function(t,e,i){var n=de(e,t);i.axis=i.axis||"xy";var a=ge(i.axis);return fe(t,n,i.intersect,a)},x:function(t,e,i){var n=de(e,t),a=[],r=!1;return he(t,function(t){t.inXRange(n.x)&&a.push(t),t.inRange(n.x,n.y)&&(r=!0)}),i.intersect&&!r&&(a=[]),a},y:function(t,e,i){var n=de(e,t),a=[],r=!1;return he(t,function(t){t.inYRange(n.y)&&a.push(t),t.inRange(n.x,n.y)&&(r=!0)}),i.intersect&&!r&&(a=[]),a}}};function ve(t,e){return ut.where(t,function(t){return t.position===e})}function ye(t,e){t.forEach(function(t,e){return t._tmpIndex_=e,t}),t.sort(function(t,i){var n=e?i:t,a=e?t:i;return n.weight===a.weight?n._tmpIndex_-a._tmpIndex_:n.weight-a.weight}),t.forEach(function(t){delete t._tmpIndex_})}function be(t,e){ut.each(t,function(t){e[t.position]+=t.isHorizontal()?t.height:t.width})}ot._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var xe={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,t.boxes.push(e)},removeBox:function(t,e){var i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure:function(t,e,i){for(var n,a=["fullWidth","position","weight"],r=a.length,o=0;odiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ke.default||ke,Me="$chartjs",Se="chartjs-size-monitor",De="chartjs-render-monitor",Ce="chartjs-render-animation",Pe=["animationstart","webkitAnimationStart"],Te={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function Oe(t,e){var i=ut.getStyle(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?Number(n[1]):void 0}var Ie=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Ae(t,e,i){t.addEventListener(e,i,Ie)}function Fe(t,e,i){t.removeEventListener(e,i,Ie)}function Re(t,e,i,n,a){return{type:t,chart:e,native:a||null,x:void 0!==i?i:null,y:void 0!==n?n:null}}function Le(t){var e=document.createElement("div");return e.className=t||"",e}function We(t,e,i){var n,a,r,o,s=t[Me]||(t[Me]={}),l=s.resizer=function(t){var e=Le(Se),i=Le(Se+"-expand"),n=Le(Se+"-shrink");i.appendChild(Le()),n.appendChild(Le()),e.appendChild(i),e.appendChild(n),e._reset=function(){i.scrollLeft=1e6,i.scrollTop=1e6,n.scrollLeft=1e6,n.scrollTop=1e6};var a=function(){e._reset(),t()};return Ae(i,"scroll",a.bind(i,"expand")),Ae(n,"scroll",a.bind(n,"shrink")),e}((n=function(){if(s.resizer){var n=i.options.maintainAspectRatio&&t.parentNode,a=n?n.clientWidth:0;e(Re("resize",i)),n&&n.clientWidth0){var r=t[0];r.label?i=r.label:r.xLabel?i=r.xLabel:a>0&&r.index-1?t.split("\n"):t}function qe(t){var e=ot.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Be(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Be(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Be(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Be(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Be(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Be(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Be(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Be(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Be(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function Ze(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function $e(t){return Ue([],Ge(t))}var Xe=gt.extend({initialize:function(){this._model=qe(this._options),this._lastActive=[]},getTitle:function(){var t=this._options.callbacks,e=t.beforeTitle.apply(this,arguments),i=t.title.apply(this,arguments),n=t.afterTitle.apply(this,arguments),a=[];return a=Ue(a,Ge(e)),a=Ue(a,Ge(i)),a=Ue(a,Ge(n))},getBeforeBody:function(){return $e(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var i=this,n=i._options.callbacks,a=[];return ut.each(t,function(t){var r={before:[],lines:[],after:[]};Ue(r.before,Ge(n.beforeLabel.call(i,t,e))),Ue(r.lines,n.label.call(i,t,e)),Ue(r.after,Ge(n.afterLabel.call(i,t,e))),a.push(r)}),a},getAfterBody:function(){return $e(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this._options.callbacks,e=t.beforeFooter.apply(this,arguments),i=t.footer.apply(this,arguments),n=t.afterFooter.apply(this,arguments),a=[];return a=Ue(a,Ge(e)),a=Ue(a,Ge(i)),a=Ue(a,Ge(n))},update:function(t){var e,i,n,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=qe(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},y={x:f.x,y:f.y},b={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],k=[];x=je[c.position].call(h,m,h._eventPosition);var w=[];for(e=0,i=m.length;en.width&&(a=n.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,b,v=function(t,e){var i,n,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(i=function(t){return t<=c},n=function(t){return t>c}):(i=function(t){return t<=e.width/2},n=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},i(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):n(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,b),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=y.x,g.y=y.y,g.width=b.width,g.height=b.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var i=this._chart.ctx,n=this._view,a=this.getCaretPosition(t,e,n);i.lineTo(a.x1,a.y1),i.lineTo(a.x2,a.y2),i.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,i){var n,a,r,o,s,l,u=i.caretSize,d=i.cornerRadius,h=i.xAlign,c=i.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(n=f)-u,r=n,o=s+u,l=s-u):(a=(n=f+m)+u,r=n,o=s-u,l=s+u);else if("left"===h?(n=(a=f+d+u)-u,r=a+u):"right"===h?(n=(a=f+m-d-u)-u,r=a+u):(n=(a=i.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=n,n=v}return{x1:n,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,i){var n=e.title;if(n.length){t.x=Ze(e,e._titleAlign),i.textAlign=e._titleAlign,i.textBaseline="top";var a,r,o=e.titleFontSize,s=e.titleSpacing;for(i.fillStyle=e.titleFontColor,i.font=ut.fontString(o,e._titleFontStyle,e._titleFontFamily),a=0,r=n.length;a0&&i.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var i={width:e.width,height:e.height},n={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(n,e,t,i),n.y+=e.yPadding,this.drawTitle(n,e,t),this.drawBody(n,e,t),this.drawFooter(n,e,t),t.restore())}},handleEvent:function(t){var e,i=this,n=i._options;return i._lastActive=i._lastActive||[],"mouseout"===t.type?i._active=[]:i._active=i._chart.getElementsAtEventForMode(t,n.mode,n),(e=!ut.arrayEquals(i._active,i._lastActive))&&(i._lastActive=i._active,(n.enabled||n.custom)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0),i.pivot())),e}}),Ke=je,Je=Xe;Je.positioners=Ke;var Qe=ut.valueOrDefault;function ti(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){if("xAxes"===t||"yAxes"===t){var a,r,o,s=i[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?ut.merge(e[t][a],[Ee.getScaleDefaults(r),o]):ut.merge(e[t][a],o)}else ut._merger(t,e,i,n)}})}function ei(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){var a=e[t]||{},r=i[t];"scales"===t?e[t]=ti(a,r):"scale"===t?e[t]=ut.merge(a,[Ee.getScaleDefaults(r.type),r]):ut._merger(t,e,i,n)}})}function ii(t){return"top"===t||"bottom"===t}ot._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var ni=function(t,e){return this.construct(t,e),this};ut.extend(ni.prototype,{construct:function(t,e){var i=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=ei(ot.global,ot[t.type],t.options||{}),t}(e);var n=Ve.acquireContext(t,e),a=n&&n.canvas,r=a&&a.height,o=a&&a.width;i.id=ut.uid(),i.ctx=n,i.canvas=a,i.config=e,i.width=o,i.height=r,i.aspectRatio=r?o/r:null,i.options=e.options,i._bufferedRender=!1,i.chart=i,i.controller=i,ni.instances[i.id]=i,Object.defineProperty(i,"data",{get:function(){return i.config.data},set:function(t){i.config.data=t}}),n&&a?(i.initialize(),i.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return He.notify(t,"beforeInit"),ut.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.initToolTip(),He.notify(t,"afterInit"),t},clear:function(){return ut.canvas.clear(this),this},stop:function(){return vt.cancelAnimation(this),this},resize:function(t){var e=this,i=e.options,n=e.canvas,a=i.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(ut.getMaximumWidth(n))),o=Math.max(0,Math.floor(a?r/a:ut.getMaximumHeight(n)));if((e.width!==r||e.height!==o)&&(n.width=e.width=r,n.height=e.height=o,n.style.width=r+"px",n.style.height=o+"px",ut.retinaScale(e,i.devicePixelRatio),!t)){var s={width:r,height:o};He.notify(e,"resize",[s]),i.onResize&&i.onResize(e,s),e.stop(),e.update({duration:i.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},i=t.scale;ut.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),ut.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,i=t.scales||{},n=[],a=Object.keys(i).reduce(function(t,e){return t[e]=!1,t},{});e.scales&&(n=n.concat((e.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category",dposition:"bottom"}}),(e.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear",dposition:"left"}}))),e.scale&&n.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),ut.each(n,function(e){var n=e.options,r=n.id,o=Qe(n.type,e.dtype);ii(n.position)!==ii(e.dposition)&&(n.position=e.dposition),a[r]=!0;var s=null;if(r in i&&i[r].type===o)(s=i[r]).options=n,s.ctx=t.ctx,s.chart=t;else{var l=Ee.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:n,ctx:t.ctx,chart:t}),i[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)}),ut.each(a,function(t,e){t||delete i[e]}),t.scales=i,Ee.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t=this,e=[];return ut.each(t.data.datasets,function(i,n){var a=t.getDatasetMeta(n),r=i.type||t.config.type;if(a.type&&a.type!==r&&(t.destroyDatasetMeta(n),a=t.getDatasetMeta(n)),a.type=r,a.controller)a.controller.updateIndex(n),a.controller.linkScales();else{var o=ue[a.type];if(void 0===o)throw new Error('"'+a.type+'" is not a chart type.');a.controller=new o(t,n),e.push(a.controller)}},t),e},resetElements:function(){var t=this;ut.each(t.data.datasets,function(e,i){t.getDatasetMeta(i).controller.reset()},t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(t){var e,i,n=this;if(t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]}),i=(e=n).options,ut.each(e.scales,function(t){xe.removeBox(e,t)}),i=ei(ot.global,ot[e.config.type],i),e.options=e.config.options=i,e.ensureScalesHaveIDs(),e.buildOrUpdateScales(),e.tooltip._options=i.tooltips,e.tooltip.initialize(),He._invalidate(n),!1!==He.notify(n,"beforeUpdate")){n.tooltip._data=n.data;var a=n.buildOrUpdateControllers();ut.each(n.data.datasets,function(t,e){n.getDatasetMeta(e).controller.buildOrUpdateElements()},n),n.updateLayout(),n.options.animation&&n.options.animation.duration&&ut.each(a,function(t){t.reset()}),n.updateDatasets(),n.tooltip.initialize(),n.lastActive=[],He.notify(n,"afterUpdate"),n._bufferedRender?n._bufferedRequest={duration:t.duration,easing:t.easing,lazy:t.lazy}:n.render(t)}},updateLayout:function(){!1!==He.notify(this,"beforeLayout")&&(xe.update(this,this.width,this.height),He.notify(this,"afterScaleUpdate"),He.notify(this,"afterLayout"))},updateDatasets:function(){if(!1!==He.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t=0;--i)e.isDatasetVisible(i)&&e.drawDataset(i,t);He.notify(e,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var i=this.getDatasetMeta(t),n={meta:i,index:t,easingValue:e};!1!==He.notify(this,"beforeDatasetDraw",[n])&&(i.controller.draw(e),He.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,i={tooltip:e,easingValue:t};!1!==He.notify(this,"beforeTooltipDraw",[i])&&(e.draw(),He.notify(this,"afterTooltipDraw",[i]))},getElementAtEvent:function(t){return pe.modes.single(this,t)},getElementsAtEvent:function(t){return pe.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return pe.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,i){var n=pe.modes[e];return"function"==typeof n?n(this,t,i):[]},getDatasetAtEvent:function(t){return pe.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var i=e._meta[this.id];return i||(i=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null}),i},getVisibleDatasetCount:function(){for(var t=0,e=0,i=this.data.datasets.length;e3?i[2]-i[1]:i[1]-i[0];Math.abs(n)>1&&t!==Math.floor(t)&&(n=t-Math.floor(t));var a=ut.log10(Math.abs(n)),r="";if(0!==t)if(Math.max(Math.abs(i[0]),Math.abs(i[i.length-1]))<1e-4){var o=ut.log10(Math.abs(t));r=t.toExponential(Math.floor(o)-Math.floor(a))}else{var s=-1*Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toFixed(s)}else r="0";return r},logarithmic:function(t,e,i){var n=t/Math.pow(10,Math.floor(ut.log10(t)));return 0===t?"0":1===n||2===n||5===n||0===e||e===i.length-1?t.toExponential():""}}},ui=ut.valueOrDefault,di=ut.valueAtIndexOrDefault;function hi(t){var e,i,n=[];for(e=0,i=t.length;eu&&rt.maxHeight){r--;break}r++,l=o*s}t.labelRotation=r},afterCalculateTickRotation:function(){ut.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){ut.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},i=hi(t._ticks),n=t.options,a=n.ticks,r=n.scaleLabel,o=n.gridLines,s=t._isVisible(),l=n.position,u=t.isHorizontal(),d=ut.options._parseFont,h=d(a),c=n.gridLines.tickMarkLength;if(e.width=u?t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:s&&o.drawTicks?c:0,e.height=u?s&&o.drawTicks?c:0:t.maxHeight,r.display&&s){var f=d(r),g=ut.options.toPadding(r.padding),m=f.lineHeight+g.height;u?e.height+=m:e.width+=m}if(a.display&&s){var p=ut.longestText(t.ctx,h.string,i,t.longestTextCache),v=ut.numberOfLabelLines(i),y=.5*h.size,b=t.options.ticks.padding;if(t._maxLabelLines=v,t.longestLabelWidth=p,u){var x=ut.toRadians(t.labelRotation),_=Math.cos(x),k=Math.sin(x)*p+h.lineHeight*v+y;e.height=Math.min(t.maxHeight,e.height+k+b),t.ctx.font=h.string;var w,M,S=ci(t.ctx,i[0],h.string),D=ci(t.ctx,i[i.length-1],h.string),C=t.getPixelForTick(0)-t.left,P=t.right-t.getPixelForTick(i.length-1);0!==t.labelRotation?(w="bottom"===l?_*S:_*y,M="bottom"===l?_*y:_*D):(w=S/2,M=D/2),t.paddingLeft=Math.max(w-C,0)+3,t.paddingRight=Math.max(M-P,0)+3}else a.mirror?p=0:p+=b+y,e.width=Math.min(t.maxWidth,e.width+p),t.paddingTop=h.size/2,t.paddingBottom=h.size/2}t.handleMargins(),t.width=e.width,t.height=e.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){ut.callback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ut.isNullOrUndef(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},getLabelForIndex:ut.noop,getPixelForValue:ut.noop,getValueForPixel:ut.noop,getPixelForTick:function(t){var e=this,i=e.options.offset;if(e.isHorizontal()){var n=(e.width-(e.paddingLeft+e.paddingRight))/Math.max(e._ticks.length-(i?0:1),1),a=n*t+e.paddingLeft;i&&(a+=n/2);var r=e.left+a;return r+=e.isFullWidth()?e.margins.left:0}var o=e.height-(e.paddingTop+e.paddingBottom);return e.top+t*(o/(e._ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var i=(e.width-(e.paddingLeft+e.paddingRight))*t+e.paddingLeft,n=e.left+i;return n+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,i,n=this,a=n.isHorizontal(),r=n.options.ticks.minor,o=t.length,s=!1,l=r.maxTicksLimit,u=n._tickSize()*(o-1),d=a?n.width-(n.paddingLeft+n.paddingRight):n.height-(n.paddingTop+n.PaddingBottom),h=[];for(u>d&&(s=1+Math.floor(u/d)),o>l&&(s=Math.max(s,1+Math.floor(o/l))),e=0;e1&&e%s>0&&delete i.label,h.push(i);return h},_tickSize:function(){var t=this,e=t.isHorizontal(),i=t.options.ticks.minor,n=ut.toRadians(t.labelRotation),a=Math.abs(Math.cos(n)),r=Math.abs(Math.sin(n)),o=i.autoSkipPadding||0,s=t.longestLabelWidth+o||0,l=ut.options._parseFont(i),u=t._maxLabelLines*l.lineHeight+o||0;return e?u*a>s*r?s/a:u/r:u*r0&&n>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,i=e.stepSize,n=e.maxTicksLimit;return i?t=Math.ceil(this.max/i)-Math.floor(this.min/i)+1:(t=this._computeTickLimit(),n=n||11),n&&(t=Math.min(n,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:pi,buildTicks:function(){var t=this,e=t.options.ticks,i=t.getTickLimit(),n={maxTicks:i=Math.max(2,i),min:e.min,max:e.max,precision:e.precision,stepSize:ut.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var i,n,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=ut.niceNum((g-f)/u/l)*l;if(m<1e-14&&vi(d)&&vi(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=ut.niceNum(r*m/u/l)*l),s||vi(c)?i=Math.pow(10,ut._decimalPlaces(m)):(i=Math.pow(10,c),m=Math.ceil(m*i)/i),n=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!vi(d)&&ut.almostWhole(d/m,m/1e3)&&(n=d),!vi(h)&&ut.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-n)/m,r=ut.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),n=Math.round(n*i)/i,a=Math.round(a*i)/i,o.push(vi(d)?n:d);for(var p=1;pt.max&&(t.max=n))})});t.min=isFinite(t.min)&&!isNaN(t.min)?t.min:0,t.max=isFinite(t.max)&&!isNaN(t.max)?t.max:1,this.handleTickRangeOptions()},_computeTickLimit:function(){var t;return this.isHorizontal()?Math.ceil(this.width/40):(t=ut.options._parseFont(this.options.ticks),Math.ceil(this.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e=this,i=e.start,n=+e.getRightValue(t),a=e.end-i;return e.isHorizontal()?e.left+e.width/a*(n-i):e.bottom-e.height/a*(n-i)},getValueForPixel:function(t){var e=this,i=e.isHorizontal(),n=i?e.width:e.height,a=(i?t-e.left:e.bottom-t)/n;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}}),_i=bi;xi._defaults=_i;var ki=ut.valueOrDefault;var wi={position:"left",ticks:{callback:li.formatters.logarithmic}};function Mi(t,e){return ut.isFinite(t)&&t>=0?t:e}var Si=fi.extend({determineDataLimits:function(){var t=this,e=t.options,i=t.chart,n=i.data.datasets,a=t.isHorizontal();function r(e){return a?e.xAxisID===t.id:e.yAxisID===t.id}t.min=null,t.max=null,t.minNotZero=null;var o=e.stacked;if(void 0===o&&ut.each(n,function(t,e){if(!o){var n=i.getDatasetMeta(e);i.isDatasetVisible(e)&&r(n)&&void 0!==n.stack&&(o=!0)}}),e.stacked||o){var s={};ut.each(n,function(n,a){var o=i.getDatasetMeta(a),l=[o.type,void 0===e.stacked&&void 0===o.stack?a:"",o.stack].join(".");i.isDatasetVisible(a)&&r(o)&&(void 0===s[l]&&(s[l]=[]),ut.each(n.data,function(e,i){var n=s[l],a=+t.getRightValue(e);isNaN(a)||o.data[i].hidden||a<0||(n[i]=n[i]||0,n[i]+=a)}))}),ut.each(s,function(e){if(e.length>0){var i=ut.min(e),n=ut.max(e);t.min=null===t.min?i:Math.min(t.min,i),t.max=null===t.max?n:Math.max(t.max,n)}})}else ut.each(n,function(e,n){var a=i.getDatasetMeta(n);i.isDatasetVisible(n)&&r(a)&&ut.each(e.data,function(e,i){var n=+t.getRightValue(e);isNaN(n)||a.data[i].hidden||n<0||(null===t.min?t.min=n:nt.max&&(t.max=n),0!==n&&(null===t.minNotZero||n0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(ut.log10(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,i=!t.isHorizontal(),n={min:Mi(e.min),max:Mi(e.max)},a=t.ticks=function(t,e){var i,n,a=[],r=ki(t.min,Math.pow(10,Math.floor(ut.log10(e.min)))),o=Math.floor(ut.log10(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(i=Math.floor(ut.log10(e.minNotZero)),n=Math.floor(e.minNotZero/Math.pow(10,i)),a.push(r),r=n*Math.pow(10,i)):(i=Math.floor(ut.log10(r)),n=Math.floor(r/Math.pow(10,i)));var l=i<0?Math.pow(10,Math.abs(i)):1;do{a.push(r),10==++n&&(n=1,l=++i>=0?1:l),r=Math.round(n*Math.pow(10,i)*l)/l}while(ia?{start:e-i,end:e}:{start:e,end:e+i}}function Ri(t){return 0===t||180===t?"center":t<180?"left":"right"}function Li(t,e,i,n){var a,r,o=i.y+n/2;if(ut.isArray(e))for(a=0,r=e.length;a270||t<90)&&(i.y-=e.h)}function Yi(t){return ut.isNumber(t)?t:0}var Ni=yi.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Ai(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,i=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY;ut.each(e.data.datasets,function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);ut.each(a.data,function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(i=Math.min(r,i),n=Math.max(r,n))})}}),t.min=i===Number.POSITIVE_INFINITY?0:i,t.max=n===Number.NEGATIVE_INFINITY?0:n,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Ai(this.options))},convertTicksToLabels:function(){var t=this;yi.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map(t.options.pointLabels.callback,t)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,i,n,a=ut.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=Ii(t);for(e=0;er.r&&(r.r=f.end,o.r=h),g.startr.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,i){var n=this,a=e.l/Math.sin(i.l),r=Math.max(e.r-n.width,0)/Math.sin(i.r),o=-e.t/Math.cos(i.t),s=-Math.max(e.b-(n.height-n.paddingTop),0)/Math.cos(i.b);a=Yi(a),r=Yi(r),o=Yi(o),s=Yi(s),n.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),n.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,i,n){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=i+a.drawingArea,l=a.height-a.paddingTop-n-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){return t*(2*Math.PI/Ii(this))+(this.chart.options&&this.chart.options.startAngle?this.chart.options.startAngle:0)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(null===t)return 0;var i=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*i:(t-e.min)*i},getPointPosition:function(t,e){var i=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(i)*e+this.xCenter,y:Math.sin(i)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(){var t=this.min,e=this.max;return this.getPointPositionForValue(0,this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0)},draw:function(){var t=this,e=t.options,i=e.gridLines,n=e.ticks;if(e.display){var a=t.ctx,r=this.getIndexAngle(0),o=ut.options._parseFont(n);(e.angleLines.display||e.pointLabels.display)&&function(t){var e=t.ctx,i=t.options,n=i.angleLines,a=i.gridLines,r=i.pointLabels,o=Ci(n.lineWidth,a.lineWidth),s=Ci(n.color,a.color),l=Ai(i);e.save(),e.lineWidth=o,e.strokeStyle=s,e.setLineDash&&(e.setLineDash(Ti([n.borderDash,a.borderDash,[]])),e.lineDashOffset=Ti([n.borderDashOffset,a.borderDashOffset,0]));var u=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),d=ut.options._parseFont(r);e.font=d.string,e.textBaseline="middle";for(var h=Ii(t)-1;h>=0;h--){if(n.display&&o&&s){var c=t.getPointPosition(h,u);e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(c.x,c.y),e.stroke()}if(r.display){var f=0===h?l/2:0,g=t.getPointPosition(h,u+f+5),m=Pi(r.fontColor,h,ot.global.defaultFontColor);e.fillStyle=m;var p=t.getIndexAngle(h),v=ut.toDegrees(p);e.textAlign=Ri(v),Wi(v,t._pointLabelSizes[h],g),Li(e,t.pointLabels[h]||"",g,d.lineHeight)}}e.restore()}(t),ut.each(t.ticks,function(e,s){if(s>0||n.reverse){var l=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]);if(i.display&&0!==s&&function(t,e,i,n){var a,r=t.ctx,o=e.circular,s=Ii(t),l=Pi(e.color,n-1),u=Pi(e.lineWidth,n-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,i,0,2*Math.PI);else{a=t.getPointPosition(0,i),r.moveTo(a.x,a.y);for(var d=1;d=0&&o<=s;){if(a=t[(n=o+s>>1)-1]||null,r=t[n],!a)return{lo:null,hi:r};if(r[e]i))return{lo:a,hi:r};s=n-1}}return{lo:r,hi:null}}(t,e,i),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(i-r[e])/s:0,u=(o[n]-r[n])*l;return r[n]+u}function Zi(t,e){var i=t._adapter,n=t.options.time,a=n.parser,r=a||n.format,o=e;return"function"==typeof a&&(o=a(o)),ut.isFinite(o)||(o="string"==typeof r?i.parse(o,r):i.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),ut.isFinite(o)||(o=i.parse(o))),o)}function $i(t,e){if(ut.isNullOrUndef(e))return null;var i=t.options.time,n=Zi(t,t.getRightValue(e));return null===n?n:(i.round&&(n=+t._adapter.startOf(n,i.round)),n)}function Xi(t){for(var e=ji.indexOf(t)+1,i=ji.length;e=a&&i<=r&&u.push(i);return n.min=a,n.max=r,n._unit=s.unit||function(t,e,i,n,a){var r,o;for(r=ji.length-1;r>=ji.indexOf(i);r--)if(o=ji[r],Bi[o].common&&t._adapter.diff(a,n,o)>=e.length)return o;return ji[i?ji.indexOf(i):0]}(n,u,s.minUnit,n.min,n.max),n._majorUnit=Xi(n._unit),n._table=function(t,e,i,n){if("linear"===n||!t.length)return[{time:e,pos:0},{time:i,pos:1}];var a,r,o,s,l,u=[],d=[e];for(a=0,r=t.length;ae&&s=0&&t0?o:1}}),Qi={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};Ji._defaults=Qi;var tn,en={category:gi,linear:xi,logarithmic:Si,radialLinear:Ni,time:Ji},nn=(function(t,e){t.exports=function(){var e,i;function n(){return e.apply(null,arguments)}function a(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function r(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function o(t){return void 0===t}function s(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function l(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function u(t,e){var i,n=[];for(i=0;i>>0,n=0;n0)for(i=0;i=0;return(r?i?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+n}var z=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},E={};function B(t,e,i,n){var a=n;"string"==typeof n&&(a=function(){return this[n]()}),t&&(E[t]=a),e&&(E[e[0]]=function(){return N(a.apply(this,arguments),e[1],e[2])}),i&&(E[i]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function j(t,e){return t.isValid()?(e=U(e,t.localeData()),H[e]=H[e]||function(t){var e,i,n,a=t.match(z);for(e=0,i=a.length;e=0&&V.test(t);)t=t.replace(V,n),V.lastIndex=0,i-=1;return t}var G=/\d/,q=/\d\d/,Z=/\d{3}/,$=/\d{4}/,X=/[+-]?\d{6}/,K=/\d\d?/,J=/\d\d\d\d?/,Q=/\d\d\d\d\d\d?/,tt=/\d{1,3}/,et=/\d{1,4}/,it=/[+-]?\d{1,6}/,nt=/\d+/,at=/[+-]?\d+/,rt=/Z|[+-]\d\d:?\d\d/gi,ot=/Z|[+-]\d\d(?::?\d\d)?/gi,st=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,lt={};function ut(t,e,i){lt[t]=T(e)?e:function(t,n){return t&&i?i:e}}function dt(t,e){return d(lt,t)?lt[t](e._strict,e._locale):new RegExp(ht(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,a){return e||i||n||a})))}function ht(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ct={};function ft(t,e){var i,n=e;for("string"==typeof t&&(t=[t]),s(e)&&(n=function(t,i){i[e]=k(t)}),i=0;i68?1900:2e3)};var Ct,Pt=Tt("FullYear",!0);function Tt(t,e){return function(i){return null!=i?(It(this,t,i),n.updateOffset(this,e),this):Ot(this,t)}}function Ot(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function It(t,e,i){t.isValid()&&!isNaN(i)&&("FullYear"===e&&Dt(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](i,t.month(),At(i,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](i))}function At(t,e){if(isNaN(t)||isNaN(e))return NaN;var i,n=(e%(i=12)+i)%i;return t+=(e-n)/12,1===n?Dt(t)?29:28:31-n%7%2}Ct=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=0){var i=Array.prototype.slice.call(arguments);i[0]=t+400,e=new Date(Date.UTC.apply(null,i)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Et(t,e,i){var n=7+e-i,a=(7+Ht(t,0,n).getUTCDay()-e)%7;return-a+n-1}function Bt(t,e,i,n,a){var r,o,s=(7+i-n)%7,l=Et(t,n,a),u=1+7*(e-1)+s+l;return u<=0?o=St(r=t-1)+u:u>St(t)?(r=t+1,o=u-St(t)):(r=t,o=u),{year:r,dayOfYear:o}}function jt(t,e,i){var n,a,r=Et(t.year(),e,i),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?(a=t.year()-1,n=o+Ut(a,e,i)):o>Ut(t.year(),e,i)?(n=o-Ut(t.year(),e,i),a=t.year()+1):(a=t.year(),n=o),{week:n,year:a}}function Ut(t,e,i){var n=Et(t,e,i),a=Et(t+1,e,i);return(St(t)-n+a)/7}function Gt(t,e){return t.slice(e,7).concat(t.slice(0,e))}B("w",["ww",2],"wo","week"),B("W",["WW",2],"Wo","isoWeek"),F("week","w"),F("isoWeek","W"),Y("week",5),Y("isoWeek",5),ut("w",K),ut("ww",K,q),ut("W",K),ut("WW",K,q),gt(["w","ww","W","WW"],function(t,e,i,n){e[n.substr(0,1)]=k(t)}),B("d",0,"do","day"),B("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),B("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),B("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),B("e",0,0,"weekday"),B("E",0,0,"isoWeekday"),F("day","d"),F("weekday","e"),F("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),ut("d",K),ut("e",K),ut("E",K),ut("dd",function(t,e){return e.weekdaysMinRegex(t)}),ut("ddd",function(t,e){return e.weekdaysShortRegex(t)}),ut("dddd",function(t,e){return e.weekdaysRegex(t)}),gt(["dd","ddd","dddd"],function(t,e,i,n){var a=i._locale.weekdaysParse(t,n,i._strict);null!=a?e.d=a:f(i).invalidWeekday=t}),gt(["d","e","E"],function(t,e,i,n){e[n]=k(t)});var qt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Zt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$t="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Xt=st,Kt=st,Jt=st;function Qt(){function t(t,e){return e.length-t.length}var e,i,n,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)i=c([2e3,1]).day(e),n=this.weekdaysMin(i,""),a=this.weekdaysShort(i,""),r=this.weekdays(i,""),o.push(n),s.push(a),l.push(r),u.push(n),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ht(s[e]),l[e]=ht(l[e]),u[e]=ht(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function te(){return this.hours()%12||12}function ee(t,e){B(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function ie(t,e){return e._meridiemParse}B("H",["HH",2],0,"hour"),B("h",["hh",2],0,te),B("k",["kk",2],0,function(){return this.hours()||24}),B("hmm",0,0,function(){return""+te.apply(this)+N(this.minutes(),2)}),B("hmmss",0,0,function(){return""+te.apply(this)+N(this.minutes(),2)+N(this.seconds(),2)}),B("Hmm",0,0,function(){return""+this.hours()+N(this.minutes(),2)}),B("Hmmss",0,0,function(){return""+this.hours()+N(this.minutes(),2)+N(this.seconds(),2)}),ee("a",!0),ee("A",!1),F("hour","h"),Y("hour",13),ut("a",ie),ut("A",ie),ut("H",K),ut("h",K),ut("k",K),ut("HH",K,q),ut("hh",K,q),ut("kk",K,q),ut("hmm",J),ut("hmmss",Q),ut("Hmm",J),ut("Hmmss",Q),ft(["H","HH"],bt),ft(["k","kk"],function(t,e,i){var n=k(t);e[bt]=24===n?0:n}),ft(["a","A"],function(t,e,i){i._isPm=i._locale.isPM(t),i._meridiem=t}),ft(["h","hh"],function(t,e,i){e[bt]=k(t),f(i).bigHour=!0}),ft("hmm",function(t,e,i){var n=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n)),f(i).bigHour=!0}),ft("hmmss",function(t,e,i){var n=t.length-4,a=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n,2)),e[_t]=k(t.substr(a)),f(i).bigHour=!0}),ft("Hmm",function(t,e,i){var n=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n))}),ft("Hmmss",function(t,e,i){var n=t.length-4,a=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n,2)),e[_t]=k(t.substr(a))});var ne,ae=Tt("Hours",!0),re={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Lt,week:{dow:0,doy:6},weekdays:qt,weekdaysMin:$t,weekdaysShort:Zt,meridiemParse:/[ap]\.?m?\.?/i},oe={},se={};function le(t){return t?t.toLowerCase().replace("_","-"):t}function ue(e){var i=null;if(!oe[e]&&t&&t.exports)try{i=ne._abbr;var n=_e;n("./locale/"+e),de(i)}catch(t){}return oe[e]}function de(t,e){var i;return t&&((i=o(e)?ce(t):he(t,e))?ne=i:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),ne._abbr}function he(t,e){if(null!==e){var i,n=re;if(e.abbr=t,null!=oe[t])P("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),n=oe[t]._config;else if(null!=e.parentLocale)if(null!=oe[e.parentLocale])n=oe[e.parentLocale]._config;else{if(null==(i=ue(e.parentLocale)))return se[e.parentLocale]||(se[e.parentLocale]=[]),se[e.parentLocale].push({name:t,config:e}),null;n=i._config}return oe[t]=new I(O(n,e)),se[t]&&se[t].forEach(function(t){he(t.name,t.config)}),de(t),oe[t]}return delete oe[t],null}function ce(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return ne;if(!a(t)){if(e=ue(t))return e;t=[t]}return function(t){for(var e,i,n,a,r=0;r0;){if(n=ue(a.slice(0,e).join("-")))return n;if(i&&i.length>=e&&w(a,i,!0)>=e-1)break;e--}r++}return ne}(t)}function fe(t){var e,i=t._a;return i&&-2===f(t).overflow&&(e=i[vt]<0||i[vt]>11?vt:i[yt]<1||i[yt]>At(i[pt],i[vt])?yt:i[bt]<0||i[bt]>24||24===i[bt]&&(0!==i[xt]||0!==i[_t]||0!==i[kt])?bt:i[xt]<0||i[xt]>59?xt:i[_t]<0||i[_t]>59?_t:i[kt]<0||i[kt]>999?kt:-1,f(t)._overflowDayOfYear&&(eyt)&&(e=yt),f(t)._overflowWeeks&&-1===e&&(e=wt),f(t)._overflowWeekday&&-1===e&&(e=Mt),f(t).overflow=e),t}function ge(t,e,i){return null!=t?t:null!=e?e:i}function me(t){var e,i,a,r,o,s=[];if(!t._d){for(a=function(t){var e=new Date(n.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[vt]&&function(t){var e,i,n,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,i=ge(e.GG,t._a[pt],jt(Ie(),1,4).year),n=ge(e.W,1),((a=ge(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=jt(Ie(),r,o);i=ge(e.gg,t._a[pt],u.year),n=ge(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}n<1||n>Ut(i,r,o)?f(t)._overflowWeeks=!0:null!=l?f(t)._overflowWeekday=!0:(s=Bt(i,n,a,r,o),t._a[pt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=ge(t._a[pt],a[pt]),(t._dayOfYear>St(o)||0===t._dayOfYear)&&(f(t)._overflowDayOfYear=!0),i=Ht(o,0,t._dayOfYear),t._a[vt]=i.getUTCMonth(),t._a[yt]=i.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=a[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[bt]&&0===t._a[xt]&&0===t._a[_t]&&0===t._a[kt]&&(t._nextDay=!0,t._a[bt]=0),t._d=(t._useUTC?Ht:function(t,e,i,n,a,r,o){var s;return t<100&&t>=0?(s=new Date(t+400,e,i,n,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,i,n,a,r,o),s}).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[bt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(f(t).weekdayMismatch=!0)}}var pe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ve=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ye=/Z|[+-]\d\d(?::?\d\d)?/,be=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],xe=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],ke=/^\/?Date\((\-?\d+)/i;function we(t){var e,i,n,a,r,o,s=t._i,l=pe.exec(s)||ve.exec(s);if(l){for(f(t).iso=!0,e=0,i=be.length;e0&&f(t).unusedInput.push(o),s=s.slice(s.indexOf(i)+i.length),u+=i.length),E[r]?(i?f(t).empty=!1:f(t).unusedTokens.push(r),mt(r,i,t)):t._strict&&!i&&f(t).unusedTokens.push(r);f(t).charsLeftOver=l-u,s.length>0&&f(t).unusedInput.push(s),t._a[bt]<=12&&!0===f(t).bigHour&&t._a[bt]>0&&(f(t).bigHour=void 0),f(t).parsedDateParts=t._a.slice(0),f(t).meridiem=t._meridiem,t._a[bt]=(d=t._locale,h=t._a[bt],null==(c=t._meridiem)?h:null!=d.meridiemHour?d.meridiemHour(h,c):null!=d.isPM?((g=d.isPM(c))&&h<12&&(h+=12),g||12!==h||(h=0),h):h),me(t),fe(t)}else Ce(t);else we(t);var d,h,c,g}function Te(t){var e=t._i,i=t._f;return t._locale=t._locale||ce(t._l),null===e||void 0===i&&""===e?m({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),x(e)?new b(fe(e)):(l(e)?t._d=e:a(i)?function(t){var e,i,n,a,r;if(0===t._f.length)return f(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis?this:t:m()});function Re(t,e){var i,n;if(1===e.length&&a(e[0])&&(e=e[0]),!e.length)return Ie();for(i=e[0],n=1;n=0?new Date(t+400,e,i)-si:new Date(t,e,i).valueOf()}function di(t,e,i){return t<100&&t>=0?Date.UTC(t+400,e,i)-si:Date.UTC(t,e,i)}function hi(t,e){B(0,[t,t.length],0,e)}function ci(t,e,i,n,a){var r;return null==t?jt(this,n,a).year:(r=Ut(t,n,a),e>r&&(e=r),function(t,e,i,n,a){var r=Bt(t,e,i,n,a),o=Ht(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}.call(this,t,e,i,n,a))}B(0,["gg",2],0,function(){return this.weekYear()%100}),B(0,["GG",2],0,function(){return this.isoWeekYear()%100}),hi("gggg","weekYear"),hi("ggggg","weekYear"),hi("GGGG","isoWeekYear"),hi("GGGGG","isoWeekYear"),F("weekYear","gg"),F("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),ut("G",at),ut("g",at),ut("GG",K,q),ut("gg",K,q),ut("GGGG",et,$),ut("gggg",et,$),ut("GGGGG",it,X),ut("ggggg",it,X),gt(["gggg","ggggg","GGGG","GGGGG"],function(t,e,i,n){e[n.substr(0,2)]=k(t)}),gt(["gg","GG"],function(t,e,i,a){e[a]=n.parseTwoDigitYear(t)}),B("Q",0,"Qo","quarter"),F("quarter","Q"),Y("quarter",7),ut("Q",G),ft("Q",function(t,e){e[vt]=3*(k(t)-1)}),B("D",["DD",2],"Do","date"),F("date","D"),Y("date",9),ut("D",K),ut("DD",K,q),ut("Do",function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient}),ft(["D","DD"],yt),ft("Do",function(t,e){e[yt]=k(t.match(K)[0])});var fi=Tt("Date",!0);B("DDD",["DDDD",3],"DDDo","dayOfYear"),F("dayOfYear","DDD"),Y("dayOfYear",4),ut("DDD",tt),ut("DDDD",Z),ft(["DDD","DDDD"],function(t,e,i){i._dayOfYear=k(t)}),B("m",["mm",2],0,"minute"),F("minute","m"),Y("minute",14),ut("m",K),ut("mm",K,q),ft(["m","mm"],xt);var gi=Tt("Minutes",!1);B("s",["ss",2],0,"second"),F("second","s"),Y("second",15),ut("s",K),ut("ss",K,q),ft(["s","ss"],_t);var mi,pi=Tt("Seconds",!1);for(B("S",0,0,function(){return~~(this.millisecond()/100)}),B(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),B(0,["SSS",3],0,"millisecond"),B(0,["SSSS",4],0,function(){return 10*this.millisecond()}),B(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),B(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),B(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),B(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),B(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),F("millisecond","ms"),Y("millisecond",16),ut("S",tt,G),ut("SS",tt,q),ut("SSS",tt,Z),mi="SSSS";mi.length<=9;mi+="S")ut(mi,nt);function vi(t,e){e[kt]=k(1e3*("0."+t))}for(mi="S";mi.length<=9;mi+="S")ft(mi,vi);var yi=Tt("Milliseconds",!1);B("z",0,0,"zoneAbbr"),B("zz",0,0,"zoneName");var bi=b.prototype;function xi(t){return t}bi.add=Je,bi.calendar=function(t,e){var i=t||Ie(),a=Ee(i,this).startOf("day"),r=n.calendarFormat(this,a)||"sameElse",o=e&&(T(e[r])?e[r].call(this,i):e[r]);return this.format(o||this.localeData().calendar(r,this,Ie(i)))},bi.clone=function(){return new b(this)},bi.diff=function(t,e,i){var n,a,r;if(!this.isValid())return NaN;if(!(n=Ee(t,this)).isValid())return NaN;switch(a=6e4*(n.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=ti(this,n)/12;break;case"month":r=ti(this,n);break;case"quarter":r=ti(this,n)/3;break;case"second":r=(this-n)/1e3;break;case"minute":r=(this-n)/6e4;break;case"hour":r=(this-n)/36e5;break;case"day":r=(this-n-a)/864e5;break;case"week":r=(this-n-a)/6048e5;break;default:r=this-n}return i?r:_(r)},bi.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var i=this._isUTC?di:ui;switch(t){case"year":e=i(this.year()+1,0,1)-1;break;case"quarter":e=i(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=i(this.year(),this.month()+1,1)-1;break;case"week":e=i(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=i(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=i(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=oi-li(e+(this._isUTC?0:this.utcOffset()*ri),oi)-1;break;case"minute":e=this._d.valueOf(),e+=ri-li(e,ri)-1;break;case"second":e=this._d.valueOf(),e+=ai-li(e,ai)-1}return this._d.setTime(e),n.updateOffset(this,!0),this},bi.format=function(t){t||(t=this.isUtc()?n.defaultFormatUtc:n.defaultFormat);var e=j(this,t);return this.localeData().postformat(e)},bi.from=function(t,e){return this.isValid()&&(x(t)&&t.isValid()||Ie(t).isValid())?qe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},bi.fromNow=function(t){return this.from(Ie(),t)},bi.to=function(t,e){return this.isValid()&&(x(t)&&t.isValid()||Ie(t).isValid())?qe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},bi.toNow=function(t){return this.to(Ie(),t)},bi.get=function(t){return T(this[t=R(t)])?this[t]():this},bi.invalidAt=function(){return f(this).overflow},bi.isAfter=function(t,e){var i=x(t)?t:Ie(t);return!(!this.isValid()||!i.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>i.valueOf():i.valueOf()9999?j(i,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):T(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",j(i,"Z")):j(i,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},bi.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var i="["+t+'("]',n=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(i+n+"-MM-DD[T]HH:mm:ss.SSS"+a)},bi.toJSON=function(){return this.isValid()?this.toISOString():null},bi.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},bi.unix=function(){return Math.floor(this.valueOf()/1e3)},bi.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},bi.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},bi.year=Pt,bi.isLeapYear=function(){return Dt(this.year())},bi.weekYear=function(t){return ci.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},bi.isoWeekYear=function(t){return ci.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},bi.quarter=bi.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},bi.month=Yt,bi.daysInMonth=function(){return At(this.year(),this.month())},bi.week=bi.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},bi.isoWeek=bi.isoWeeks=function(t){var e=jt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},bi.weeksInYear=function(){var t=this.localeData()._week;return Ut(this.year(),t.dow,t.doy)},bi.isoWeeksInYear=function(){return Ut(this.year(),1,4)},bi.date=fi,bi.day=bi.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},bi.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},bi.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},bi.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},bi.hour=bi.hours=ae,bi.minute=bi.minutes=gi,bi.second=bi.seconds=pi,bi.millisecond=bi.milliseconds=yi,bi.utcOffset=function(t,e,i){var a,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=He(ot,t)))return this}else Math.abs(t)<16&&!i&&(t*=60);return!this._isUTC&&e&&(a=Be(this)),this._offset=t,this._isUTC=!0,null!=a&&this.add(a,"m"),r!==t&&(!e||this._changeInProgress?Ke(this,qe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,n.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Be(this)},bi.utc=function(t){return this.utcOffset(0,t)},bi.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Be(this),"m")),this},bi.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=He(rt,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},bi.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Ie(t).utcOffset():0,(this.utcOffset()-t)%60==0)},bi.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},bi.isLocal=function(){return!!this.isValid()&&!this._isUTC},bi.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},bi.isUtc=je,bi.isUTC=je,bi.zoneAbbr=function(){return this._isUTC?"UTC":""},bi.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},bi.dates=S("dates accessor is deprecated. Use date instead.",fi),bi.months=S("months accessor is deprecated. Use month instead",Yt),bi.years=S("years accessor is deprecated. Use year instead",Pt),bi.zone=S("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}),bi.isDSTShifted=S("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var t={};if(v(t,this),(t=Te(t))._a){var e=t._isUTC?c(t._a):Ie(t._a);this._isDSTShifted=this.isValid()&&w(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted});var _i=I.prototype;function ki(t,e,i,n){var a=ce(),r=c().set(n,e);return a[i](r,t)}function wi(t,e,i){if(s(t)&&(e=t,t=void 0),t=t||"",null!=e)return ki(t,e,i,"month");var n,a=[];for(n=0;n<12;n++)a[n]=ki(t,n,i,"month");return a}function Mi(t,e,i,n){"boolean"==typeof t?(s(e)&&(i=e,e=void 0),e=e||""):(i=e=t,t=!1,s(e)&&(i=e,e=void 0),e=e||"");var a,r=ce(),o=t?r._week.dow:0;if(null!=i)return ki(e,(i+o)%7,n,"day");var l=[];for(a=0;a<7;a++)l[a]=ki(e,(a+o)%7,n,"day");return l}_i.calendar=function(t,e,i){var n=this._calendar[t]||this._calendar.sameElse;return T(n)?n.call(e,i):n},_i.longDateFormat=function(t){var e=this._longDateFormat[t],i=this._longDateFormat[t.toUpperCase()];return e||!i?e:(this._longDateFormat[t]=i.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])},_i.invalidDate=function(){return this._invalidDate},_i.ordinal=function(t){return this._ordinal.replace("%d",t)},_i.preparse=xi,_i.postformat=xi,_i.relativeTime=function(t,e,i,n){var a=this._relativeTime[i];return T(a)?a(t,e,i,n):a.replace(/%d/i,t)},_i.pastFuture=function(t,e){var i=this._relativeTime[t>0?"future":"past"];return T(i)?i(e):i.replace(/%s/i,e)},_i.set=function(t){var e,i;for(i in t)T(e=t[i])?this[i]=e:this["_"+i]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},_i.months=function(t,e){return t?a(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Ft).test(e)?"format":"standalone"][t.month()]:a(this._months)?this._months:this._months.standalone},_i.monthsShort=function(t,e){return t?a(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Ft.test(e)?"format":"standalone"][t.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},_i.monthsParse=function(t,e,i){var n,a,r;if(this._monthsParseExact)return function(t,e,i){var n,a,r,o=t.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],n=0;n<12;++n)r=c([2e3,n]),this._shortMonthsParse[n]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[n]=this.months(r,"").toLocaleLowerCase();return i?"MMM"===e?-1!==(a=Ct.call(this._shortMonthsParse,o))?a:null:-1!==(a=Ct.call(this._longMonthsParse,o))?a:null:"MMM"===e?-1!==(a=Ct.call(this._shortMonthsParse,o))?a:-1!==(a=Ct.call(this._longMonthsParse,o))?a:null:-1!==(a=Ct.call(this._longMonthsParse,o))?a:-1!==(a=Ct.call(this._shortMonthsParse,o))?a:null}.call(this,t,e,i);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),n=0;n<12;n++){if(a=c([2e3,n]),i&&!this._longMonthsParse[n]&&(this._longMonthsParse[n]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[n]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),i||this._monthsParse[n]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[n]=new RegExp(r.replace(".",""),"i")),i&&"MMMM"===e&&this._longMonthsParse[n].test(t))return n;if(i&&"MMM"===e&&this._shortMonthsParse[n].test(t))return n;if(!i&&this._monthsParse[n].test(t))return n}},_i.monthsRegex=function(t){return this._monthsParseExact?(d(this,"_monthsRegex")||Vt.call(this),t?this._monthsStrictRegex:this._monthsRegex):(d(this,"_monthsRegex")||(this._monthsRegex=zt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},_i.monthsShortRegex=function(t){return this._monthsParseExact?(d(this,"_monthsRegex")||Vt.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(d(this,"_monthsShortRegex")||(this._monthsShortRegex=Nt),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},_i.week=function(t){return jt(t,this._week.dow,this._week.doy).week},_i.firstDayOfYear=function(){return this._week.doy},_i.firstDayOfWeek=function(){return this._week.dow},_i.weekdays=function(t,e){var i=a(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?Gt(i,this._week.dow):t?i[t.day()]:i},_i.weekdaysMin=function(t){return!0===t?Gt(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},_i.weekdaysShort=function(t){return!0===t?Gt(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},_i.weekdaysParse=function(t,e,i){var n,a,r;if(this._weekdaysParseExact)return function(t,e,i){var n,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],n=0;n<7;++n)r=c([2e3,1]).day(n),this._minWeekdaysParse[n]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[n]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[n]=this.weekdays(r,"").toLocaleLowerCase();return i?"dddd"===e?-1!==(a=Ct.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:null}.call(this,t,e,i);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),n=0;n<7;n++){if(a=c([2e3,1]).day(n),i&&!this._fullWeekdaysParse[n]&&(this._fullWeekdaysParse[n]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[n]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[n]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[n]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[n]=new RegExp(r.replace(".",""),"i")),i&&"dddd"===e&&this._fullWeekdaysParse[n].test(t))return n;if(i&&"ddd"===e&&this._shortWeekdaysParse[n].test(t))return n;if(i&&"dd"===e&&this._minWeekdaysParse[n].test(t))return n;if(!i&&this._weekdaysParse[n].test(t))return n}},_i.weekdaysRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(d(this,"_weekdaysRegex")||(this._weekdaysRegex=Xt),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},_i.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(d(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Kt),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},_i.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(d(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Jt),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},_i.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},_i.meridiem=function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},de("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,i=1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),n.lang=S("moment.lang is deprecated. Use moment.locale instead.",de),n.langData=S("moment.langData is deprecated. Use moment.localeData instead.",ce);var Si=Math.abs;function Di(t,e,i,n){var a=qe(e,i);return t._milliseconds+=n*a._milliseconds,t._days+=n*a._days,t._months+=n*a._months,t._bubble()}function Ci(t){return t<0?Math.floor(t):Math.ceil(t)}function Pi(t){return 4800*t/146097}function Ti(t){return 146097*t/4800}function Oi(t){return function(){return this.as(t)}}var Ii=Oi("ms"),Ai=Oi("s"),Fi=Oi("m"),Ri=Oi("h"),Li=Oi("d"),Wi=Oi("w"),Yi=Oi("M"),Ni=Oi("Q"),zi=Oi("y");function Vi(t){return function(){return this.isValid()?this._data[t]:NaN}}var Hi=Vi("milliseconds"),Ei=Vi("seconds"),Bi=Vi("minutes"),ji=Vi("hours"),Ui=Vi("days"),Gi=Vi("months"),qi=Vi("years"),Zi=Math.round,$i={ss:44,s:45,m:45,h:22,d:26,M:11},Xi=Math.abs;function Ki(t){return(t>0)-(t<0)||+t}function Ji(){if(!this.isValid())return this.localeData().invalidDate();var t,e,i=Xi(this._milliseconds)/1e3,n=Xi(this._days),a=Xi(this._months);t=_(i/60),e=_(t/60),i%=60,t%=60;var r=_(a/12),o=a%=12,s=n,l=e,u=t,d=i?i.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=Ki(this._months)!==Ki(h)?"-":"",g=Ki(this._days)!==Ki(h)?"-":"",m=Ki(this._milliseconds)!==Ki(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var Qi=We.prototype;return Qi.isValid=function(){return this._isValid},Qi.abs=function(){var t=this._data;return this._milliseconds=Si(this._milliseconds),this._days=Si(this._days),this._months=Si(this._months),t.milliseconds=Si(t.milliseconds),t.seconds=Si(t.seconds),t.minutes=Si(t.minutes),t.hours=Si(t.hours),t.months=Si(t.months),t.years=Si(t.years),this},Qi.add=function(t,e){return Di(this,t,e,1)},Qi.subtract=function(t,e){return Di(this,t,e,-1)},Qi.as=function(t){if(!this.isValid())return NaN;var e,i,n=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+n/864e5,i=this._months+Pi(e),t){case"month":return i;case"quarter":return i/3;case"year":return i/12}else switch(e=this._days+Math.round(Ti(this._months)),t){case"week":return e/7+n/6048e5;case"day":return e+n/864e5;case"hour":return 24*e+n/36e5;case"minute":return 1440*e+n/6e4;case"second":return 86400*e+n/1e3;case"millisecond":return Math.floor(864e5*e)+n;default:throw new Error("Unknown unit "+t)}},Qi.asMilliseconds=Ii,Qi.asSeconds=Ai,Qi.asMinutes=Fi,Qi.asHours=Ri,Qi.asDays=Li,Qi.asWeeks=Wi,Qi.asMonths=Yi,Qi.asQuarters=Ni,Qi.asYears=zi,Qi.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},Qi._bubble=function(){var t,e,i,n,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Ci(Ti(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=_(r/1e3),l.seconds=t%60,e=_(t/60),l.minutes=e%60,i=_(e/60),l.hours=i%24,o+=_(i/24),a=_(Pi(o)),s+=a,o-=Ci(Ti(a)),n=_(s/12),s%=12,l.days=o,l.months=s,l.years=n,this},Qi.clone=function(){return qe(this)},Qi.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},Qi.milliseconds=Hi,Qi.seconds=Ei,Qi.minutes=Bi,Qi.hours=ji,Qi.days=Ui,Qi.weeks=function(){return _(this.days()/7)},Qi.months=Gi,Qi.years=qi,Qi.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),i=function(t,e,i){var n=qe(t).abs(),a=Zi(n.as("s")),r=Zi(n.as("m")),o=Zi(n.as("h")),s=Zi(n.as("d")),l=Zi(n.as("M")),u=Zi(n.as("y")),d=a<=$i.ss&&["s",a]||a<$i.s&&["ss",a]||r<=1&&["m"]||r<$i.m&&["mm",r]||o<=1&&["h"]||o<$i.h&&["hh",o]||s<=1&&["d"]||s<$i.d&&["dd",s]||l<=1&&["M"]||l<$i.M&&["MM",l]||u<=1&&["y"]||["yy",u];return d[2]=e,d[3]=+t>0,d[4]=i,function(t,e,i,n,a){return a.relativeTime(e||1,!!i,t,n)}.apply(null,d)}(this,!t,e);return t&&(i=e.pastFuture(+this,i)),e.postformat(i)},Qi.toISOString=Ji,Qi.toString=Ji,Qi.toJSON=Ji,Qi.locale=ei,Qi.localeData=ni,Qi.toIsoString=S("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Ji),Qi.lang=ii,B("X",0,0,"unix"),B("x",0,0,"valueOf"),ut("x",at),ut("X",/[+-]?\d+(\.\d{1,3})?/),ft("X",function(t,e,i){i._d=new Date(1e3*parseFloat(t,10))}),ft("x",function(t,e,i){i._d=new Date(k(t))}),n.version="2.24.0",e=Ie,n.fn=bi,n.min=function(){return Re("isBefore",[].slice.call(arguments,0))},n.max=function(){return Re("isAfter",[].slice.call(arguments,0))},n.now=function(){return Date.now?Date.now():+new Date},n.utc=c,n.unix=function(t){return Ie(1e3*t)},n.months=function(t,e){return wi(t,e,"months")},n.isDate=l,n.locale=de,n.invalid=m,n.duration=qe,n.isMoment=x,n.weekdays=function(t,e,i){return Mi(t,e,i,"weekdays")},n.parseZone=function(){return Ie.apply(null,arguments).parseZone()},n.localeData=ce,n.isDuration=Ye,n.monthsShort=function(t,e){return wi(t,e,"monthsShort")},n.weekdaysMin=function(t,e,i){return Mi(t,e,i,"weekdaysMin")},n.defineLocale=he,n.updateLocale=function(t,e){if(null!=e){var i,n,a=re;null!=(n=ue(t))&&(a=n._config),e=O(a,e),(i=new I(e)).parentLocale=oe[t],oe[t]=i,de(t)}else null!=oe[t]&&(null!=oe[t].parentLocale?oe[t]=oe[t].parentLocale:null!=oe[t]&&delete oe[t]);return oe[t]},n.locales=function(){return D(oe)},n.weekdaysShort=function(t,e,i){return Mi(t,e,i,"weekdaysShort")},n.normalizeUnits=R,n.relativeTimeRounding=function(t){return void 0===t?Zi:"function"==typeof t&&(Zi=t,!0)},n.relativeTimeThreshold=function(t,e){return void 0!==$i[t]&&(void 0===e?$i[t]:($i[t]=e,"s"===t&&($i.ss=e-1),!0))},n.calendarFormat=function(t,e){var i=t.diff(e,"days",!0);return i<-6?"sameElse":i<-1?"lastWeek":i<0?"lastDay":i<1?"sameDay":i<2?"nextDay":i<7?"nextWeek":"sameElse"},n.prototype=bi,n.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},n}()}(tn={exports:{}},tn.exports),tn.exports),an={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};si._date.override("function"==typeof nn?{_id:"moment",formats:function(){return an},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=nn(t,e):t instanceof nn||(t=nn(t)),t.isValid()?t.valueOf():null},format:function(t,e){return nn(t).format(e)},add:function(t,e,i){return nn(t).add(e,i).valueOf()},diff:function(t,e,i){return nn.duration(nn(t).diff(nn(e))).as(i)},startOf:function(t,e,i){return t=nn(t),"isoWeek"===e?t.isoWeekday(i).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return nn(t).endOf(e).valueOf()},_create:function(t){return nn(t)}}:{}),ot._set("global",{plugins:{filler:{propagate:!0}}});var rn={dataset:function(t){var e=t.fill,i=t.chart,n=i.getDatasetMeta(e),a=n&&i.isDatasetVisible(e)&&n.dataset._children||[],r=a.length||0;return r?function(t,e){return e=i)&&n;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function sn(t){var e,i=t.el._model||{},n=t.el._scale||{},a=t.fill,r=null;if(isFinite(a))return null;if("start"===a?r=void 0===i.scaleBottom?n.bottom:i.scaleBottom:"end"===a?r=void 0===i.scaleTop?n.top:i.scaleTop:void 0!==i.scaleZero?r=i.scaleZero:n.getBasePosition?r=n.getBasePosition():n.getBasePixel&&(r=n.getBasePixel()),null!=r){if(void 0!==r.x&&void 0!==r.y)return r;if(ut.isFinite(r))return{x:(e=n.isHorizontal())?r:null,y:e?null:r}}return null}function ln(t,e,i){var n,a=t[e].fill,r=[e];if(!i)return a;for(;!1!==a&&-1===r.indexOf(a);){if(!isFinite(a))return a;if(!(n=t[a]))return!1;if(n.visible)return a;r.push(a),a=n.fill}return!1}function un(t){var e=t.fill,i="dataset";return!1===e?null:(isFinite(e)||(i="boundary"),rn[i](t))}function dn(t){return t&&!t.skip}function hn(t,e,i,n,a){var r;if(n&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r0;--r)ut.canvas.lineTo(t,i[r],i[r-1],!0)}}var cn={id:"filler",afterDatasetsUpdate:function(t,e){var i,n,a,r,o=(t.data.datasets||[]).length,s=e.propagate,l=[];for(n=0;ne?e:t.boxWidth}ot._set("global",{legend:{display:!0,position:"top",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var i=e.datasetIndex,n=this.chart,a=n.getDatasetMeta(i);a.hidden=null===a.hidden?!n.data.datasets[i].hidden:null,n.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data;return ut.isArray(e.datasets)?e.datasets.map(function(e,i){return{text:e.label,fillStyle:ut.isArray(e.backgroundColor)?e.backgroundColor[0]:e.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:e.borderCapStyle,lineDash:e.borderDash,lineDashOffset:e.borderDashOffset,lineJoin:e.borderJoinStyle,lineWidth:e.borderWidth,strokeStyle:e.borderColor,pointStyle:e.pointStyle,datasetIndex:i}},this):[]}}},legendCallback:function(t){var e=[];e.push('
    ');for(var i=0;i'),t.data.datasets[i].label&&e.push(t.data.datasets[i].label),e.push("");return e.push("
"),e.join("")}});var pn=gt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1},beforeUpdate:fn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:fn,beforeSetDimensions:fn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:fn,beforeBuildLabels:fn,buildLabels:function(){var t=this,e=t.options.labels||{},i=ut.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter(function(i){return e.filter(i,t.chart.data)})),t.options.reverse&&i.reverse(),t.legendItems=i},afterBuildLabels:fn,beforeFit:fn,fit:function(){var t=this,e=t.options,i=e.labels,n=e.display,a=t.ctx,r=ut.options._parseFont(i),o=r.size,s=t.legendHitBoxes=[],l=t.minSize,u=t.isHorizontal();if(u?(l.width=t.maxWidth,l.height=n?10:0):(l.width=n?10:0,l.height=t.maxHeight),n)if(a.font=r.string,u){var d=t.lineWidths=[0],h=0;a.textAlign="left",a.textBaseline="top",ut.each(t.legendItems,function(t,e){var n=mn(i,o)+o/2+a.measureText(t.text).width;(0===e||d[d.length-1]+n+i.padding>l.width)&&(h+=o+i.padding,d[d.length-(e>0?0:1)]=i.padding),s[e]={left:0,top:0,width:n,height:o},d[d.length-1]+=n+i.padding}),l.height+=h}else{var c=i.padding,f=t.columnWidths=[],g=i.padding,m=0,p=0,v=o+c;ut.each(t.legendItems,function(t,e){var n=mn(i,o)+o/2+a.measureText(t.text).width;e>0&&p+v>l.height-c&&(g+=m+i.padding,f.push(m),m=0,p=0),m=Math.max(m,n),p+=v,s[e]={left:0,top:0,width:n,height:o}}),g+=m,f.push(m),l.width+=g}t.width=l.width,t.height=l.height},afterFit:fn,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,i=e.labels,n=ot.global,a=n.defaultColor,r=n.elements.line,o=t.width,s=t.lineWidths;if(e.display){var l,u=t.ctx,d=gn(i.fontColor,n.defaultFontColor),h=ut.options._parseFont(i),c=h.size;u.textAlign="left",u.textBaseline="middle",u.lineWidth=.5,u.strokeStyle=d,u.fillStyle=d,u.font=h.string;var f=mn(i,c),g=t.legendHitBoxes,m=t.isHorizontal();l=m?{x:t.left+(o-s[0])/2+i.padding,y:t.top+i.padding,line:0}:{x:t.left+i.padding,y:t.top+i.padding,line:0};var p=c+i.padding;ut.each(t.legendItems,function(n,d){var h=u.measureText(n.text).width,v=f+c/2+h,y=l.x,b=l.y;m?d>0&&y+v+i.padding>t.left+t.minSize.width&&(b=l.y+=p,l.line++,y=l.x=t.left+(o-s[l.line])/2+i.padding):d>0&&b+p>t.top+t.minSize.height&&(y=l.x=y+t.columnWidths[l.line]+i.padding,b=l.y=t.top+i.padding,l.line++),function(t,i,n){if(!(isNaN(f)||f<=0)){u.save();var o=gn(n.lineWidth,r.borderWidth);if(u.fillStyle=gn(n.fillStyle,a),u.lineCap=gn(n.lineCap,r.borderCapStyle),u.lineDashOffset=gn(n.lineDashOffset,r.borderDashOffset),u.lineJoin=gn(n.lineJoin,r.borderJoinStyle),u.lineWidth=o,u.strokeStyle=gn(n.strokeStyle,a),u.setLineDash&&u.setLineDash(gn(n.lineDash,r.borderDash)),e.labels&&e.labels.usePointStyle){var s=f*Math.SQRT2/2,l=t+f/2,d=i+c/2;ut.canvas.drawPoint(u,n.pointStyle,s,l,d)}else 0!==o&&u.strokeRect(t,i,f,c),u.fillRect(t,i,f,c);u.restore()}}(y,b,n),g[d].left=y,g[d].top=b,function(t,e,i,n){var a=c/2,r=f+a+t,o=e+a;u.fillText(i.text,r,o),i.hidden&&(u.beginPath(),u.lineWidth=2,u.moveTo(r,o),u.lineTo(r+n,o),u.stroke())}(y,b,n,h),m?l.x+=v+i.padding:l.y+=p})}},_getLegendItemAt:function(t,e){var i,n,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,i=0;i=(n=a[i]).left&&t<=n.left+n.width&&e>=n.top&&e<=n.top+n.height)return r.legendItems[i];return null},handleEvent:function(t){var e,i=this,n=i.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!n.onHover&&!n.onLeave)return}else{if("click"!==a)return;if(!n.onClick)return}e=i._getLegendItemAt(t.x,t.y),"click"===a?e&&n.onClick&&n.onClick.call(i,t.native,e):(n.onLeave&&e!==i._hoveredItem&&(i._hoveredItem&&n.onLeave.call(i,t.native,i._hoveredItem),i._hoveredItem=e),n.onHover&&e&&n.onHover.call(i,t.native,e))}});function vn(t,e){var i=new pn({ctx:t.ctx,options:e,chart:t});xe.configure(t,i,e),xe.addBox(t,i),t.legend=i}var yn={id:"legend",_element:pn,beforeInit:function(t){var e=t.options.legend;e&&vn(t,e)},beforeUpdate:function(t){var e=t.options.legend,i=t.legend;e?(ut.mergeIf(e,ot.global.legend),i?(xe.configure(t,i,e),i.options=e):vn(t,e)):i&&(xe.removeBox(t,i),delete t.legend)},afterEvent:function(t,e){var i=t.legend;i&&i.handleEvent(e)}},bn=ut.noop;ot._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var xn=gt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:bn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:bn,beforeSetDimensions:bn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:bn,beforeBuildLabels:bn,buildLabels:bn,afterBuildLabels:bn,beforeFit:bn,fit:function(){var t=this,e=t.options,i=e.display,n=t.minSize,a=ut.isArray(e.text)?e.text.length:1,r=ut.options._parseFont(e),o=i?a*r.lineHeight+2*e.padding:0;t.isHorizontal()?(n.width=t.maxWidth,n.height=o):(n.width=o,n.height=t.maxHeight),t.width=n.width,t.height=n.height},afterFit:bn,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,i=t.options;if(i.display){var n,a,r,o=ut.options._parseFont(i),s=o.lineHeight,l=s/2+i.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=ut.valueOrDefault(i.fontColor,ot.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,n=f-h):(a="left"===i.position?h+l:f-l,r=d+(c-d)/2,n=c-d,u=Math.PI*("left"===i.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=i.text;if(ut.isArray(g))for(var m=0,p=0;p=0;n--){var a=t[n];if(e(a))return a}},ut.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},ut.almostEquals=function(t,e,i){return Math.abs(t-e)t},ut.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},ut.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},ut.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0==(t=+t)||isNaN(t)?t:t>0?1:-1},ut.log10=Math.log10?function(t){return Math.log10(t)}:function(t){var e=Math.log(t)*Math.LOG10E,i=Math.round(e);return t===Math.pow(10,i)?i:e},ut.toRadians=function(t){return t*(Math.PI/180)},ut.toDegrees=function(t){return t*(180/Math.PI)},ut._decimalPlaces=function(t){if(ut.isFinite(t)){for(var e=1,i=0;Math.round(t*e)/e!==t;)e*=10,i++;return i}},ut.getAngleFromPoint=function(t,e){var i=e.x-t.x,n=e.y-t.y,a=Math.sqrt(i*i+n*n),r=Math.atan2(n,i);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},ut.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},ut.aliasPixel=function(t){return t%2==0?0:.5},ut._alignPixel=function(t,e,i){var n=t.currentDevicePixelRatio,a=i/2;return Math.round((e-a)*n)/n+a},ut.splineCurve=function(t,e,i,n){var a=t.skip?e:t,r=e,o=i.skip?e:i,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=n*(u=isNaN(u)?0:u),c=n*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},ut.EPSILON=Number.EPSILON||1e-14,ut.splineCurveMonotone=function(t){var e,i,n,a,r,o,s,l,u,d=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),h=d.length;for(e=0;e0?d[e-1]:null,(a=e0?d[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},ut.previousItem=function(t,e,i){return i?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},ut.niceNum=function(t,e){var i=Math.floor(ut.log10(t)),n=t/Math.pow(10,i);return(e?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10)*Math.pow(10,i)},ut.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},ut.getRelativePosition=function(t,e){var i,n,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(i=s[0].clientX,n=s[0].clientY):(i=a.clientX,n=a.clientY);var l=parseFloat(ut.getStyle(r,"padding-left")),u=parseFloat(ut.getStyle(r,"padding-top")),d=parseFloat(ut.getStyle(r,"padding-right")),h=parseFloat(ut.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:i=Math.round((i-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:n=Math.round((n-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},ut.getConstraintWidth=function(t){return i(t,"max-width","clientWidth")},ut.getConstraintHeight=function(t){return i(t,"max-height","clientHeight")},ut._calculatePadding=function(t,e,i){return(e=ut.getStyle(t,e)).indexOf("%")>-1?i*parseInt(e,10)/100:parseInt(e,10)},ut._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},ut.getMaximumWidth=function(t){var e=ut._getParentNode(t);if(!e)return t.clientWidth;var i=e.clientWidth,n=i-ut._calculatePadding(e,"padding-left",i)-ut._calculatePadding(e,"padding-right",i),a=ut.getConstraintWidth(t);return isNaN(a)?n:Math.min(n,a)},ut.getMaximumHeight=function(t){var e=ut._getParentNode(t);if(!e)return t.clientHeight;var i=e.clientHeight,n=i-ut._calculatePadding(e,"padding-top",i)-ut._calculatePadding(e,"padding-bottom",i),a=ut.getConstraintHeight(t);return isNaN(a)?n:Math.min(n,a)},ut.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},ut.retinaScale=function(t,e){var i=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==i){var n=t.canvas,a=t.height,r=t.width;n.height=a*i,n.width=r*i,t.ctx.scale(i,i),n.style.height||n.style.width||(n.style.height=a+"px",n.style.width=r+"px")}},ut.fontString=function(t,e,i){return e+" "+t+"px "+i},ut.longestText=function(t,e,i,n){var a=(n=n||{}).data=n.data||{},r=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(a=n.data={},r=n.garbageCollect=[],n.font=e),t.font=e;var o=0;ut.each(i,function(e){null!=e&&!0!==ut.isArray(e)?o=ut.measureText(t,a,r,o,e):ut.isArray(e)&&ut.each(e,function(e){null==e||ut.isArray(e)||(o=ut.measureText(t,a,r,o,e))})});var s=r.length/2;if(s>i.length){for(var l=0;ln&&(n=r),n},ut.numberOfLabelLines=function(t){var e=1;return ut.each(t,function(t){ut.isArray(t)&&t.length>e&&(e=t.length)}),e},ut.color=G?function(t){return t instanceof CanvasGradient&&(t=ot.global.defaultColor),G(t)}:function(t){return console.error("Color.js not found!"),t},ut.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:ut.color(t).saturate(.5).darken(.1).rgbString()}}(),ai._adapters=si,ai.Animation=pt,ai.animationService=vt,ai.controllers=ue,ai.DatasetController=kt,ai.defaults=ot,ai.Element=gt,ai.elements=Nt,ai.Interaction=pe,ai.layouts=xe,ai.platform=Ve,ai.plugins=He,ai.Scale=fi,ai.scaleService=Ee,ai.Ticks=li,ai.Tooltip=Je,ai.helpers.each(en,function(t,e){ai.scaleService.registerScaleType(e,t,t._defaults)}),kn)kn.hasOwnProperty(Dn)&&ai.plugins.register(kn[Dn]);ai.platform.initialize();var Cn=ai;return"undefined"!=typeof window&&(window.Chart=ai),ai.Chart=ai,ai.Legend=kn.legend._element,ai.Title=kn.title._element,ai.pluginService=ai.plugins,ai.PluginBase=ai.Element.extend({}),ai.canvasHelpers=ai.helpers.canvas,ai.layoutService=ai.layouts,ai.LinearScaleBase=yi,ai.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],function(t){ai[t]=function(e,i){return new ai(e,ai.helpers.merge(i||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}}),Cn}); diff --git a/DNS price tracking/static/chart_js_2.9.3/Chart.bundle.min.js b/DNS price tracking/static/chart_js_2.9.3/Chart.bundle.min.js new file mode 100644 index 000000000..55d9eb03f --- /dev/null +++ b/DNS price tracking/static/chart_js_2.9.3/Chart.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Chart.js v2.9.3 + * https://www.chartjs.org + * (c) 2019 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Chart=e()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},i=e((function(t){var e={};for(var i in n)n.hasOwnProperty(i)&&(e[n[i]]=i);var a=t.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var r in a)if(a.hasOwnProperty(r)){if(!("channels"in a[r]))throw new Error("missing channels property: "+r);if(!("labels"in a[r]))throw new Error("missing channel labels property: "+r);if(a[r].labels.length!==a[r].channels)throw new Error("channel and label counts mismatch: "+r);var o=a[r].channels,s=a[r].labels;delete a[r].channels,delete a[r].labels,Object.defineProperty(a[r],"channels",{value:o}),Object.defineProperty(a[r],"labels",{value:s})}a.rgb.hsl=function(t){var e,n,i=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o;return s===o?e=0:i===s?e=(a-r)/l:a===s?e=2+(r-i)/l:r===s&&(e=4+(i-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),n=(o+s)/2,[e,100*(s===o?0:n<=.5?l/(s+o):l/(2-s-o)),100*n]},a.rgb.hsv=function(t){var e,n,i,a,r,o=t[0]/255,s=t[1]/255,l=t[2]/255,u=Math.max(o,s,l),d=u-Math.min(o,s,l),h=function(t){return(u-t)/6/d+.5};return 0===d?a=r=0:(r=d/u,e=h(o),n=h(s),i=h(l),o===u?a=i-n:s===u?a=1/3+e-i:l===u&&(a=2/3+n-e),a<0?a+=1:a>1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a1&&(e=Array.prototype.slice.call(arguments));var n=t(e);if("object"==typeof n)for(var i=n.length,a=0;a1&&(e=Array.prototype.slice.call(arguments)),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(i)}))}));var l=s,u={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},d={getRgba:h,getHsla:c,getRgb:function(t){var e=h(t);return e&&e.slice(0,3)},getHsl:function(t){var e=c(t);return e&&e.slice(0,3)},getHwb:f,getAlpha:function(t){var e=h(t);if(e)return e[3];if(e=c(t))return e[3];if(e=f(t))return e[3]},hexString:function(t,e){e=void 0!==e&&3===t.length?e:t[3];return"#"+b(t[0])+b(t[1])+b(t[2])+(e>=0&&e<1?b(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return g(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:g,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return m(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"},percentaString:m,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return p(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:p,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return y[t.slice(0,3)]}};function h(t){if(t){var e=[0,0,0],n=1,i=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(i){a=(i=i[1])[3];for(var r=0;rn?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=t,i=void 0===e?.5:e,a=2*i-1,r=this.alpha()-n.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*n.red(),o*this.green()+s*n.green(),o*this.blue()+s*n.blue()).alpha(this.alpha()*i+n.alpha()*(1-i))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new _,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},_.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},_.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},_.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i=0;a--)e.call(n,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),-i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n))},easeOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),i*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/n)+1)},easeInOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:2==(t/=.5)?1:(n||(n=.45),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),t<1?i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*-.5:i*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-D.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*D.easeInBounce(2*t):.5*D.easeOutBounce(2*t-1)+.5}},C={effects:D};S.easingEffects=D;var P=Math.PI,T=P/180,O=2*P,A=P/2,F=P/4,I=2*P/3,L={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,n,i,a,r){if(r){var o=Math.min(r,a/2,i/2),s=e+o,l=n+o,u=e+i-o,d=n+a-o;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,n,i,a=this.animations,r=0;r=n?(H.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},Q=H.options.resolve,tt=["push","pop","shift","splice","unshift"];function et(t,e){var n=t._chartjs;if(n){var i=n.listeners,a=i.indexOf(e);-1!==a&&i.splice(a,1),i.length>0||(tt.forEach((function(e){delete t[e]})),delete t._chartjs)}}var nt=function(t,e){this.initialize(t,e)};H.extend(nt.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements(),n._type=n.getMeta().type},updateIndex:function(t){this.index=t},linkScales:function(){var t=this.getMeta(),e=this.chart,n=e.scales,i=this.getDataset(),a=e.options.scales;null!==t.xAxisID&&t.xAxisID in n&&!i.xAxisID||(t.xAxisID=i.xAxisID||a.xAxes[0].id),null!==t.yAxisID&&t.yAxisID in n&&!i.yAxisID||(t.yAxisID=i.yAxisID||a.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&et(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,n=this.getMeta(),i=this.getDataset().data||[],a=n.data;for(t=0,e=i.length;tn&&this.insertElements(n,i-n)},insertElements:function(t,e){for(var n=0;na?(r=a/e.innerRadius,t.arc(o,s,e.innerRadius-a,i+r,n-r,!0)):t.arc(o,s,a,i+Math.PI/2,n-Math.PI/2),t.closePath(),t.clip()}function ot(t,e,n){var i="inner"===e.borderAlign;i?(t.lineWidth=2*e.borderWidth,t.lineJoin="round"):(t.lineWidth=e.borderWidth,t.lineJoin="bevel"),n.fullCircles&&function(t,e,n,i){var a,r=n.endAngle;for(i&&(n.endAngle=n.startAngle+at,rt(t,n),n.endAngle=r,n.endAngle===n.startAngle&&n.fullCircles&&(n.endAngle+=at,n.fullCircles--)),t.beginPath(),t.arc(n.x,n.y,n.innerRadius,n.startAngle+at,n.startAngle,!0),a=0;as;)a-=at;for(;a=o&&a<=s,u=r>=n.innerRadius&&r<=n.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t,e=this._chart.ctx,n=this._view,i="inner"===n.borderAlign?.33:0,a={x:n.x,y:n.y,innerRadius:n.innerRadius,outerRadius:Math.max(n.outerRadius-i,0),pixelMargin:i,startAngle:n.startAngle,endAngle:n.endAngle,fullCircles:Math.floor(n.circumference/at)};if(e.save(),e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+at,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),t=0;tt.x&&(e=bt(e,"left","right")):t.basen?n:i,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>n?n:r,l:l.left||o<0?0:o>e?e:o}}function xt(t,e,n){var i=null===e,a=null===n,r=!(!t||i&&a)&&vt(t);return r&&(i||e>=r.left&&e<=r.right)&&(a||n>=r.top&&n<=r.bottom)}W._set("global",{elements:{rectangle:{backgroundColor:mt,borderColor:mt,borderSkipped:"bottom",borderWidth:0}}});var _t=$.extend({_type:"rectangle",draw:function(){var t=this._chart.ctx,e=this._view,n=function(t){var e=vt(t),n=e.right-e.left,i=e.bottom-e.top,a=yt(t,n/2,i/2);return{outer:{x:e.left,y:e.top,w:n,h:i},inner:{x:e.left+a.l,y:e.top+a.t,w:n-a.l-a.r,h:i-a.t-a.b}}}(e),i=n.outer,a=n.inner;t.fillStyle=e.backgroundColor,t.fillRect(i.x,i.y,i.w,i.h),i.w===a.w&&i.h===a.h||(t.save(),t.beginPath(),t.rect(i.x,i.y,i.w,i.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return xt(this._view,t,e)},inLabelRange:function(t,e){var n=this._view;return pt(n)?xt(n,t,null):xt(n,null,e)},inXRange:function(t){return xt(this._view,t,null)},inYRange:function(t){return xt(this._view,null,t)},getCenterPoint:function(){var t,e,n=this._view;return pt(n)?(t=n.x,e=(n.y+n.base)/2):(t=(n.x+n.base)/2,e=n.y),{x:t,y:e}},getArea:function(){var t=this._view;return pt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),wt={},kt=st,Mt=dt,St=gt,Dt=_t;wt.Arc=kt,wt.Line=Mt,wt.Point=St,wt.Rectangle=Dt;var Ct=H._deprecated,Pt=H.valueOrDefault;function Tt(t,e,n){var i,a,r=n.barThickness,o=e.stackCount,s=e.pixels[t],l=H.isNullOrUndef(r)?function(t,e){var n,i,a,r,o=t._length;for(a=1,r=e.length;a0?Math.min(o,Math.abs(i-n)):o,n=i;return o}(e.scale,e.pixels):-1;return H.isNullOrUndef(r)?(i=l*n.categoryPercentage,a=n.barPercentage):(i=r*o,a=1),{chunk:i/o,ratio:a,start:s-i/2}}W._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),W._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var Ot=it.extend({dataElementType:wt.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,e,n=this;it.prototype.initialize.apply(n,arguments),(t=n.getMeta()).stack=n.getDataset().stack,t.bar=!0,e=n._getIndexScale().options,Ct("bar chart",e.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Ct("bar chart",e.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Ct("bar chart",e.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Ct("bar chart",n._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Ct("bar chart",e.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(t){var e,n,i=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,n=i.length;e=0&&m.min>=0?m.min:m.max,x=void 0===m.start?m.end:m.max>=0&&m.min>=0?m.max-m.min:m.min-m.max,_=g.length;if(v||void 0===v&&void 0!==b)for(i=0;i<_&&(a=g[i]).index!==t;++i)a.stack===b&&(r=void 0===(u=h._parseValue(f[a.index].data[e])).start?u.end:u.min>=0&&u.max>=0?u.max:u.min,(m.min<0&&r<0||m.max>=0&&r>0)&&(y+=r));return o=h.getPixelForValue(y),l=(s=h.getPixelForValue(y+x))-o,void 0!==p&&Math.abs(l)=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t=Rt?-Nt:b<-Rt?Nt:0)+p,x=Math.cos(b),_=Math.sin(b),w=Math.cos(y),k=Math.sin(y),M=b<=0&&y>=0||y>=Nt,S=b<=Wt&&y>=Wt||y>=Nt+Wt,D=b<=-Wt&&y>=-Wt||y>=Rt+Wt,C=b===-Rt||y>=Rt?-1:Math.min(x,x*m,w,w*m),P=D?-1:Math.min(_,_*m,k,k*m),T=M?1:Math.max(x,x*m,w,w*m),O=S?1:Math.max(_,_*m,k,k*m);u=(T-C)/2,d=(O-P)/2,h=-(T+C)/2,c=-(O+P)/2}for(i=0,a=g.length;i0&&!isNaN(t)?Nt*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,n,i,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,n=d.data.datasets.length;e(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Lt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Lt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Lt(n.hoverBorderWidth,n.borderWidth)},_getRingWeightOffset:function(t){for(var e=0,n=0;n0&&Ht(l[t-1]._model,s)&&(n.controlPointPreviousX=u(n.controlPointPreviousX,s.left,s.right),n.controlPointPreviousY=u(n.controlPointPreviousY,s.top,s.bottom)),t0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return ae(t,e,{intersect:!1})},point:function(t,e){return ee(t,Qt(e,t))},nearest:function(t,e,n){var i=Qt(e,t);n.axis=n.axis||"xy";var a=ie(n.axis);return ne(t,i,n.intersect,a)},x:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inXRange(i.x)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a},y:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inYRange(i.y)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a}}},oe=H.extend;function se(t,e){return H.where(t,(function(t){return t.pos===e}))}function le(t,e){return t.sort((function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i.index-a.index:i.weight-a.weight}))}function ue(t,e,n,i){return Math.max(t[n],e[n])+Math.max(t[i],e[i])}function de(t,e,n){var i,a,r=n.box,o=t.maxPadding;if(n.size&&(t[n.pos]-=n.size),n.size=n.horizontal?r.height:r.width,t[n.pos]+=n.size,r.getPadding){var s=r.getPadding();o.top=Math.max(o.top,s.top),o.left=Math.max(o.left,s.left),o.bottom=Math.max(o.bottom,s.bottom),o.right=Math.max(o.right,s.right)}if(i=e.outerWidth-ue(o,t,"left","right"),a=e.outerHeight-ue(o,t,"top","bottom"),i!==t.w||a!==t.h)return t.w=i,t.h=a,n.horizontal?i!==t.w:a!==t.h}function he(t,e){var n=e.maxPadding;function i(t){var i={left:0,top:0,right:0,bottom:0};return t.forEach((function(t){i[t]=Math.max(e[t],n[t])})),i}return i(t?["left","right"]:["top","bottom"])}function ce(t,e,n){var i,a,r,o,s,l,u=[];for(i=0,a=t.length;idiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ge.default||ge,ve="$chartjs",be="chartjs-size-monitor",ye="chartjs-render-monitor",xe="chartjs-render-animation",_e=["animationstart","webkitAnimationStart"],we={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function ke(t,e){var n=H.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}var Me=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Se(t,e,n){t.addEventListener(e,n,Me)}function De(t,e,n){t.removeEventListener(e,n,Me)}function Ce(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function Pe(t){var e=document.createElement("div");return e.className=t||"",e}function Te(t,e,n){var i,a,r,o,s=t[ve]||(t[ve]={}),l=s.resizer=function(t){var e=Pe(be),n=Pe(be+"-expand"),i=Pe(be+"-shrink");n.appendChild(Pe()),i.appendChild(Pe()),e.appendChild(n),e.appendChild(i),e._reset=function(){n.scrollLeft=1e6,n.scrollTop=1e6,i.scrollLeft=1e6,i.scrollTop=1e6};var a=function(){e._reset(),t()};return Se(n,"scroll",a.bind(n,"expand")),Se(i,"scroll",a.bind(i,"shrink")),e}((i=function(){if(s.resizer){var i=n.options.maintainAspectRatio&&t.parentNode,a=i?i.clientWidth:0;e(Ce("resize",n)),i&&i.clientWidth0){var r=t[0];r.label?n=r.label:r.xLabel?n=r.xLabel:a>0&&r.index-1?t.split("\n"):t}function Ve(t){var e=W.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,rtl:t.rtl,textDirection:t.textDirection,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Ne(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Ne(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Ne(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Ne(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Ne(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Ne(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Ne(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Ne(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Ne(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function He(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function Be(t){return ze([],Ee(t))}var je=$.extend({initialize:function(){this._model=Ve(this._options),this._lastActive=[]},getTitle:function(){var t=this,e=t._options,n=e.callbacks,i=n.beforeTitle.apply(t,arguments),a=n.title.apply(t,arguments),r=n.afterTitle.apply(t,arguments),o=[];return o=ze(o,Ee(i)),o=ze(o,Ee(a)),o=ze(o,Ee(r))},getBeforeBody:function(){return Be(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var n=this,i=n._options.callbacks,a=[];return H.each(t,(function(t){var r={before:[],lines:[],after:[]};ze(r.before,Ee(i.beforeLabel.call(n,t,e))),ze(r.lines,i.label.call(n,t,e)),ze(r.after,Ee(i.afterLabel.call(n,t,e))),a.push(r)})),a},getAfterBody:function(){return Be(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this,e=t._options.callbacks,n=e.beforeFooter.apply(t,arguments),i=e.footer.apply(t,arguments),a=e.afterFooter.apply(t,arguments),r=[];return r=ze(r,Ee(n)),r=ze(r,Ee(i)),r=ze(r,Ee(a))},update:function(t){var e,n,i,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=Ve(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},y={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],w=[];x=Ye[c.position].call(h,m,h._eventPosition);var k=[];for(e=0,n=m.length;ei.width&&(a=i.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,y,v=function(t,e){var n,i,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(n=function(t){return t<=c},i=function(t){return t>c}):(n=function(t){return t<=e.width/2},i=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},n(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):i(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,y),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=y.width,g.height=y.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var n=this._chart.ctx,i=this._view,a=this.getCaretPosition(t,e,i);n.lineTo(a.x1,a.y1),n.lineTo(a.x2,a.y2),n.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,n){var i,a,r,o,s,l,u=n.caretSize,d=n.cornerRadius,h=n.xAlign,c=n.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(i=f)-u,r=i,o=s+u,l=s-u):(a=(i=f+m)+u,r=i,o=s-u,l=s+u);else if("left"===h?(i=(a=f+d+u)-u,r=a+u):"right"===h?(i=(a=f+m-d-u)-u,r=a+u):(i=(a=n.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=i,i=v}return{x1:i,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,n){var i,a,r,o=e.title,s=o.length;if(s){var l=We(e.rtl,e.x,e.width);for(t.x=He(e,e._titleAlign),n.textAlign=l.textAlign(e._titleAlign),n.textBaseline="middle",i=e.titleFontSize,a=e.titleSpacing,n.fillStyle=e.titleFontColor,n.font=H.fontString(i,e._titleFontStyle,e._titleFontFamily),r=0;r0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,H.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),H.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!H.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ue=Ye,Ge=je;Ge.positioners=Ue;var qe=H.valueOrDefault;function Ze(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?H.merge(e[t][a],[Re.getScaleDefaults(r),o]):H.merge(e[t][a],o)}else H._merger(t,e,n,i)}})}function $e(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||{},r=n[t];"scales"===t?e[t]=Ze(a,r):"scale"===t?e[t]=H.merge(a,[Re.getScaleDefaults(r.type),r]):H._merger(t,e,n,i)}})}function Xe(t){var e=t.options;H.each(t.scales,(function(e){me.removeBox(t,e)})),e=$e(W.global,W[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Ke(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(H.findIndex(t,a)>=0);return i}function Je(t){return"top"===t||"bottom"===t}function Qe(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}W._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var tn=function(t,e){return this.construct(t,e),this};H.extend(tn.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=$e(W.global,W[t.type],t.options||{}),t}(e);var i=Ie.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=H.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,tn.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Le.notify(t,"beforeInit"),H.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Le.notify(t,"afterInit"),t},clear:function(){return H.canvas.clear(this),this},stop:function(){return J.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(H.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:H.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",H.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Le.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;H.each(e.xAxes,(function(t,n){t.id||(t.id=Ke(e.xAxes,"x-axis-",n))})),H.each(e.yAxes,(function(t,n){t.id||(t.id=Ke(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),H.each(i,(function(e){var i=e.options,r=i.id,o=qe(i.type,e.dtype);Je(i.position)!==Je(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Re.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),H.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Re.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t=0;--n)this.drawDataset(e[n],t);Le.notify(this,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n={meta:t,index:t.index,easingValue:e};!1!==Le.notify(this,"beforeDatasetDraw",[n])&&(t.controller.draw(e),Le.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,n={tooltip:e,easingValue:t};!1!==Le.notify(this,"beforeTooltipDraw",[n])&&(e.draw(),Le.notify(this,"afterTooltipDraw",[n]))},getElementAtEvent:function(t){return re.modes.single(this,t)},getElementsAtEvent:function(t){return re.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return re.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,n){var i=re.modes[e];return"function"==typeof i?i(this,t,n):[]},getDatasetAtEvent:function(t){return re.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var n=e._meta[this.id];return n||(n=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e.order||0,index:t}),n},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e3?n[2]-n[1]:n[1]-n[0];Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));var a=H.log10(Math.abs(i)),r="";if(0!==t)if(Math.max(Math.abs(n[0]),Math.abs(n[n.length-1]))<1e-4){var o=H.log10(Math.abs(t)),s=Math.floor(o)-Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toExponential(s)}else{var l=-1*Math.floor(a);l=Math.max(Math.min(l,20),0),r=t.toFixed(l)}else r="0";return r},logarithmic:function(t,e,n){var i=t/Math.pow(10,Math.floor(H.log10(t)));return 0===t?"0":1===i||2===i||5===i||0===e||e===n.length-1?t.toExponential():""}}},sn=H.isArray,ln=H.isNullOrUndef,un=H.valueOrDefault,dn=H.valueAtIndexOrDefault;function hn(t,e,n){var i,a=t.getTicks().length,r=Math.min(e,a-1),o=t.getPixelForTick(r),s=t._startPixel,l=t._endPixel;if(!(n&&(i=1===a?Math.max(o-s,l-o):0===e?(t.getPixelForTick(1)-o)/2:(o-t.getPixelForTick(r-1))/2,(o+=rl+1e-6)))return o}function cn(t,e,n,i){var a,r,o,s,l,u,d,h,c,f,g,m,p,v=n.length,b=[],y=[],x=[];for(a=0;ae){for(n=0;n=c||d<=1||!s.isHorizontal()?s.labelRotation=h:(e=(t=s._getLabelSizes()).widest.width,n=t.highest.height-t.highest.offset,i=Math.min(s.maxWidth,s.chart.width-e),e+6>(a=l.offset?s.maxWidth/d:i/(d-1))&&(a=i/(d-(l.offset?.5:1)),r=s.maxHeight-fn(l.gridLines)-u.padding-gn(l.scaleLabel),o=Math.sqrt(e*e+n*n),f=H.toDegrees(Math.min(Math.asin(Math.min((t.highest.height+6)/a,1)),Math.asin(Math.min(r/o,1))-Math.asin(n/o))),f=Math.max(h,Math.min(c,f))),s.labelRotation=f)},afterCalculateTickRotation:function(){H.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){H.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},n=t.chart,i=t.options,a=i.ticks,r=i.scaleLabel,o=i.gridLines,s=t._isVisible(),l="bottom"===i.position,u=t.isHorizontal();if(u?e.width=t.maxWidth:s&&(e.width=fn(o)+gn(r)),u?s&&(e.height=fn(o)+gn(r)):e.height=t.maxHeight,a.display&&s){var d=pn(a),h=t._getLabelSizes(),c=h.first,f=h.last,g=h.widest,m=h.highest,p=.4*d.minor.lineHeight,v=a.padding;if(u){var b=0!==t.labelRotation,y=H.toRadians(t.labelRotation),x=Math.cos(y),_=Math.sin(y),w=_*g.width+x*(m.height-(b?m.offset:0))+(b?0:p);e.height=Math.min(t.maxHeight,e.height+w+v);var k,M,S=t.getPixelForTick(0)-t.left,D=t.right-t.getPixelForTick(t.getTicks().length-1);b?(k=l?x*c.width+_*c.offset:_*(c.height-c.offset),M=l?_*(f.height-f.offset):x*f.width+_*f.offset):(k=c.width/2,M=f.width/2),t.paddingLeft=Math.max((k-S)*t.width/(t.width-S),0)+3,t.paddingRight=Math.max((M-D)*t.width/(t.width-D),0)+3}else{var C=a.mirror?0:g.width+v+p;e.width=Math.min(t.maxWidth,e.width+C),t.paddingTop=c.height/2,t.paddingBottom=f.height/2}}t.handleMargins(),u?(t.width=t._length=n.width-t.margins.left-t.margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=n.height-t.margins.top-t.margins.bottom)},handleMargins:function(){var t=this;t.margins&&(t.margins.left=Math.max(t.paddingLeft,t.margins.left),t.margins.top=Math.max(t.paddingTop,t.margins.top),t.margins.right=Math.max(t.paddingRight,t.margins.right),t.margins.bottom=Math.max(t.paddingBottom,t.margins.bottom))},afterFit:function(){H.callback(this.options.afterFit,[this])},isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ln(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},_convertTicksToLabels:function(t){var e,n,i,a=this;for(a.ticks=t.map((function(t){return t.value})),a.beforeTickToLabelConversion(),e=a.convertTicksToLabels(t)||a.ticks,a.afterTickToLabelConversion(),n=0,i=t.length;nn-1?null:this.getPixelForDecimal(t*i+(e?i/2:0))},getPixelForDecimal:function(t){return this._reversePixels&&(t=1-t),this._startPixel+t*this._length},getDecimalForPixel:function(t){var e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,n,i,a,r=this.options.ticks,o=this._length,s=r.maxTicksLimit||o/this._tickSize()+1,l=r.major.enabled?function(t){var e,n,i=[];for(e=0,n=t.length;es)return function(t,e,n){var i,a,r=0,o=e[0];for(n=Math.ceil(n),i=0;iu)return r;return Math.max(u,1)}(l,t,0,s),u>0){for(e=0,n=u-1;e1?(h-d)/(u-1):null,bn(t,i,H.isNullOrUndef(a)?0:d-a,d),bn(t,i,h,H.isNullOrUndef(a)?t.length:h+a),vn(t)}return bn(t,i),vn(t)},_tickSize:function(){var t=this.options.ticks,e=H.toRadians(this.labelRotation),n=Math.abs(Math.cos(e)),i=Math.abs(Math.sin(e)),a=this._getLabelSizes(),r=t.autoSkipPadding||0,o=a?a.widest.width+r:0,s=a?a.highest.height+r:0;return this.isHorizontal()?s*n>o*i?o/n:s/i:s*i=0&&(o=t),void 0!==r&&(t=n.indexOf(r))>=0&&(s=t),e.minIndex=o,e.maxIndex=s,e.min=n[o],e.max=n[s]},buildTicks:function(){var t=this._getLabels(),e=this.minIndex,n=this.maxIndex;this.ticks=0===e&&n===t.length-1?t:t.slice(e,n+1)},getLabelForIndex:function(t,e){var n=this.chart;return n.getDatasetMeta(e).controller._getValueScaleId()===this.id?this.getRightValue(n.data.datasets[e].data[t]):this._getLabels()[t]},_configure:function(){var t=this,e=t.options.offset,n=t.ticks;xn.prototype._configure.call(t),t.isHorizontal()||(t._reversePixels=!t._reversePixels),n&&(t._startValue=t.minIndex-(e?.5:0),t._valueRange=Math.max(n.length-(e?0:1),1))},getPixelForValue:function(t,e,n){var i,a,r,o=this;return _n(e)||_n(n)||(t=o.chart.data.datasets[n].data[e]),_n(t)||(i=o.isHorizontal()?t.x:t.y),(void 0!==i||void 0!==t&&isNaN(e))&&(a=o._getLabels(),t=H.valueOrDefault(i,t),e=-1!==(r=a.indexOf(t))?r:e,isNaN(e)&&(e=t)),o.getPixelForDecimal((e-o._startValue)/o._valueRange)},getPixelForTick:function(t){var e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t],t+this.minIndex)},getValueForPixel:function(t){var e=Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange);return Math.min(Math.max(e,0),this.ticks.length-1)},getBasePixel:function(){return this.bottom}}),kn={position:"bottom"};wn._defaults=kn;var Mn=H.noop,Sn=H.isNullOrUndef;var Dn=xn.extend({getRightValue:function(t){return"string"==typeof t?+t:xn.prototype.getRightValue.call(this,t)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var n=H.sign(t.min),i=H.sign(t.max);n<0&&i<0?t.max=0:n>0&&i>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,n=e.stepSize,i=e.maxTicksLimit;return n?t=Math.ceil(this.max/n)-Math.floor(this.min/n)+1:(t=this._computeTickLimit(),i=i||11),i&&(t=Math.min(i,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Mn,buildTicks:function(){var t=this,e=t.options.ticks,n=t.getTickLimit(),i={maxTicks:n=Math.max(2,n),min:e.min,max:e.max,precision:e.precision,stepSize:H.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var n,i,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=H.niceNum((g-f)/u/l)*l;if(m<1e-14&&Sn(d)&&Sn(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=H.niceNum(r*m/u/l)*l),s||Sn(c)?n=Math.pow(10,H._decimalPlaces(m)):(n=Math.pow(10,c),m=Math.ceil(m*n)/n),i=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!Sn(d)&&H.almostWhole(d/m,m/1e3)&&(i=d),!Sn(h)&&H.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-i)/m,r=H.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),i=Math.round(i*n)/n,a=Math.round(a*n)/n,o.push(Sn(d)?i:d);for(var p=1;pe.length-1?null:this.getPixelForValue(e[t])}}),An=Cn;On._defaults=An;var Fn=H.valueOrDefault,In=H.math.log10;var Ln={position:"left",ticks:{callback:on.formatters.logarithmic}};function Rn(t,e){return H.isFinite(t)&&t>=0?t:e}var Nn=xn.extend({determineDataLimits:function(){var t,e,n,i,a,r,o=this,s=o.options,l=o.chart,u=l.data.datasets,d=o.isHorizontal();function h(t){return d?t.xAxisID===o.id:t.yAxisID===o.id}o.min=Number.POSITIVE_INFINITY,o.max=Number.NEGATIVE_INFINITY,o.minNotZero=Number.POSITIVE_INFINITY;var c=s.stacked;if(void 0===c)for(t=0;t0){var e=H.min(t),n=H.max(t);o.min=Math.min(o.min,e),o.max=Math.max(o.max,n)}}))}else for(t=0;t0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(In(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,n=!t.isHorizontal(),i={min:Rn(e.min),max:Rn(e.max)},a=t.ticks=function(t,e){var n,i,a=[],r=Fn(t.min,Math.pow(10,Math.floor(In(e.min)))),o=Math.floor(In(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(n=Math.floor(In(e.minNotZero)),i=Math.floor(e.minNotZero/Math.pow(10,n)),a.push(r),r=i*Math.pow(10,n)):(n=Math.floor(In(r)),i=Math.floor(r/Math.pow(10,n)));var l=n<0?Math.pow(10,Math.abs(n)):1;do{a.push(r),10===++i&&(i=1,l=++n>=0?1:l),r=Math.round(i*Math.pow(10,n)*l)/l}while(ne.length-1?null:this.getPixelForValue(e[t])},_getFirstTickValue:function(t){var e=Math.floor(In(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},_configure:function(){var t=this,e=t.min,n=0;xn.prototype._configure.call(t),0===e&&(e=t._getFirstTickValue(t.minNotZero),n=Fn(t.options.ticks.fontSize,W.global.defaultFontSize)/t._length),t._startValue=In(e),t._valueOffset=n,t._valueRange=(In(t.max)-In(e))/(1-n)},getPixelForValue:function(t){var e=this,n=0;return(t=+e.getRightValue(t))>e.min&&t>0&&(n=(In(t)-e._startValue)/e._valueRange+e._valueOffset),e.getPixelForDecimal(n)},getValueForPixel:function(t){var e=this,n=e.getDecimalForPixel(t);return 0===n&&0===e.min?0:Math.pow(10,e._startValue+(n-e._valueOffset)*e._valueRange)}}),Wn=Ln;Nn._defaults=Wn;var Yn=H.valueOrDefault,zn=H.valueAtIndexOrDefault,En=H.options.resolve,Vn={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:on.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function Hn(t){var e=t.ticks;return e.display&&t.display?Yn(e.fontSize,W.global.defaultFontSize)+2*e.backdropPaddingY:0}function Bn(t,e,n,i,a){return t===i||t===a?{start:e-n/2,end:e+n/2}:ta?{start:e-n,end:e}:{start:e,end:e+n}}function jn(t){return 0===t||180===t?"center":t<180?"left":"right"}function Un(t,e,n,i){var a,r,o=n.y+i/2;if(H.isArray(e))for(a=0,r=e.length;a270||t<90)&&(n.y-=e.h)}function qn(t){return H.isNumber(t)?t:0}var Zn=Dn.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Hn(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,n=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;H.each(e.data.datasets,(function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);H.each(a.data,(function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(n=Math.min(r,n),i=Math.max(r,i))}))}})),t.min=n===Number.POSITIVE_INFINITY?0:n,t.max=i===Number.NEGATIVE_INFINITY?0:i,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Hn(this.options))},convertTicksToLabels:function(){var t=this;Dn.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map((function(){var e=H.callback(t.options.pointLabels.callback,arguments,t);return e||0===e?e:""}))},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,n,i,a=H.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=t.chart.data.labels.length;for(e=0;er.r&&(r.r=f.end,o.r=h),g.startr.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,n){var i=this,a=e.l/Math.sin(n.l),r=Math.max(e.r-i.width,0)/Math.sin(n.r),o=-e.t/Math.cos(n.t),s=-Math.max(e.b-(i.height-i.paddingTop),0)/Math.cos(n.b);a=qn(a),r=qn(r),o=qn(o),s=qn(s),i.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),i.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,n,i){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=n+a.drawingArea,l=a.height-a.paddingTop-i-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){var e=this.chart,n=(t*(360/e.data.labels.length)+((e.options||{}).startAngle||0))%360;return(n<0?n+360:n)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(H.isNullOrUndef(t))return NaN;var n=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*n:(t-e.min)*n},getPointPosition:function(t,e){var n=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(n)*e+this.xCenter,y:Math.sin(n)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(t){var e=this.min,n=this.max;return this.getPointPositionForValue(t||0,this.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0)},_drawGrid:function(){var t,e,n,i=this,a=i.ctx,r=i.options,o=r.gridLines,s=r.angleLines,l=Yn(s.lineWidth,o.lineWidth),u=Yn(s.color,o.color);if(r.pointLabels.display&&function(t){var e=t.ctx,n=t.options,i=n.pointLabels,a=Hn(n),r=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),o=H.options._parseFont(i);e.save(),e.font=o.string,e.textBaseline="middle";for(var s=t.chart.data.labels.length-1;s>=0;s--){var l=0===s?a/2:0,u=t.getPointPosition(s,r+l+5),d=zn(i.fontColor,s,W.global.defaultFontColor);e.fillStyle=d;var h=t.getIndexAngle(s),c=H.toDegrees(h);e.textAlign=jn(c),Gn(c,t._pointLabelSizes[s],u),Un(e,t.pointLabels[s],u,o.lineHeight)}e.restore()}(i),o.display&&H.each(i.ticks,(function(t,n){0!==n&&(e=i.getDistanceFromCenterForValue(i.ticksAsNumbers[n]),function(t,e,n,i){var a,r=t.ctx,o=e.circular,s=t.chart.data.labels.length,l=zn(e.color,i-1),u=zn(e.lineWidth,i-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,n,0,2*Math.PI);else{a=t.getPointPosition(0,n),r.moveTo(a.x,a.y);for(var d=1;d=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=H.options._parseFont(n),s=Yn(n.fontColor,W.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",H.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:H.noop}),$n=Vn;Zn._defaults=$n;var Xn=H._deprecated,Kn=H.options.resolve,Jn=H.valueOrDefault,Qn=Number.MIN_SAFE_INTEGER||-9007199254740991,ti=Number.MAX_SAFE_INTEGER||9007199254740991,ei={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ni=Object.keys(ei);function ii(t,e){return t-e}function ai(t){return H.valueOrDefault(t.time.min,t.ticks.min)}function ri(t){return H.valueOrDefault(t.time.max,t.ticks.max)}function oi(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]n))return{lo:a,hi:r};s=i-1}}return{lo:r,hi:null}}(t,e,n),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(n-r[e])/s:0,u=(o[i]-r[i])*l;return r[i]+u}function si(t,e){var n=t._adapter,i=t.options.time,a=i.parser,r=a||i.format,o=e;return"function"==typeof a&&(o=a(o)),H.isFinite(o)||(o="string"==typeof r?n.parse(o,r):n.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),H.isFinite(o)||(o=n.parse(o))),o)}function li(t,e){if(H.isNullOrUndef(e))return null;var n=t.options.time,i=si(t,t.getRightValue(e));return null===i?i:(n.round&&(i=+t._adapter.startOf(i,n.round)),i)}function ui(t,e,n,i){var a,r,o,s=ni.length;for(a=ni.indexOf(t);a=0&&(e[r].major=!0);return e}(t,r,o,n):r}var hi=xn.extend({initialize:function(){this.mergeTicksOptions(),xn.prototype.initialize.call(this)},update:function(){var t=this,e=t.options,n=e.time||(e.time={}),i=t._adapter=new rn._date(e.adapters.date);return Xn("time scale",n.format,"time.format","time.parser"),Xn("time scale",n.min,"time.min","ticks.min"),Xn("time scale",n.max,"time.max","ticks.max"),H.mergeIf(n.displayFormats,i.formats()),xn.prototype.update.apply(t,arguments)},getRightValue:function(t){return t&&void 0!==t.t&&(t=t.t),xn.prototype.getRightValue.call(this,t)},determineDataLimits:function(){var t,e,n,i,a,r,o,s=this,l=s.chart,u=s._adapter,d=s.options,h=d.time.unit||"day",c=ti,f=Qn,g=[],m=[],p=[],v=s._getLabels();for(t=0,n=v.length;t1?function(t){var e,n,i,a={},r=[];for(e=0,n=t.length;e1e5*u)throw e+" and "+n+" are too far apart with stepSize of "+u+" "+l;for(a=h;a=a&&n<=r&&d.push(n);return i.min=a,i.max=r,i._unit=l.unit||(s.autoSkip?ui(l.minUnit,i.min,i.max,h):function(t,e,n,i,a){var r,o;for(r=ni.length-1;r>=ni.indexOf(n);r--)if(o=ni[r],ei[o].common&&t._adapter.diff(a,i,o)>=e-1)return o;return ni[n?ni.indexOf(n):0]}(i,d.length,l.minUnit,i.min,i.max)),i._majorUnit=s.major.enabled&&"year"!==i._unit?function(t){for(var e=ni.indexOf(t)+1,n=ni.length;ee&&s=0&&t0?s:1}}),ci={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};hi._defaults=ci;var fi={category:wn,linear:On,logarithmic:Nn,radialLinear:Zn,time:hi},gi=e((function(e,n){e.exports=function(){var n,i;function a(){return n.apply(null,arguments)}function r(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function o(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function s(t){return void 0===t}function l(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function u(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function d(t,e){var n,i=[];for(n=0;n>>0,i=0;i0)for(n=0;n=0?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}var E=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},B={};function j(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(B[t]=a),e&&(B[e[0]]=function(){return z(a.apply(this,arguments),e[1],e[2])}),n&&(B[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function U(t,e){return t.isValid()?(e=G(e,t.localeData()),H[e]=H[e]||function(t){var e,n,i,a=t.match(E);for(e=0,n=a.length;e=0&&V.test(t);)t=t.replace(V,i),V.lastIndex=0,n-=1;return t}var q=/\d/,Z=/\d\d/,$=/\d{3}/,X=/\d{4}/,K=/[+-]?\d{6}/,J=/\d\d?/,Q=/\d\d\d\d?/,tt=/\d\d\d\d\d\d?/,et=/\d{1,3}/,nt=/\d{1,4}/,it=/[+-]?\d{1,6}/,at=/\d+/,rt=/[+-]?\d+/,ot=/Z|[+-]\d\d:?\d\d/gi,st=/Z|[+-]\d\d(?::?\d\d)?/gi,lt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,ut={};function dt(t,e,n){ut[t]=O(e)?e:function(t,i){return t&&n?n:e}}function ht(t,e){return h(ut,t)?ut[t](e._strict,e._locale):new RegExp(ct(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(t,e,n,i,a){return e||n||i||a}))))}function ct(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ft={};function gt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n68?1900:2e3)};var Pt,Tt=Ot("FullYear",!0);function Ot(t,e){return function(n){return null!=n?(Ft(this,t,n),a.updateOffset(this,e),this):At(this,t)}}function At(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function Ft(t,e,n){t.isValid()&&!isNaN(n)&&("FullYear"===e&&Ct(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](n,t.month(),It(n,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](n))}function It(t,e){if(isNaN(t)||isNaN(e))return NaN;var n=function(t,e){return(t%e+e)%e}(e,12);return t+=(e-n)/12,1===n?Ct(t)?29:28:31-n%7%2}Pt=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=0?(s=new Date(t+400,e,n,i,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,n,i,a,r,o),s}function jt(t){var e;if(t<100&&t>=0){var n=Array.prototype.slice.call(arguments);n[0]=t+400,e=new Date(Date.UTC.apply(null,n)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Ut(t,e,n){var i=7+e-n;return-(7+jt(t,0,i).getUTCDay()-e)%7+i-1}function Gt(t,e,n,i,a){var r,o,s=1+7*(e-1)+(7+n-i)%7+Ut(t,i,a);return s<=0?o=Dt(r=t-1)+s:s>Dt(t)?(r=t+1,o=s-Dt(t)):(r=t,o=s),{year:r,dayOfYear:o}}function qt(t,e,n){var i,a,r=Ut(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?i=o+Zt(a=t.year()-1,e,n):o>Zt(t.year(),e,n)?(i=o-Zt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Zt(t,e,n){var i=Ut(t,e,n),a=Ut(t+1,e,n);return(Dt(t)-i+a)/7}function $t(t,e){return t.slice(e,7).concat(t.slice(0,e))}j("w",["ww",2],"wo","week"),j("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),dt("w",J),dt("ww",J,Z),dt("W",J),dt("WW",J,Z),mt(["w","ww","W","WW"],(function(t,e,n,i){e[i.substr(0,1)]=k(t)})),j("d",0,"do","day"),j("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),j("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),j("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),j("e",0,0,"weekday"),j("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),dt("d",J),dt("e",J),dt("E",J),dt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),dt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),dt("dddd",(function(t,e){return e.weekdaysRegex(t)})),mt(["dd","ddd","dddd"],(function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t})),mt(["d","e","E"],(function(t,e,n,i){e[i]=k(t)}));var Xt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Kt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Qt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null}var te=lt,ee=lt,ne=lt;function ie(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ct(s[e]),l[e]=ct(l[e]),u[e]=ct(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function ae(){return this.hours()%12||12}function re(t,e){j(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function oe(t,e){return e._meridiemParse}j("H",["HH",2],0,"hour"),j("h",["hh",2],0,ae),j("k",["kk",2],0,(function(){return this.hours()||24})),j("hmm",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)})),j("hmmss",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)+z(this.seconds(),2)})),j("Hmm",0,0,(function(){return""+this.hours()+z(this.minutes(),2)})),j("Hmmss",0,0,(function(){return""+this.hours()+z(this.minutes(),2)+z(this.seconds(),2)})),re("a",!0),re("A",!1),L("hour","h"),Y("hour",13),dt("a",oe),dt("A",oe),dt("H",J),dt("h",J),dt("k",J),dt("HH",J,Z),dt("hh",J,Z),dt("kk",J,Z),dt("hmm",Q),dt("hmmss",tt),dt("Hmm",Q),dt("Hmmss",tt),gt(["H","HH"],xt),gt(["k","kk"],(function(t,e,n){var i=k(t);e[xt]=24===i?0:i})),gt(["a","A"],(function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t})),gt(["h","hh"],(function(t,e,n){e[xt]=k(t),g(n).bigHour=!0})),gt("hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i)),g(n).bigHour=!0})),gt("hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a)),g(n).bigHour=!0})),gt("Hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i))})),gt("Hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a))}));var se,le=Ot("Hours",!0),ue={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Xt,weekdaysMin:Jt,weekdaysShort:Kt,meridiemParse:/[ap]\.?m?\.?/i},de={},he={};function ce(t){return t?t.toLowerCase().replace("_","-"):t}function fe(n){var i=null;if(!de[n]&&e&&e.exports)try{i=se._abbr,t(),ge(i)}catch(t){}return de[n]}function ge(t,e){var n;return t&&((n=s(e)?pe(t):me(t,e))?se=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),se._abbr}function me(t,e){if(null!==e){var n,i=ue;if(e.abbr=t,null!=de[t])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),i=de[t]._config;else if(null!=e.parentLocale)if(null!=de[e.parentLocale])i=de[e.parentLocale]._config;else{if(null==(n=fe(e.parentLocale)))return he[e.parentLocale]||(he[e.parentLocale]=[]),he[e.parentLocale].push({name:t,config:e}),null;i=n._config}return de[t]=new F(A(i,e)),he[t]&&he[t].forEach((function(t){me(t.name,t.config)})),ge(t),de[t]}return delete de[t],null}function pe(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return se;if(!r(t)){if(e=fe(t))return e;t=[t]}return function(t){for(var e,n,i,a,r=0;r0;){if(i=fe(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&M(a,n,!0)>=e-1)break;e--}r++}return se}(t)}function ve(t){var e,n=t._a;return n&&-2===g(t).overflow&&(e=n[bt]<0||n[bt]>11?bt:n[yt]<1||n[yt]>It(n[vt],n[bt])?yt:n[xt]<0||n[xt]>24||24===n[xt]&&(0!==n[_t]||0!==n[wt]||0!==n[kt])?xt:n[_t]<0||n[_t]>59?_t:n[wt]<0||n[wt]>59?wt:n[kt]<0||n[kt]>999?kt:-1,g(t)._overflowDayOfYear&&(eyt)&&(e=yt),g(t)._overflowWeeks&&-1===e&&(e=Mt),g(t)._overflowWeekday&&-1===e&&(e=St),g(t).overflow=e),t}function be(t,e,n){return null!=t?t:null!=e?e:n}function ye(t){var e,n,i,r,o,s=[];if(!t._d){for(i=function(t){var e=new Date(a.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[bt]&&function(t){var e,n,i,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,n=be(e.GG,t._a[vt],qt(Le(),1,4).year),i=be(e.W,1),((a=be(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=qt(Le(),r,o);n=be(e.gg,t._a[vt],u.year),i=be(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Zt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=Gt(n,i,a,r,o),t._a[vt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=be(t._a[vt],i[vt]),(t._dayOfYear>Dt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),n=jt(o,0,t._dayOfYear),t._a[bt]=n.getUTCMonth(),t._a[yt]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=i[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[xt]&&0===t._a[_t]&&0===t._a[wt]&&0===t._a[kt]&&(t._nextDay=!0,t._a[xt]=0),t._d=(t._useUTC?jt:Bt).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[xt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(g(t).weekdayMismatch=!0)}}var xe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_e=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,we=/Z|[+-]\d\d(?::?\d\d)?/,ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Me=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Se=/^\/?Date\((\-?\d+)/i;function De(t){var e,n,i,a,r,o,s=t._i,l=xe.exec(s)||_e.exec(s);if(l){for(g(t).iso=!0,e=0,n=ke.length;e0&&g(t).unusedInput.push(o),s=s.slice(s.indexOf(n)+n.length),u+=n.length),B[r]?(n?g(t).empty=!1:g(t).unusedTokens.push(r),pt(r,n,t)):t._strict&&!n&&g(t).unusedTokens.push(r);g(t).charsLeftOver=l-u,s.length>0&&g(t).unusedInput.push(s),t._a[xt]<=12&&!0===g(t).bigHour&&t._a[xt]>0&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[xt]=function(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?((i=t.isPM(n))&&e<12&&(e+=12),i||12!==e||(e=0),e):e}(t._locale,t._a[xt],t._meridiem),ye(t),ve(t)}else Oe(t);else De(t)}function Fe(t){var e=t._i,n=t._f;return t._locale=t._locale||pe(t._l),null===e||void 0===n&&""===e?p({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new x(ve(e)):(u(e)?t._d=e:r(n)?function(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis?this:t:p()}));function We(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Le();for(n=e[0],i=1;i=0?new Date(t+400,e,n)-hn:new Date(t,e,n).valueOf()}function gn(t,e,n){return t<100&&t>=0?Date.UTC(t+400,e,n)-hn:Date.UTC(t,e,n)}function mn(t,e){j(0,[t,t.length],0,e)}function pn(t,e,n,i,a){var r;return null==t?qt(this,i,a).year:(e>(r=Zt(t,i,a))&&(e=r),vn.call(this,t,e,n,i,a))}function vn(t,e,n,i,a){var r=Gt(t,e,n,i,a),o=jt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}j(0,["gg",2],0,(function(){return this.weekYear()%100})),j(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),mn("gggg","weekYear"),mn("ggggg","weekYear"),mn("GGGG","isoWeekYear"),mn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),dt("G",rt),dt("g",rt),dt("GG",J,Z),dt("gg",J,Z),dt("GGGG",nt,X),dt("gggg",nt,X),dt("GGGGG",it,K),dt("ggggg",it,K),mt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,n,i){e[i.substr(0,2)]=k(t)})),mt(["gg","GG"],(function(t,e,n,i){e[i]=a.parseTwoDigitYear(t)})),j("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),dt("Q",q),gt("Q",(function(t,e){e[bt]=3*(k(t)-1)})),j("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),dt("D",J),dt("DD",J,Z),dt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),gt(["D","DD"],yt),gt("Do",(function(t,e){e[yt]=k(t.match(J)[0])}));var bn=Ot("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),dt("DDD",et),dt("DDDD",$),gt(["DDD","DDDD"],(function(t,e,n){n._dayOfYear=k(t)})),j("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),dt("m",J),dt("mm",J,Z),gt(["m","mm"],_t);var yn=Ot("Minutes",!1);j("s",["ss",2],0,"second"),L("second","s"),Y("second",15),dt("s",J),dt("ss",J,Z),gt(["s","ss"],wt);var xn,_n=Ot("Seconds",!1);for(j("S",0,0,(function(){return~~(this.millisecond()/100)})),j(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),j(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),j(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),j(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),j(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),j(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),L("millisecond","ms"),Y("millisecond",16),dt("S",et,q),dt("SS",et,Z),dt("SSS",et,$),xn="SSSS";xn.length<=9;xn+="S")dt(xn,at);function wn(t,e){e[kt]=k(1e3*("0."+t))}for(xn="S";xn.length<=9;xn+="S")gt(xn,wn);var kn=Ot("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var Mn=x.prototype;function Sn(t){return t}Mn.add=en,Mn.calendar=function(t,e){var n=t||Le(),i=Ue(n,this).startOf("day"),r=a.calendarFormat(this,i)||"sameElse",o=e&&(O(e[r])?e[r].call(this,n):e[r]);return this.format(o||this.localeData().calendar(r,this,Le(n)))},Mn.clone=function(){return new x(this)},Mn.diff=function(t,e,n){var i,a,r;if(!this.isValid())return NaN;if(!(i=Ue(t,this)).isValid())return NaN;switch(a=6e4*(i.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=an(this,i)/12;break;case"month":r=an(this,i);break;case"quarter":r=an(this,i)/3;break;case"second":r=(this-i)/1e3;break;case"minute":r=(this-i)/6e4;break;case"hour":r=(this-i)/36e5;break;case"day":r=(this-i-a)/864e5;break;case"week":r=(this-i-a)/6048e5;break;default:r=this-i}return n?r:w(r)},Mn.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=dn-cn(e+(this._isUTC?0:this.utcOffset()*un),dn)-1;break;case"minute":e=this._d.valueOf(),e+=un-cn(e,un)-1;break;case"second":e=this._d.valueOf(),e+=ln-cn(e,ln)-1}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.format=function(t){t||(t=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var e=U(this,t);return this.localeData().postformat(e)},Mn.from=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.fromNow=function(t){return this.from(Le(),t)},Mn.to=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.toNow=function(t){return this.to(Le(),t)},Mn.get=function(t){return O(this[t=R(t)])?this[t]():this},Mn.invalidAt=function(){return g(this).overflow},Mn.isAfter=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()9999?U(n,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(n+i+"-MM-DD[T]HH:mm:ss.SSS"+a)},Mn.toJSON=function(){return this.isValid()?this.toISOString():null},Mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Mn.unix=function(){return Math.floor(this.valueOf()/1e3)},Mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},Mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Mn.year=Tt,Mn.isLeapYear=function(){return Ct(this.year())},Mn.weekYear=function(t){return pn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Mn.isoWeekYear=function(t){return pn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},Mn.quarter=Mn.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},Mn.month=zt,Mn.daysInMonth=function(){return It(this.year(),this.month())},Mn.week=Mn.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},Mn.isoWeek=Mn.isoWeeks=function(t){var e=qt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},Mn.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},Mn.isoWeeksInYear=function(){return Zt(this.year(),1,4)},Mn.date=bn,Mn.day=Mn.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},Mn.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},Mn.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},Mn.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},Mn.hour=Mn.hours=le,Mn.minute=Mn.minutes=yn,Mn.second=Mn.seconds=_n,Mn.millisecond=Mn.milliseconds=kn,Mn.utcOffset=function(t,e,n){var i,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=je(st,t)))return this}else Math.abs(t)<16&&!n&&(t*=60);return!this._isUTC&&e&&(i=Ge(this)),this._offset=t,this._isUTC=!0,null!=i&&this.add(i,"m"),r!==t&&(!e||this._changeInProgress?tn(this,Xe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Ge(this)},Mn.utc=function(t){return this.utcOffset(0,t)},Mn.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ge(this),"m")),this},Mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=je(ot,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},Mn.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Le(t).utcOffset():0,(this.utcOffset()-t)%60==0)},Mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mn.isUtc=qe,Mn.isUTC=qe,Mn.zoneAbbr=function(){return this._isUTC?"UTC":""},Mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mn.dates=D("dates accessor is deprecated. Use date instead.",bn),Mn.months=D("months accessor is deprecated. Use month instead",zt),Mn.years=D("years accessor is deprecated. Use year instead",Tt),Mn.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()})),Mn.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),(t=Fe(t))._a){var e=t._isUTC?f(t._a):Le(t._a);this._isDSTShifted=this.isValid()&&M(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var Dn=F.prototype;function Cn(t,e,n,i){var a=pe(),r=f().set(i,e);return a[n](r,t)}function Pn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return Cn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=Cn(t,i,n,"month");return a}function Tn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(n=e=t,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a,r=pe(),o=t?r._week.dow:0;if(null!=n)return Cn(e,(n+o)%7,i,"day");var s=[];for(a=0;a<7;a++)s[a]=Cn(e,(a+o)%7,i,"day");return s}Dn.calendar=function(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return O(i)?i.call(e,n):i},Dn.longDateFormat=function(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,(function(t){return t.slice(1)})),this._longDateFormat[t])},Dn.invalidDate=function(){return this._invalidDate},Dn.ordinal=function(t){return this._ordinal.replace("%d",t)},Dn.preparse=Sn,Dn.postformat=Sn,Dn.relativeTime=function(t,e,n,i){var a=this._relativeTime[n];return O(a)?a(t,e,n,i):a.replace(/%d/i,t)},Dn.pastFuture=function(t,e){var n=this._relativeTime[t>0?"future":"past"];return O(n)?n(e):n.replace(/%s/i,e)},Dn.set=function(t){var e,n;for(n in t)O(e=t[n])?this[n]=e:this["_"+n]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Dn.months=function(t,e){return t?r(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Lt).test(e)?"format":"standalone"][t.month()]:r(this._months)?this._months:this._months.standalone},Dn.monthsShort=function(t,e){return t?r(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Lt.test(e)?"format":"standalone"][t.month()]:r(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Dn.monthsParse=function(t,e,n){var i,a,r;if(this._monthsParseExact)return Wt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;i<12;i++){if(a=f([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}},Dn.monthsRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=Vt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},Dn.monthsShortRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=Et),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},Dn.week=function(t){return qt(t,this._week.dow,this._week.doy).week},Dn.firstDayOfYear=function(){return this._week.doy},Dn.firstDayOfWeek=function(){return this._week.dow},Dn.weekdays=function(t,e){var n=r(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?$t(n,this._week.dow):t?n[t.day()]:n},Dn.weekdaysMin=function(t){return!0===t?$t(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},Dn.weekdaysShort=function(t){return!0===t?$t(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},Dn.weekdaysParse=function(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Qt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}},Dn.weekdaysRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},Dn.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Dn.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ne),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Dn.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},Dn.meridiem=function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},ge("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),a.lang=D("moment.lang is deprecated. Use moment.locale instead.",ge),a.langData=D("moment.langData is deprecated. Use moment.localeData instead.",pe);var On=Math.abs;function An(t,e,n,i){var a=Xe(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Fn(t){return t<0?Math.floor(t):Math.ceil(t)}function In(t){return 4800*t/146097}function Ln(t){return 146097*t/4800}function Rn(t){return function(){return this.as(t)}}var Nn=Rn("ms"),Wn=Rn("s"),Yn=Rn("m"),zn=Rn("h"),En=Rn("d"),Vn=Rn("w"),Hn=Rn("M"),Bn=Rn("Q"),jn=Rn("y");function Un(t){return function(){return this.isValid()?this._data[t]:NaN}}var Gn=Un("milliseconds"),qn=Un("seconds"),Zn=Un("minutes"),$n=Un("hours"),Xn=Un("days"),Kn=Un("months"),Jn=Un("years"),Qn=Math.round,ti={ss:44,s:45,m:45,h:22,d:26,M:11};function ei(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}var ni=Math.abs;function ii(t){return(t>0)-(t<0)||+t}function ai(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n=ni(this._milliseconds)/1e3,i=ni(this._days),a=ni(this._months);t=w(n/60),e=w(t/60),n%=60,t%=60;var r=w(a/12),o=a%=12,s=i,l=e,u=t,d=n?n.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=ii(this._months)!==ii(h)?"-":"",g=ii(this._days)!==ii(h)?"-":"",m=ii(this._milliseconds)!==ii(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var ri=ze.prototype;return ri.isValid=function(){return this._isValid},ri.abs=function(){var t=this._data;return this._milliseconds=On(this._milliseconds),this._days=On(this._days),this._months=On(this._months),t.milliseconds=On(t.milliseconds),t.seconds=On(t.seconds),t.minutes=On(t.minutes),t.hours=On(t.hours),t.months=On(t.months),t.years=On(t.years),this},ri.add=function(t,e){return An(this,t,e,1)},ri.subtract=function(t,e){return An(this,t,e,-1)},ri.as=function(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+i/864e5,n=this._months+In(e),t){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(e=this._days+Math.round(Ln(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}},ri.asMilliseconds=Nn,ri.asSeconds=Wn,ri.asMinutes=Yn,ri.asHours=zn,ri.asDays=En,ri.asWeeks=Vn,ri.asMonths=Hn,ri.asQuarters=Bn,ri.asYears=jn,ri.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},ri._bubble=function(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Fn(Ln(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=w(r/1e3),l.seconds=t%60,e=w(t/60),l.minutes=e%60,n=w(e/60),l.hours=n%24,o+=w(n/24),a=w(In(o)),s+=a,o-=Fn(Ln(a)),i=w(s/12),s%=12,l.days=o,l.months=s,l.years=i,this},ri.clone=function(){return Xe(this)},ri.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},ri.milliseconds=Gn,ri.seconds=qn,ri.minutes=Zn,ri.hours=$n,ri.days=Xn,ri.weeks=function(){return w(this.days()/7)},ri.months=Kn,ri.years=Jn,ri.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=function(t,e,n){var i=Xe(t).abs(),a=Qn(i.as("s")),r=Qn(i.as("m")),o=Qn(i.as("h")),s=Qn(i.as("d")),l=Qn(i.as("M")),u=Qn(i.as("y")),d=a<=ti.ss&&["s",a]||a0,d[4]=n,ei.apply(null,d)}(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)},ri.toISOString=ai,ri.toString=ai,ri.toJSON=ai,ri.locale=rn,ri.localeData=sn,ri.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ai),ri.lang=on,j("X",0,0,"unix"),j("x",0,0,"valueOf"),dt("x",rt),dt("X",/[+-]?\d+(\.\d{1,3})?/),gt("X",(function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))})),gt("x",(function(t,e,n){n._d=new Date(k(t))})),a.version="2.24.0",n=Le,a.fn=Mn,a.min=function(){return We("isBefore",[].slice.call(arguments,0))},a.max=function(){return We("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=f,a.unix=function(t){return Le(1e3*t)},a.months=function(t,e){return Pn(t,e,"months")},a.isDate=u,a.locale=ge,a.invalid=p,a.duration=Xe,a.isMoment=_,a.weekdays=function(t,e,n){return Tn(t,e,n,"weekdays")},a.parseZone=function(){return Le.apply(null,arguments).parseZone()},a.localeData=pe,a.isDuration=Ee,a.monthsShort=function(t,e){return Pn(t,e,"monthsShort")},a.weekdaysMin=function(t,e,n){return Tn(t,e,n,"weekdaysMin")},a.defineLocale=me,a.updateLocale=function(t,e){if(null!=e){var n,i,a=ue;null!=(i=fe(t))&&(a=i._config),e=A(a,e),(n=new F(e)).parentLocale=de[t],de[t]=n,ge(t)}else null!=de[t]&&(null!=de[t].parentLocale?de[t]=de[t].parentLocale:null!=de[t]&&delete de[t]);return de[t]},a.locales=function(){return C(de)},a.weekdaysShort=function(t,e,n){return Tn(t,e,n,"weekdaysShort")},a.normalizeUnits=R,a.relativeTimeRounding=function(t){return void 0===t?Qn:"function"==typeof t&&(Qn=t,!0)},a.relativeTimeThreshold=function(t,e){return void 0!==ti[t]&&(void 0===e?ti[t]:(ti[t]=e,"s"===t&&(ti.ss=e-1),!0))},a.calendarFormat=function(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},a.prototype=Mn,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()})),mi={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};rn._date.override("function"==typeof gi?{_id:"moment",formats:function(){return mi},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=gi(t,e):t instanceof gi||(t=gi(t)),t.isValid()?t.valueOf():null},format:function(t,e){return gi(t).format(e)},add:function(t,e,n){return gi(t).add(e,n).valueOf()},diff:function(t,e,n){return gi(t).diff(gi(e),n)},startOf:function(t,e,n){return t=gi(t),"isoWeek"===e?t.isoWeekday(n).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return gi(t).endOf(e).valueOf()},_create:function(t){return gi(t)}}:{}),W._set("global",{plugins:{filler:{propagate:!0}}});var pi={dataset:function(t){var e=t.fill,n=t.chart,i=n.getDatasetMeta(e),a=i&&n.isDatasetVisible(e)&&i.dataset._children||[],r=a.length||0;return r?function(t,e){return e=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function bi(t){return(t.el._scale||{}).getPointPositionForValue?function(t){var e,n,i,a,r,o=t.el._scale,s=o.options,l=o.chart.data.labels.length,u=t.fill,d=[];if(!l)return null;for(e=s.ticks.reverse?o.max:o.min,n=s.ticks.reverse?o.min:o.max,i=o.getPointPositionForValue(0,e),a=0;a0;--r)H.canvas.lineTo(t,n[r],n[r-1],!0);else for(o=n[0].cx,s=n[0].cy,l=Math.sqrt(Math.pow(n[0].x-o,2)+Math.pow(n[0].y-s,2)),r=a-1;r>0;--r)t.arc(o,s,l,n[r].angle,n[r-1].angle,!0)}}function ki(t,e,n,i,a,r){var o,s,l,u,d,h,c,f,g=e.length,m=i.spanGaps,p=[],v=[],b=0,y=0;for(t.beginPath(),o=0,s=g;o=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||W.global.defaultColor,o&&s&&r.length&&(H.canvas.clipArea(u,t.chartArea),ki(u,r,o,a,s,i._loop),H.canvas.unclipArea(u)))}},Si=H.rtl.getRtlAdapter,Di=H.noop,Ci=H.valueOrDefault;function Pi(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}W._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;el.width)&&(h+=o+n.padding,d[d.length-(e>0?0:1)]=0),s[e]={left:0,top:0,width:i,height:o},d[d.length-1]+=i+n.padding})),l.height+=h}else{var c=n.padding,f=t.columnWidths=[],g=t.columnHeights=[],m=n.padding,p=0,v=0;H.each(t.legendItems,(function(t,e){var i=Pi(n,o)+o/2+a.measureText(t.text).width;e>0&&v+o+2*c>l.height&&(m+=p+n.padding,f.push(p),g.push(v),p=0,v=0),p=Math.max(p,i),v+=o+c,s[e]={left:0,top:0,width:i,height:o}})),m+=p,f.push(p),g.push(v),l.width+=m}t.width=l.width,t.height=l.height}else t.width=l.width=t.height=l.height=0},afterFit:Di,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,n=e.labels,i=W.global,a=i.defaultColor,r=i.elements.line,o=t.height,s=t.columnHeights,l=t.width,u=t.lineWidths;if(e.display){var d,h=Si(e.rtl,t.left,t.minSize.width),c=t.ctx,f=Ci(n.fontColor,i.defaultFontColor),g=H.options._parseFont(n),m=g.size;c.textAlign=h.textAlign("left"),c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=g.string;var p=Pi(n,m),v=t.legendHitBoxes,b=function(t,i){switch(e.align){case"start":return n.padding;case"end":return t-i;default:return(t-i+n.padding)/2}},y=t.isHorizontal();d=y?{x:t.left+b(l,u[0]),y:t.top+n.padding,line:0}:{x:t.left+n.padding,y:t.top+b(o,s[0]),line:0},H.rtl.overrideTextDirection(t.ctx,e.textDirection);var x=m+n.padding;H.each(t.legendItems,(function(e,i){var f=c.measureText(e.text).width,g=p+m/2+f,_=d.x,w=d.y;h.setWidth(t.minSize.width),y?i>0&&_+g+n.padding>t.left+t.minSize.width&&(w=d.y+=x,d.line++,_=d.x=t.left+b(l,u[d.line])):i>0&&w+x>t.top+t.minSize.height&&(_=d.x=_+t.columnWidths[d.line]+n.padding,d.line++,w=d.y=t.top+b(o,s[d.line]));var k=h.x(_);!function(t,e,i){if(!(isNaN(p)||p<=0)){c.save();var o=Ci(i.lineWidth,r.borderWidth);if(c.fillStyle=Ci(i.fillStyle,a),c.lineCap=Ci(i.lineCap,r.borderCapStyle),c.lineDashOffset=Ci(i.lineDashOffset,r.borderDashOffset),c.lineJoin=Ci(i.lineJoin,r.borderJoinStyle),c.lineWidth=o,c.strokeStyle=Ci(i.strokeStyle,a),c.setLineDash&&c.setLineDash(Ci(i.lineDash,r.borderDash)),n&&n.usePointStyle){var s=p*Math.SQRT2/2,l=h.xPlus(t,p/2),u=e+m/2;H.canvas.drawPoint(c,i.pointStyle,s,l,u,i.rotation)}else c.fillRect(h.leftForLtr(t,p),e,p,m),0!==o&&c.strokeRect(h.leftForLtr(t,p),e,p,m);c.restore()}}(k,w,e),v[i].left=h.leftForLtr(k,v[i].width),v[i].top=w,function(t,e,n,i){var a=m/2,r=h.xPlus(t,p+a),o=e+a;c.fillText(n.text,r,o),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(r,o),c.lineTo(h.xPlus(r,i),o),c.stroke())}(k,w,e,f),y?d.x+=g+n.padding:d.y+=x})),H.rtl.restoreTextDirection(t.ctx,e.textDirection)}},_getLegendItemAt:function(t,e){var n,i,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,n=0;n=(i=a[n]).left&&t<=i.left+i.width&&e>=i.top&&e<=i.top+i.height)return r.legendItems[n];return null},handleEvent:function(t){var e,n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover&&!i.onLeave)return}else{if("click"!==a)return;if(!i.onClick)return}e=n._getLegendItemAt(t.x,t.y),"click"===a?e&&i.onClick&&i.onClick.call(n,t.native,e):(i.onLeave&&e!==n._hoveredItem&&(n._hoveredItem&&i.onLeave.call(n,t.native,n._hoveredItem),n._hoveredItem=e),i.onHover&&e&&i.onHover.call(n,t.native,e))}});function Oi(t,e){var n=new Ti({ctx:t.ctx,options:e,chart:t});me.configure(t,n,e),me.addBox(t,n),t.legend=n}var Ai={id:"legend",_element:Ti,beforeInit:function(t){var e=t.options.legend;e&&Oi(t,e)},beforeUpdate:function(t){var e=t.options.legend,n=t.legend;e?(H.mergeIf(e,W.global.legend),n?(me.configure(t,n,e),n.options=e):Oi(t,e)):n&&(me.removeBox(t,n),delete t.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}},Fi=H.noop;W._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Ii=$.extend({initialize:function(t){H.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:Fi,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:Fi,beforeSetDimensions:Fi,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:Fi,beforeBuildLabels:Fi,buildLabels:Fi,afterBuildLabels:Fi,beforeFit:Fi,fit:function(){var t,e=this,n=e.options,i=e.minSize={},a=e.isHorizontal();n.display?(t=(H.isArray(n.text)?n.text.length:1)*H.options._parseFont(n).lineHeight+2*n.padding,e.width=i.width=a?e.maxWidth:t,e.height=i.height=a?t:e.maxHeight):e.width=i.width=e.height=i.height=0},afterFit:Fi,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,n=t.options;if(n.display){var i,a,r,o=H.options._parseFont(n),s=o.lineHeight,l=s/2+n.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=H.valueOrDefault(n.fontColor,W.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,i=f-h):(a="left"===n.position?h+l:f-l,r=d+(c-d)/2,i=c-d,u=Math.PI*("left"===n.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=n.text;if(H.isArray(g))for(var m=0,p=0;p=0;i--){var a=t[i];if(e(a))return a}},H.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},H.almostEquals=function(t,e,n){return Math.abs(t-e)=t},H.max=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.max(t,e)}),Number.NEGATIVE_INFINITY)},H.min=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.min(t,e)}),Number.POSITIVE_INFINITY)},H.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},H.toRadians=function(t){return t*(Math.PI/180)},H.toDegrees=function(t){return t*(180/Math.PI)},H._decimalPlaces=function(t){if(H.isFinite(t)){for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}},H.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},H.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},H.aliasPixel=function(t){return t%2==0?0:.5},H._alignPixel=function(t,e,n){var i=t.currentDevicePixelRatio,a=n/2;return Math.round((e-a)*i)/i+a},H.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=i*(u=isNaN(u)?0:u),c=i*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},H.EPSILON=Number.EPSILON||1e-14,H.splineCurveMonotone=function(t){var e,n,i,a,r,o,s,l,u,d=(t||[]).map((function(t){return{model:t._model,deltaK:0,mK:0}})),h=d.length;for(e=0;e0?d[e-1]:null,(a=e0?d[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},H.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},H.niceNum=function(t,e){var n=Math.floor(H.log10(t)),i=t/Math.pow(10,n);return(e?i<1.5?1:i<3?2:i<7?5:10:i<=1?1:i<=2?2:i<=5?5:10)*Math.pow(10,n)},H.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},H.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(n=s[0].clientX,i=s[0].clientY):(n=a.clientX,i=a.clientY);var l=parseFloat(H.getStyle(r,"padding-left")),u=parseFloat(H.getStyle(r,"padding-top")),d=parseFloat(H.getStyle(r,"padding-right")),h=parseFloat(H.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:n=Math.round((n-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:i=Math.round((i-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},H.getConstraintWidth=function(t){return n(t,"max-width","clientWidth")},H.getConstraintHeight=function(t){return n(t,"max-height","clientHeight")},H._calculatePadding=function(t,e,n){return(e=H.getStyle(t,e)).indexOf("%")>-1?n*parseInt(e,10)/100:parseInt(e,10)},H._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},H.getMaximumWidth=function(t){var e=H._getParentNode(t);if(!e)return t.clientWidth;var n=e.clientWidth,i=n-H._calculatePadding(e,"padding-left",n)-H._calculatePadding(e,"padding-right",n),a=H.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},H.getMaximumHeight=function(t){var e=H._getParentNode(t);if(!e)return t.clientHeight;var n=e.clientHeight,i=n-H._calculatePadding(e,"padding-top",n)-H._calculatePadding(e,"padding-bottom",n),a=H.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},H.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},H.retinaScale=function(t,e){var n=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==n){var i=t.canvas,a=t.height,r=t.width;i.height=a*n,i.width=r*n,t.ctx.scale(n,n),i.style.height||i.style.width||(i.style.height=a+"px",i.style.width=r+"px")}},H.fontString=function(t,e,n){return e+" "+t+"px "+n},H.longestText=function(t,e,n,i){var a=(i=i||{}).data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var o,s,l,u,d,h=0,c=n.length;for(o=0;on.length){for(o=0;oi&&(i=r),i},H.numberOfLabelLines=function(t){var e=1;return H.each(t,(function(t){H.isArray(t)&&t.length>e&&(e=t.length)})),e},H.color=k?function(t){return t instanceof CanvasGradient&&(t=W.global.defaultColor),k(t)}:function(t){return console.error("Color.js not found!"),t},H.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:H.color(t).saturate(.5).darken(.1).rgbString()}}(),en._adapters=rn,en.Animation=K,en.animationService=J,en.controllers=Jt,en.DatasetController=it,en.defaults=W,en.Element=$,en.elements=wt,en.Interaction=re,en.layouts=me,en.platform=Ie,en.plugins=Le,en.Scale=xn,en.scaleService=Re,en.Ticks=on,en.Tooltip=Ge,en.helpers.each(fi,(function(t,e){en.scaleService.registerScaleType(e,t,t._defaults)})),Ri)Ri.hasOwnProperty(zi)&&en.plugins.register(Ri[zi]);en.platform.initialize();var Ei=en;return"undefined"!=typeof window&&(window.Chart=en),en.Chart=en,en.Legend=Ri.legend._element,en.Title=Ri.title._element,en.pluginService=en.plugins,en.PluginBase=en.Element.extend({}),en.canvasHelpers=en.helpers.canvas,en.layoutService=en.layouts,en.LinearScaleBase=Dn,en.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],(function(t){en[t]=function(e,n){return new en(e,en.helpers.merge(n||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}})),Ei})); diff --git a/DNS price tracking/static/img/favicon.png b/DNS price tracking/static/img/favicon.png new file mode 100644 index 000000000..a252f24f1 Binary files /dev/null and b/DNS price tracking/static/img/favicon.png differ diff --git a/DNS price tracking/static/js/index.js b/DNS price tracking/static/js/index.js new file mode 100644 index 000000000..af5fe12d7 --- /dev/null +++ b/DNS price tracking/static/js/index.js @@ -0,0 +1,474 @@ +// SOURCE: https://gist.github.com/nosp4mSnippets/5846729 +// Wait for final event example during resize +var waitForFinalEvent = (function () { + var timers = {}; + return function (callback, ms, uniqueId) { + if (!uniqueId) { + uniqueId = "Don't call this twice without a uniqueId"; + } + if (timers[uniqueId]) { + clearTimeout (timers[uniqueId]); + } + timers[uniqueId] = setTimeout(callback, ms); + }; +})(); + +const SEARCH_DATA_VISIBLE = [ + { field : 'visible', value : true, operator : 'is' } +]; +const SEARCH_DATA_FAVORITE = [ + { field : 'favorite', value : true, operator : 'is' } +]; + +function showOnlyVisibleProducts() { + w2ui.products.search(SEARCH_DATA_VISIBLE); +} + +function showOnlyFavoriteProducts() { + w2ui.products.search(SEARCH_DATA_FAVORITE); +} + +function showFavoriteTotals() { + let prices_dns = []; + let prices_tp = []; + + let items = w2ui.products.find({ favorite: true }); + for (const i of items) { + let product = w2ui.products.get(i); + + if (product.price_dns != null) { + prices_dns.push(product.price_dns); + } + if (product.price_techopoint != null) { + prices_tp.push(product.price_techopoint); + } + } + + // DNS + let total_DNS_favorites_prices = $('#favorite_totals .dns_prices'); + if (prices_dns.length == 1) { + total_DNS_favorites_prices.text(prices_dns[0]); + + } else if (prices_dns.length > 1) { + total_DNS_favorites_prices.text( + prices_dns.join(' + ') + ' = ' + prices_dns.reduce((a, b) => a + b, 0) + ); + } + + let total_tp_favorites_prices = $('#favorite_totals .techopoint_prices'); + if (prices_tp.length == 1) { + total_tp_favorites_prices.text(prices_tp[0]); + } else if (prices_tp.length > 1) { + total_tp_favorites_prices.text( + prices_tp.join(' + ') + ' = ' + prices_tp.reduce((a, b) => a + b, 0) + ); + } + + // Diff + let total_diff = $('#favorite_totals .diff'); + total_diff.text( + prices_dns.reduce((a, b) => a + b, 0) + - prices_tp.reduce((a, b) => a + b, 0) + ); + + $('#favorite_totals').show(); +} + +function getLastSelectedProduct() { + // Get last saved row + let product_id; + if (localStorage.product_id) { + product_id = localStorage.product_id; + } else { + product_id = w2ui.products.records[0].recid; + } + + return product_id; +} + +function selectLastProduct() { + let product_id = getLastSelectedProduct(); + w2ui.products.select(product_id); +} + +// Плагин, что рисует отметки на элементах, что выделены в таблице #prices +Chart.pluginService.register({ + afterDraw: function(chartInstance) { + let ctx = chartInstance.chart.ctx; + + let items = []; + for (const i of w2ui.prices.getSelection()) { + items.push(w2ui.prices.get(i).datetime); + } + + ctx.fillStyle = 'red'; + ctx.lineWidth = 5; + ctx.strokeStyle = ctx.fillStyle; + + chartInstance.data.datasets.forEach(function (dataset) { + for (let i = 0; i < dataset.data.length; i++) { + // Проверяем, что точка относится к элементу, выделенному в таблице + let datetime = dataset.data[i].x; + if (!items.includes(datetime)) { + continue; + } + + let model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model; + + ctx.beginPath(); + ctx.arc(model.x, model.y, 3, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + } + }); + } +}); + + +$(function () { + $('#products').w2grid({ + name: 'products', + show: { + footer: true, + }, + multiSelect: false, + columns: [ + { field: 'recid', caption: 'ID', size: '35px', sortable: true }, + { field: 'title', caption: 'Title', size: '80%', sortable: true }, + { field: 'price_dns', caption: 'DNS', size: '70px', sortable: true, render: 'float' }, + { field: 'price_techopoint', caption: 'TP', size: '70px', sortable: true, render: 'float' }, + { field: 'link', caption: 'Link', size: '50px', + render: function(record) { + return ` + + DNS + + + TechnoPoint + `; + }, + }, + { field: 'favorite', caption: '
', size: '30px', hidden: true, + editable: { type: 'checkbox' } + }, + { field: 'visible', caption: '
👁️
', size: '30px', + style: 'text-align: center', hidden: true, + editable: { type: 'checkbox' } + }, + ], + sortData: [ { field: 'recid', direction: 'asc' } ], + records: PRODUCTS, + searchData: SEARCH_DATA_VISIBLE, + + onClick: function(event) { + // Не даем убирать выделение при повторном клике на выделенную строку + var sel = this.getSelection(); + if (sel.length >= 1 && sel[0] == event.recid) { + event.preventDefault(); + return; + } + + // Не выполняем отображение цен при клике на колонки favorite или visible + if (event.column != 5 && event.column != 6) { + event.onComplete = function() { + // Сохранение последнего выделенного товара + localStorage.product_id = event.recid; + + // Заполнение цен + w2ui.prices.clear(); + w2ui.prices.add(PRODUCT_BY_PRICES[`${event.recid}`]); + + // Рисование графика + fill_chart(w2ui.prices.records); + } + } + }, + onSave: function(event) { + // Checked favorite + let favorites = localStorage.favorites == null ? + [] : JSON.parse(localStorage.favorites); + let invisible = localStorage.invisible == null ? + [] : JSON.parse(localStorage.invisible); + + for (const value of event.changes) { + if (value.favorite != undefined) { + // Если флаг стоит и значения нет среди сохраненных + if (value.favorite && !favorites.includes(value.recid)) { + favorites.push(value.recid); + + // Если флаг убран и значение есть среди сохраненных + } else if (!value.favorite && favorites.includes(value.recid)) { + const index = favorites.indexOf(value.recid); + favorites.splice(index, 1); + } + } + + if (value.visible != undefined) { + // Если флаг стоит и значение есть среди сохраненных + if (value.visible && invisible.includes(value.recid)) { + const index = invisible.indexOf(value.recid); + invisible.splice(index, 1); + + // Если флаг убран и значения нет среди сохраненных + } else if (!value.visible && !invisible.includes(value.recid)) { + invisible.push(value.recid); + } + } + } + + localStorage.favorites = JSON.stringify(favorites); + localStorage.invisible = JSON.stringify(invisible); + }, + onRender: function(event) { + event.onComplete = function() { + // Даем время другим компонентам на рендеринг (#prices и #lineChart) + // После кликом выделяем товар, что вызовет подгрузку цен и рисование графика + setTimeout(() => { + let product_id = getLastSelectedProduct(); + this.click(product_id); + }, 100) + } + } + }); + + $('#prices').w2grid({ + name: 'prices', + show: { + footer: true, + }, + sortData: [ + { field: 'recid', direction: 'desc' }, + ], + columns: [ + { field: 'recid', caption: 'ID', size: '50px', hidden: true }, + { field: 'datetime', caption: 'Datetime', size: '100px', render: 'date:dd/mm/yyyy' }, + { field: 'price_dns', caption: 'DNS', size: '30%', render: 'float' }, + { field: 'price_techopoint', caption: 'TP', size: '30%', render: 'float' }, + ], + onSelect: function(event) { + event.onComplete = function () { + if (LINE_CHART != null) { + LINE_CHART.update(); + } + } + } + }); + + // Layout + var pstyle = 'border: 1px solid #dfdfdf; padding: 5px;'; + $('#layout').w2layout({ + name: 'layout', + panels: [ + { type: 'main', style: pstyle, content: 'products', resizable: true }, + { type: 'right', size: '280px', style: pstyle, content: 'prices' }, + { type: 'bottom', size: '1000px', style: pstyle, content: 'bottom', resizable: true }, + ], + onRender: function(event) { + if (localStorage.layout_sizes == null) { + return; + } + + event.onComplete = function() { + let layout_sizes = JSON.parse(localStorage.layout_sizes); + this.sizeTo('bottom', layout_sizes['bottom']); + } + }, + onResizing: function(event) { + let layout = this; + waitForFinalEvent( + function() { + let layout_sizes = { + bottom: layout.get('bottom').height, + }; + localStorage.layout_sizes = JSON.stringify(layout_sizes); + }, + 1000, "layout.onResizing" + ); + }, + }); + + w2ui.layout.content('main', w2ui.products); + w2ui.layout.content('right', w2ui.prices); + w2ui.layout.content('bottom', ` +
+
+ + + + + +
+ + + +
+ + + +
Total DNS prices:
Total Technopoint prices:
Diff DNS and Technopoint:
+ + + + + `); + + $('#cb_favorite').change(function() { + if (this.checked) { + w2ui.products.showColumn('favorite'); + showOnlyFavoriteProducts(); + selectLastProduct(); + showFavoriteTotals(); + + } else { + w2ui.products.hideColumn('favorite'); + w2ui.products.save(); + showOnlyVisibleProducts(); + selectLastProduct(); + $('#favorite_totals').hide(); + } + }); + + $('#cb_visible').change(function() { + if (this.checked) { + w2ui.products.showColumn('visible', 'favorite'); + w2ui.products.searchReset(); + selectLastProduct(); + $('#cb_toggle').show(); + + } else { + w2ui.products.hideColumn('visible', 'favorite'); + w2ui.products.save(); + showOnlyVisibleProducts(); + selectLastProduct(); + $('#cb_toggle').hide(); + } + }); + + $('#cb_toggle_favorite').change(function() { + w2ui.products.set({ favorite: this.checked }); + }); + + $('#cb_toggle_visible').change(function() { + w2ui.products.set({ visible: this.checked }); + }); + + // Save state legend to localStorage + var legendClickHandler = function(e, legendItem) { + // SOURCE: https://www.chartjs.org/docs/latest/configuration/legend.html + var index = legendItem.datasetIndex; + var ci = this.chart; + var meta = ci.getDatasetMeta(index); + + // See controller.isDatasetVisible comment + meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; + + // We hid a dataset ... rerender the chart + ci.update(); + // + + // Save to localStorage + let legend_hidden; + if (localStorage.legend_hidden != null) { + legend_hidden = JSON.parse(localStorage.legend_hidden); + } else { + legend_hidden = {}; + } + legend_hidden[index] = meta.hidden; + localStorage.legend_hidden = JSON.stringify(legend_hidden); + }; + + var LINE_CHART = null; + + function fill_chart(records) { + let labels = []; + let data_dns = []; + let data_tp = []; + + for (const value of records) { + // Наличие хотя бы одной из двух цен будет причиной для добавление метки на графике + if (value.price_dns || value.price_techopoint) { + let date_iso = value.datetime; + labels.push(date_iso); + + if (value.price_dns) { + data_dns.push({ + x: date_iso, + y: value.price_dns + }); + } + + if (value.price_techopoint) { + data_tp.push({ + x: date_iso, + y: value.price_techopoint + }); + } + } + } + + let ctx = document.getElementById("lineChart").getContext("2d"); + + if (LINE_CHART != null) { + LINE_CHART.destroy(); + } + + LINE_CHART = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'DNS', + lineTension: 0, + borderColor: "rgb(246, 139, 31)", + data: data_dns, + }, + { + label: 'TP', + lineTension: 0, + borderColor: "rgb(68, 44, 110)", + data: data_tp, + }, + ], + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + unit: 'month', + tooltipFormat: 'DD/MM/YYYY HH:mm:ss', + displayFormats: { + month: 'DD/MM/YYYY', + } + }, + distribution: 'linear' + }] + }, + legend: { + onClick: legendClickHandler, + } + } + }); + + // По умолчанию, цены технопоинта не показывать + LINE_CHART.data.datasets[1].hidden = true; + LINE_CHART.update(); + + // Read from localStorage + if (localStorage.legend_hidden != null) { + let legend_hidden = JSON.parse(localStorage.legend_hidden); + for (const [key, value] of Object.entries(legend_hidden)) { + LINE_CHART.data.datasets[key].hidden = value; + } + LINE_CHART.update(); + } + } +}); diff --git a/DNS price tracking/static/js/jquery-2.1.1.min.js b/DNS price tracking/static/js/jquery-2.1.1.min.js new file mode 100644 index 000000000..9ed2acc66 --- /dev/null +++ b/DNS price tracking/static/js/jquery-2.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n(" - """) + """ + ) @app.route("/get_html") def get_html(): - theme = request.args.get('theme', 'default') + theme = request.args.get("theme", "default") - return render_template_string("""\ + return render_template_string( + """\ set_theme

{{ theme }}

- + @@ -97,39 +103,117 @@ def get_html(): - """, theme=theme) + """, + theme=theme, + ) @app.route("/datagrid_data1.json") def get_datalist_data(): - return jsonify({"total": 28, "rows": [ - {"productid": "FI-SW-01", "productname": "Koi", "unitcost": "10.00", "status": "P", "listprice": "36.50", "attr1": "Large", "itemid": "EST-1"}, - {"productid": "K9-DL-01", "productname": "Dalmation", "unitcost": "12.00", "status": "P", "listprice": "18.50", "attr1": "Spotted Adult Female", "itemid": "EST-10"}, - {"productid": "RP-SN-01", "productname": "Rattlesnake", "unitcost": "12.00", "status": "P", "listprice": "38.50", "attr1": "Venomless", "itemid": "EST-11"}, - {"productid": "RP-SN-01", "productname": "Rattlesnake", "unitcost": "12.00", "status": "P", "listprice": "26.50", "attr1": "Rattleless", "itemid": "EST-12"}, - {"productid": "RP-LI-02", "productname": "Iguana", "unitcost": "12.00", "status": "P", "listprice": "35.50", "attr1": "Green Adult", "itemid": "EST-13"}, - {"productid": "FL-DSH-01", "productname": "Manx", "unitcost": "12.00", "status": "P", "listprice": "158.50", "attr1": "Tailless", "itemid": "EST-14"}, - {"productid": "FL-DSH-01", "productname": "Manx", "unitcost": "12.00", "status": "P", "listprice": "83.50", "attr1": "With tail", "itemid": "EST-15"}, - {"productid": "FL-DLH-02", "productname": "Persian", "unitcost": "12.00", "status": "P", "listprice": "23.50", "attr1": "Adult Female", "itemid": "EST-16"}, - {"productid": "FL-DLH-02", "productname": "Persian", "unitcost": "12.00", "status": "P", "listprice": "89.50", "attr1": "Adult Male", "itemid": "EST-17"}, - {"productid": "AV-CB-01", "productname": "Amazon Parrot", "unitcost": "92.00", "status": "P", "listprice": "63.50", "attr1": "Adult Male", "itemid": "EST-18"} - ]}) - -if __name__ == '__main__': + return jsonify({ + "total": 28, + "rows": [ + { + "productid": "FI-SW-01", + "productname": "Koi", + "unitcost": "10.00", + "status": "P", + "listprice": "36.50", + "attr1": "Large", + "itemid": "EST-1", + }, + { + "productid": "K9-DL-01", + "productname": "Dalmation", + "unitcost": "12.00", + "status": "P", + "listprice": "18.50", + "attr1": "Spotted Adult Female", + "itemid": "EST-10", + }, + { + "productid": "RP-SN-01", + "productname": "Rattlesnake", + "unitcost": "12.00", + "status": "P", + "listprice": "38.50", + "attr1": "Venomless", + "itemid": "EST-11", + }, + { + "productid": "RP-SN-01", + "productname": "Rattlesnake", + "unitcost": "12.00", + "status": "P", + "listprice": "26.50", + "attr1": "Rattleless", + "itemid": "EST-12", + }, + { + "productid": "RP-LI-02", + "productname": "Iguana", + "unitcost": "12.00", + "status": "P", + "listprice": "35.50", + "attr1": "Green Adult", + "itemid": "EST-13", + }, + { + "productid": "FL-DSH-01", + "productname": "Manx", + "unitcost": "12.00", + "status": "P", + "listprice": "158.50", + "attr1": "Tailless", + "itemid": "EST-14", + }, + { + "productid": "FL-DSH-01", + "productname": "Manx", + "unitcost": "12.00", + "status": "P", + "listprice": "83.50", + "attr1": "With tail", + "itemid": "EST-15", + }, + { + "productid": "FL-DLH-02", + "productname": "Persian", + "unitcost": "12.00", + "status": "P", + "listprice": "23.50", + "attr1": "Adult Female", + "itemid": "EST-16", + }, + { + "productid": "FL-DLH-02", + "productname": "Persian", + "unitcost": "12.00", + "status": "P", + "listprice": "89.50", + "attr1": "Adult Male", + "itemid": "EST-17", + }, + { + "productid": "AV-CB-01", + "productname": "Amazon Parrot", + "unitcost": "92.00", + "status": "P", + "listprice": "63.50", + "attr1": "Adult Male", + "itemid": "EST-18", + }, + ], + }) + + +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/3_ways_to_get_table/ajax__json/main.py b/flask__webservers/3_ways_to_get_table/ajax__json/main.py index c25e2240a..aeb481969 100644 --- a/flask__webservers/3_ways_to_get_table/ajax__json/main.py +++ b/flask__webservers/3_ways_to_get_table/ajax__json/main.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, render_template_string, jsonify -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) - -# Для импорта common.py import sys -sys.path.append('..') +from flask import Flask, render_template_string, jsonify + +# Для импорта common.py +sys.path.append("..") from common import generate_table +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) + + @app.route("/") def index(): - return render_template_string("""\ + return render_template_string( + """\ @@ -93,7 +96,8 @@ def index(): - """) + """ + ) @app.route("/get_table") @@ -103,21 +107,13 @@ def get_table(): return jsonify(items) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/3_ways_to_get_table/common.py b/flask__webservers/3_ways_to_get_table/common.py index 745c7f49e..879f6dd02 100644 --- a/flask__webservers/3_ways_to_get_table/common.py +++ b/flask__webservers/3_ways_to_get_table/common.py @@ -1,13 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - - -def generate_table(size=20) -> List[List[str]]: +def generate_table(size=20) -> list[list[str]]: items = [[str(i) for i in range(size + 1)]] for i in range(1, size + 1): @@ -18,13 +15,13 @@ def generate_table(size=20) -> List[List[str]]: items.append(row) - items[0][0] = '' + items[0][0] = "" return items -if __name__ == '__main__': +if __name__ == "__main__": print(generate_table(5)) print(generate_table(2)) - assert generate_table(2) == [['', '1', '2'], ['1', '1', '2'], ['2', '2', '4']] + assert generate_table(2) == [["", "1", "2"], ["1", "1", "2"], ["2", "2", "4"]] diff --git a/flask__webservers/3_ways_to_get_table/html__script/main.py b/flask__webservers/3_ways_to_get_table/html__script/main.py index 8e40234a5..ade936069 100644 --- a/flask__webservers/3_ways_to_get_table/html__script/main.py +++ b/flask__webservers/3_ways_to_get_table/html__script/main.py @@ -1,32 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, render_template_string - -app = Flask(__name__) +import json import logging +import sys -logging.basicConfig(level=logging.DEBUG) +from flask import Flask, render_template_string # Для импорта common.py -import sys +sys.path.append("..") +from common import generate_table -sys.path.append('..') -from common import generate_table +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): items = generate_table(10) - - import json items = json.dumps(items, ensure_ascii=False) - return render_template_string("""\ + return render_template_string( + """\ @@ -85,24 +85,18 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/3_ways_to_get_table/html__tag__table/main.py b/flask__webservers/3_ways_to_get_table/html__tag__table/main.py index 9ab6cbf8d..2a572ff07 100644 --- a/flask__webservers/3_ways_to_get_table/html__tag__table/main.py +++ b/flask__webservers/3_ways_to_get_table/html__tag__table/main.py @@ -1,27 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, render_template_string -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) - -# Для импорта common.py import sys -sys.path.append('..') +from flask import Flask, render_template_string + +# Для импорта common.py +sys.path.append("..") from common import generate_table +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) + + @app.route("/") def index(): items = generate_table(10) - return render_template_string("""\ + return render_template_string( + """\ @@ -68,24 +71,18 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/ajax_and_jquery/main.py b/flask__webservers/ajax_and_jquery/main.py index c460506fe..544ec2e8b 100644 --- a/flask__webservers/ajax_and_jquery/main.py +++ b/flask__webservers/ajax_and_jquery/main.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +from datetime import datetime + from flask import Flask, render_template_string, request, jsonify -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -78,10 +82,11 @@ def index():

-''') +""" + ) -@app.route("/post_method", methods=['POST']) +@app.route("/post_method", methods=["POST"]) def post_method(): data = request.get_json() print(data) @@ -91,7 +96,6 @@ def post_method(): @app.route("/get_method") def get_method(): - from datetime import datetime data = datetime.today() print(data) diff --git a/flask__webservers/ajax_recursive_ul_from_json/main.py b/flask__webservers/ajax_recursive_ul_from_json/main.py new file mode 100644 index 000000000..4d29c35a1 --- /dev/null +++ b/flask__webservers/ajax_recursive_ul_from_json/main.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +from flask import Flask, jsonify, render_template + + +# SOURCE: https://ru.stackoverflow.com/questions/1364702/ + + +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) + + +DATA = [ + { + "title": "Родитель 1", + "url": "http://domain.com/url/" + }, + { + "title": "Родитель 2", + "url": "http://domain.com/url2/", + "children": [ + { + "title": "Потомок 1", + "url": "http://domain.com/url2/child1/" + }, + { + "title": "Потомок 2", + "url": "http://domain.com/url2/child2/", + "children": [ + { + "title": "Потомок Потомка 1", + "url": "http://domain.com/url2/child1/child2/", + }, + { + "title": "Потомок Потомка 2", + "url": "http://domain.com/url2/child1/child3/", + }, + ], + }, + ], + }, + { + "title": "Родитель 3", + "url": "http://domain.com/url3/" + } +] + + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/json/") +def get_json(): + return jsonify(DATA) + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=5000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/webserver__handler_key_mouse_click_and_move/static/js/jquery-3.1.1.min.js b/flask__webservers/ajax_recursive_ul_from_json/static/js/jquery-3.1.1.min.js similarity index 100% rename from webserver__handler_key_mouse_click_and_move/static/js/jquery-3.1.1.min.js rename to flask__webservers/ajax_recursive_ul_from_json/static/js/jquery-3.1.1.min.js diff --git a/flask__webservers/ajax_recursive_ul_from_json/templates/index.html b/flask__webservers/ajax_recursive_ul_from_json/templates/index.html new file mode 100644 index 000000000..37e80efd4 --- /dev/null +++ b/flask__webservers/ajax_recursive_ul_from_json/templates/index.html @@ -0,0 +1,49 @@ + + + + + ajax_recursive_ul_from_json + + + +
+
+
+ + + + \ No newline at end of file diff --git a/flask__webservers/bootstrap_4__examples/clickable_table/main.py b/flask__webservers/bootstrap_4__examples/clickable_table/main.py index 722c9deca..3b76e8f9e 100644 --- a/flask__webservers/bootstrap_4__examples/clickable_table/main.py +++ b/flask__webservers/bootstrap_4__examples/clickable_table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://getbootstrap.com/docs/4.3/content/tables/ +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__examples/grid__layout/main.py b/flask__webservers/bootstrap_4__examples/grid__layout/main.py index 0fbbc0843..1fb24211a 100644 --- a/flask__webservers/bootstrap_4__examples/grid__layout/main.py +++ b/flask__webservers/bootstrap_4__examples/grid__layout/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://www.w3schools.com/bootstrap4/bootstrap_grid_examples.asp +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__examples/shadow/main.py b/flask__webservers/bootstrap_4__examples/shadow/main.py index 572f8b15c..9304065bd 100644 --- a/flask__webservers/bootstrap_4__examples/shadow/main.py +++ b/flask__webservers/bootstrap_4__examples/shadow/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://www.w3schools.com/bootstrap4/bootstrap_utilities.asp +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__examples/table/main.py b/flask__webservers/bootstrap_4__examples/table/main.py index 722c9deca..3b76e8f9e 100644 --- a/flask__webservers/bootstrap_4__examples/table/main.py +++ b/flask__webservers/bootstrap_4__examples/table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://getbootstrap.com/docs/4.3/content/tables/ +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__examples/tabs/main.py b/flask__webservers/bootstrap_4__examples/tabs/main.py index 10ccd9c91..554e3bb78 100644 --- a/flask__webservers/bootstrap_4__examples/tabs/main.py +++ b/flask__webservers/bootstrap_4__examples/tabs/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://getbootstrap.com/docs/4.0/components/navs/ +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py b/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py index ea7a93c35..214eecaf7 100644 --- a/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py +++ b/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap # SOURCE: https://github.com/twbs/bootstrap/releases # SOURCE: https://getbootstrap.com/docs/4.3/components/badge/ + +import logging from typing import NamedTuple from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') -import logging + +app = Flask(__name__, static_folder="../_static") + logging.basicConfig(level=logging.DEBUG) @@ -77,24 +80,16 @@ def index(): ] columns = ["NAME", "URL", "GENRES"] - return render_template('index.html', columns=columns, items=items) + return render_template("index.html", columns=columns, items=items) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/bootstrap_4__toggle_switch__examples/main.py b/flask__webservers/bootstrap_4__toggle_switch__examples/main.py index 134809d31..ed308867d 100644 --- a/flask__webservers/bootstrap_4__toggle_switch__examples/main.py +++ b/flask__webservers/bootstrap_4__toggle_switch__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -9,33 +9,27 @@ # SOURCE: https://gitbrent.github.io/bootstrap4-toggle/ +import logging from flask import Flask, render_template + + app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/caching_response.py b/flask__webservers/caching_response.py new file mode 100644 index 000000000..ac2b6128e --- /dev/null +++ b/flask__webservers/caching_response.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install flask==2.3.3 +from flask import Flask, render_template_string + +# pip install flask-caching=2.0.2 +from flask_caching import Cache + + +config = { + "DEBUG": True, # some Flask specific configs + "CACHE_TYPE": "SimpleCache", # Flask-Caching related configs + "CACHE_DEFAULT_TIMEOUT": 300, +} + +app = Flask(__name__) + +# tell Flask to use the above defined config +app.config.from_mapping(config) +cache = Cache(app) + + +@app.route("/") +@cache.cached(timeout=50) +def index(): + # SOURCE: https://stackoverflow.com/a/55050637/5909792 + return render_template_string( + """ + + + + + +
+

{{ text }}

+
+ + + """, + text=str(datetime.now()), + ) + + +if __name__ == "__main__": + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=50000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py index b4ce83c28..4a32996f5 100644 --- a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py +++ b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py @@ -1,82 +1,78 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../static') -import logging + +app = Flask(__name__, static_folder="../static") logging.basicConfig(level=logging.DEBUG) +title = "chart_js/radar_chart__DarkSouls__builds_stats" +labels = [ + "Vitality", + "Attunement", + "Endurance", + "Strength", + "Dexterity", + "Resistance", + "Intelligence", + "Faith", +] +items = [ + { + "name": "Cult Cleric Regen", + "url": "https://darksouls.wiki.fextralife.com/Cult+Cleric+Regen", + "color": "rgb(255, 99, 132)", + "stats": [95, 12, 30, 16, 13, 10, 9, 16], + "hidden": "true", + }, + { + "name": "Holy Dragon Knightess", + "url": "https://darksouls.wiki.fextralife.com/Holy+Dragon+Knightess", + "color": "rgb(255, 159, 64)", + "stats": [26, 14, 36, 16, 10, 12, 8, 60], + "hidden": "false", + }, + { + "name": "Royal Tank", + "url": "https://darksouls.wiki.fextralife.com/Royal+Tank", + "color": "rgb(75, 192, 192)", + "stats": [50, 8, 40, 46, 10, 11, 8, 10], + "hidden": "false", + }, + { + "name": "Vergil Style (Wanderer Style)", + "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", + "color": "rgb(54, 162, 235)", + "stats": [31, 12, 40, 16, 40, 12, 24, 8], + "hidden": "false", + }, + { + "name": "Vergil Style (Thief Style)", + "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", + "color": "rgb(153, 102, 255)", + "stats": [28, 12, 40, 16, 40, 10, 24, 11], + "hidden": "false", + }, +] + + @app.route("/") def index(): - title = 'chart_js/radar_chart__DarkSouls__builds_stats' - labels = ['Vitality', 'Attunement', 'Endurance', 'Strength', 'Dexterity', 'Resistance', 'Intelligence', 'Faith'] - items = [ - { - "name": "Cult Cleric Regen", - "url": "https://darksouls.wiki.fextralife.com/Cult+Cleric+Regen", - "color": "rgb(255, 99, 132)", - "stats": [95, 12, 30, 16, 13, 10, 9, 16], - "hidden": 'true', - }, - { - "name": "Holy Dragon Knightess", - "url": "https://darksouls.wiki.fextralife.com/Holy+Dragon+Knightess", - "color": "rgb(255, 159, 64)", - "stats": [26, 14, 36, 16, 10, 12, 8, 60], - "hidden": 'false', - }, - { - "name": "Royal Tank", - "url": "https://darksouls.wiki.fextralife.com/Royal+Tank", - "color": "rgb(75, 192, 192)", - "stats": [50, 8, 40, 46, 10, 11, 8, 10], - "hidden": 'false', - }, - { - "name": "Vergil Style (Wanderer Style)", - "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", - "color": "rgb(54, 162, 235)", - "stats": [31, 12, 40, 16, 40, 12, 24, 8], - "hidden": 'false', - }, - { - "name": "Vergil Style (Thief Style)", - "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", - "color": "rgb(153, 102, 255)", - "stats": [28, 12, 40, 16, 40, 10, 24, 11], - "hidden": 'false', - }, - ] - return render_template( - 'index.html', + "index.html", title=title, - title_chart='DarkSouls builds stats', + title_chart="DarkSouls builds stats", labels=labels, items=items, ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True - - # Localhost - # port=0 -- random free port - # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) - - # # Public IP - # app.run(host='0.0.0.0') + app.run(port=5000) diff --git a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/templates/index.html b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/templates/index.html index c151bf216..b9bb3366f 100644 --- a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/templates/index.html +++ b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/templates/index.html @@ -6,7 +6,7 @@ - + diff --git a/flask__webservers/chart_js/static/chart_js_2.8.0/Chart.bundle.min.js b/flask__webservers/chart_js/static/chart_js_2.8.0/Chart.bundle.min.js deleted file mode 100644 index 0bf9ea9e4..000000000 --- a/flask__webservers/chart_js/static/chart_js_2.8.0/Chart.bundle.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Chart.js v2.8.0 - * https://www.chartjs.org - * (c) 2019 Chart.js Contributors - * Released under the MIT License - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Chart=e()}(this,function(){"use strict";var t={rgb2hsl:e,rgb2hsv:i,rgb2hwb:n,rgb2cmyk:a,rgb2keyword:o,rgb2xyz:s,rgb2lab:l,rgb2lch:function(t){return v(l(t))},hsl2rgb:u,hsl2hsv:function(t){var e=t[0],i=t[1]/100,n=t[2]/100;if(0===n)return[0,0,0];return[e,100*(2*(i*=(n*=2)<=1?n:2-n)/(n+i)),100*((n+i)/2)]},hsl2hwb:function(t){return n(u(t))},hsl2cmyk:function(t){return a(u(t))},hsl2keyword:function(t){return o(u(t))},hsv2rgb:d,hsv2hsl:function(t){var e,i,n=t[0],a=t[1]/100,r=t[2]/100;return e=a*r,[n,100*(e=(e/=(i=(2-a)*r)<=1?i:2-i)||0),100*(i/=2)]},hsv2hwb:function(t){return n(d(t))},hsv2cmyk:function(t){return a(d(t))},hsv2keyword:function(t){return o(d(t))},hwb2rgb:h,hwb2hsl:function(t){return e(h(t))},hwb2hsv:function(t){return i(h(t))},hwb2cmyk:function(t){return a(h(t))},hwb2keyword:function(t){return o(h(t))},cmyk2rgb:c,cmyk2hsl:function(t){return e(c(t))},cmyk2hsv:function(t){return i(c(t))},cmyk2hwb:function(t){return n(c(t))},cmyk2keyword:function(t){return o(c(t))},keyword2rgb:_,keyword2hsl:function(t){return e(_(t))},keyword2hsv:function(t){return i(_(t))},keyword2hwb:function(t){return n(_(t))},keyword2cmyk:function(t){return a(_(t))},keyword2lab:function(t){return l(_(t))},keyword2xyz:function(t){return s(_(t))},xyz2rgb:f,xyz2lab:m,xyz2lch:function(t){return v(m(t))},lab2xyz:p,lab2rgb:y,lab2lch:v,lch2lab:x,lch2xyz:function(t){return p(x(t))},lch2rgb:function(t){return y(x(t))}};function e(t){var e,i,n=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(n,a,r),s=Math.max(n,a,r),l=s-o;return s==o?e=0:n==s?e=(a-r)/l:a==s?e=2+(r-n)/l:r==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),i=(o+s)/2,[e,100*(s==o?0:i<=.5?l/(s+o):l/(2-s-o)),100*i]}function i(t){var e,i,n=t[0],a=t[1],r=t[2],o=Math.min(n,a,r),s=Math.max(n,a,r),l=s-o;return i=0==s?0:l/s*1e3/10,s==o?e=0:n==s?e=(a-r)/l:a==s?e=2+(r-n)/l:r==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),[e,i,s/255*1e3/10]}function n(t){var i=t[0],n=t[1],a=t[2];return[e(t)[0],100*(1/255*Math.min(i,Math.min(n,a))),100*(a=1-1/255*Math.max(i,Math.max(n,a)))]}function a(t){var e,i=t[0]/255,n=t[1]/255,a=t[2]/255;return[100*((1-i-(e=Math.min(1-i,1-n,1-a)))/(1-e)||0),100*((1-n-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]}function o(t){return w[JSON.stringify(t)]}function s(t){var e=t[0]/255,i=t[1]/255,n=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)+.1805*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)),100*(.2126*e+.7152*i+.0722*n),100*(.0193*e+.1192*i+.9505*n)]}function l(t){var e=s(t),i=e[0],n=e[1],a=e[2];return n/=100,a/=108.883,i=(i/=95.047)>.008856?Math.pow(i,1/3):7.787*i+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(i-n),200*(n-(a=a>.008856?Math.pow(a,1/3):7.787*a+16/116))]}function u(t){var e,i,n,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return[r=255*l,r,r];e=2*l-(i=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(n=o+1/3*-(u-1))<0&&n++,n>1&&n--,r=6*n<1?e+6*(i-e)*n:2*n<1?i:3*n<2?e+(i-e)*(2/3-n)*6:e,a[u]=255*r;return a}function d(t){var e=t[0]/60,i=t[1]/100,n=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*n*(1-i),s=255*n*(1-i*r),l=255*n*(1-i*(1-r));n*=255;switch(a){case 0:return[n,l,o];case 1:return[s,n,o];case 2:return[o,n,l];case 3:return[o,s,n];case 4:return[l,o,n];case 5:return[n,o,s]}}function h(t){var e,i,n,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,u=s+l;switch(u>1&&(s/=u,l/=u),n=6*o-(e=Math.floor(6*o)),0!=(1&e)&&(n=1-n),a=s+n*((i=1-l)-s),e){default:case 6:case 0:r=i,g=a,b=s;break;case 1:r=a,g=i,b=s;break;case 2:r=s,g=i,b=a;break;case 3:r=s,g=a,b=i;break;case 4:r=a,g=s,b=i;break;case 5:r=i,g=s,b=a}return[255*r,255*g,255*b]}function c(t){var e=t[0]/100,i=t[1]/100,n=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a))]}function f(t){var e,i,n,a=t[0]/100,r=t[1]/100,o=t[2]/100;return i=-.9689*a+1.8758*r+.0415*o,n=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,[255*(e=Math.min(Math.max(0,e),1)),255*(i=Math.min(Math.max(0,i),1)),255*(n=Math.min(Math.max(0,n),1))]}function m(t){var e=t[0],i=t[1],n=t[2];return i/=100,n/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(e-i),200*(i-(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116))]}function p(t){var e,i,n,a,r=t[0],o=t[1],s=t[2];return r<=8?a=(i=100*r/903.3)/100*7.787+16/116:(i=100*Math.pow((r+16)/116,3),a=Math.pow(i/100,1/3)),[e=e/95.047<=.008856?e=95.047*(o/500+a-16/116)/7.787:95.047*Math.pow(o/500+a,3),i,n=n/108.883<=.008859?n=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3)]}function v(t){var e,i=t[0],n=t[1],a=t[2];return(e=360*Math.atan2(a,n)/2/Math.PI)<0&&(e+=360),[i,Math.sqrt(n*n+a*a),e]}function y(t){return f(p(t))}function x(t){var e,i=t[0],n=t[1];return e=t[2]/360*2*Math.PI,[i,n*Math.cos(e),n*Math.sin(e)]}function _(t){return k[t]}var k={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},w={};for(var M in k)w[JSON.stringify(k[M])]=M;var S=function(){return new O};for(var D in t){S[D+"Raw"]=function(e){return function(i){return"number"==typeof i&&(i=Array.prototype.slice.call(arguments)),t[e](i)}}(D);var C=/(\w+)2(\w+)/.exec(D),P=C[1],T=C[2];(S[P]=S[P]||{})[T]=S[D]=function(e){return function(i){"number"==typeof i&&(i=Array.prototype.slice.call(arguments));var n=t[e](i);if("string"==typeof n||void 0===n)return n;for(var a=0;a=0&&e<1?H(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return Y(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:Y,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return N(t,e);var i=Math.round(t[0]/255*100),n=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+i+"%, "+n+"%, "+a+"%)"},percentaString:N,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return z(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:z,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return E[t.slice(0,3)]}};function R(t){if(t){var e=[0,0,0],i=1,n=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(n){a=(n=n[1])[3];for(var r=0;ri?(e+.05)/(i+.05):(i+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,i=(e[0]+t)%360;return e[0]=i<0?360+i:i,this.setValues("hsl",e),this},mix:function(t,e){var i=t,n=void 0===e?.5:e,a=2*n-1,r=this.alpha()-i.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*i.red(),o*this.green()+s*i.green(),o*this.blue()+s*i.blue()).alpha(this.alpha()*n+i.alpha()*(1-n))},toJSON:function(){return this.rgb()},clone:function(){var t,e,i=new j,n=this.values,a=i.values;for(var r in n)n.hasOwnProperty(r)&&(t=n[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return i}},j.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},j.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},j.prototype.getValues=function(t){for(var e=this.values,i={},n=0;n=0;a--)e.call(i,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i))},easeOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/i)+1)},easeInOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:2==(t/=.5)?1:(i||(i=.45),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),t<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-$.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*$.easeInBounce(2*t):.5*$.easeOutBounce(2*t-1)+.5}},X={effects:$};Z.easingEffects=$;var K=Math.PI,J=K/180,Q=2*K,tt=K/2,et=K/4,it=2*K/3,nt={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,i,n,a,r){if(r){var o=Math.min(r,a/2,n/2),s=e+o,l=i+o,u=e+n-o,d=i+a-o;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,i,n,a=this.animations,r=0;r=i?(ut.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},yt=ut.options.resolve,bt=["push","pop","shift","splice","unshift"];function xt(t,e){var i=t._chartjs;if(i){var n=i.listeners,a=n.indexOf(e);-1!==a&&n.splice(a,1),n.length>0||(bt.forEach(function(e){delete t[e]}),delete t._chartjs)}}var _t=function(t,e){this.initialize(t,e)};ut.extend(_t.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){this.chart=t,this.index=e,this.linkScales(),this.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),i=t.getDataset();null!==e.xAxisID&&e.xAxisID in t.chart.scales||(e.xAxisID=i.xAxisID||t.chart.options.scales.xAxes[0].id),null!==e.yAxisID&&e.yAxisID in t.chart.scales||(e.yAxisID=i.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this.update(!0)},destroy:function(){this._data&&xt(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,i=this.getMeta(),n=this.getDataset().data||[],a=i.data;for(t=0,e=n.length;ti&&this.insertElements(i,n-i)},insertElements:function(t,e){for(var i=0;is;)a-=2*Math.PI;for(;a=o&&a<=s,u=r>=i.innerRadius&&r<=i.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,i=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,i=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},draw:function(){var t,e=this._chart.ctx,i=this._view,n=i.startAngle,a=i.endAngle,r="inner"===i.borderAlign?.33:0;e.save(),e.beginPath(),e.arc(i.x,i.y,Math.max(i.outerRadius-r,0),n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.fillStyle=i.backgroundColor,e.fill(),i.borderWidth&&("inner"===i.borderAlign?(e.beginPath(),t=r/i.outerRadius,e.arc(i.x,i.y,i.outerRadius,n-t,a+t),i.innerRadius>r?(t=r/i.innerRadius,e.arc(i.x,i.y,i.innerRadius-r,a+t,n-t,!0)):e.arc(i.x,i.y,r,a+Math.PI/2,n-Math.PI/2),e.closePath(),e.clip(),e.beginPath(),e.arc(i.x,i.y,i.outerRadius,n,a),e.arc(i.x,i.y,i.innerRadius,a,n,!0),e.closePath(),e.lineWidth=2*i.borderWidth,e.lineJoin="round"):(e.lineWidth=i.borderWidth,e.lineJoin="bevel"),e.strokeStyle=i.borderColor,e.stroke()),e.restore()}}),Mt=ut.valueOrDefault,St=ot.global.defaultColor;ot._set("global",{elements:{line:{tension:.4,backgroundColor:St,borderWidth:3,borderColor:St,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var Dt=gt.extend({draw:function(){var t,e,i,n,a=this._view,r=this._chart.ctx,o=a.spanGaps,s=this._children.slice(),l=ot.global,u=l.elements.line,d=-1;for(this._loop&&s.length&&s.push(s[0]),r.save(),r.lineCap=a.borderCapStyle||u.borderCapStyle,r.setLineDash&&r.setLineDash(a.borderDash||u.borderDash),r.lineDashOffset=Mt(a.borderDashOffset,u.borderDashOffset),r.lineJoin=a.borderJoinStyle||u.borderJoinStyle,r.lineWidth=Mt(a.borderWidth,u.borderWidth),r.strokeStyle=a.borderColor||l.defaultColor,r.beginPath(),d=-1,t=0;tt.x&&(e=Rt(e,"left","right")):t.basei?i:n,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>i?i:r,l:l.left||o<0?0:o>e?e:o}}function Wt(t,e,i){var n=null===e,a=null===i,r=!(!t||n&&a)&&Ft(t);return r&&(n||e>=r.left&&e<=r.right)&&(a||i>=r.top&&i<=r.bottom)}ot._set("global",{elements:{rectangle:{backgroundColor:It,borderColor:It,borderSkipped:"bottom",borderWidth:0}}});var Yt=gt.extend({draw:function(){var t=this._chart.ctx,e=this._view,i=function(t){var e=Ft(t),i=e.right-e.left,n=e.bottom-e.top,a=Lt(t,i/2,n/2);return{outer:{x:e.left,y:e.top,w:i,h:n},inner:{x:e.left+a.l,y:e.top+a.t,w:i-a.l-a.r,h:n-a.t-a.b}}}(e),n=i.outer,a=i.inner;t.fillStyle=e.backgroundColor,t.fillRect(n.x,n.y,n.w,n.h),n.w===a.w&&n.h===a.h||(t.save(),t.beginPath(),t.rect(n.x,n.y,n.w,n.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return Wt(this._view,t,e)},inLabelRange:function(t,e){var i=this._view;return At(i)?Wt(i,t,null):Wt(i,null,e)},inXRange:function(t){return Wt(this._view,t,null)},inYRange:function(t){return Wt(this._view,null,t)},getCenterPoint:function(){var t,e,i=this._view;return At(i)?(t=i.x,e=(i.y+i.base)/2):(t=(i.x+i.base)/2,e=i.y),{x:t,y:e}},getArea:function(){var t=this._view;return At(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),Nt={},zt=wt,Vt=Dt,Ht=Ot,Et=Yt;Nt.Arc=zt,Nt.Line=Vt,Nt.Point=Ht,Nt.Rectangle=Et;var Bt=ut.options.resolve;ot._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}});var jt=kt.extend({dataElementType:Nt.Rectangle,initialize:function(){var t;kt.prototype.initialize.apply(this,arguments),(t=this.getMeta()).stack=this.getDataset().stack,t.bar=!0},update:function(t){var e,i,n=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,i=n.length;e0?Math.min(o,n-i):o,i=n;return o}(i,l):-1,pixels:l,start:o,end:s,stackCount:n,scale:i}},calculateBarValuePixels:function(t,e){var i,n,a,r,o,s,l=this.chart,u=this.getMeta(),d=this._getValueScale(),h=d.isHorizontal(),c=l.data.datasets,f=+d.getRightValue(c[t].data[e]),g=d.options.minBarLength,m=d.options.stacked,p=u.stack,v=0;if(m||void 0===m&&void 0!==p)for(i=0;i=0&&a>0)&&(v+=a));return r=d.getPixelForValue(v),s=(o=d.getPixelForValue(v+f))-r,void 0!==g&&Math.abs(s)=0&&!h||f<0&&h?r-g:r+g),{size:s,base:r,head:o,center:o+s/2}},calculateBarIndexPixels:function(t,e,i){var n=i.scale.options,a="flex"===n.barThickness?function(t,e,i){var n,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],o=a.data[n],s=o&&o.custom||{},l=t.options.elements.arc;return{text:i,fillStyle:Zt([s.backgroundColor,r.backgroundColor,l.backgroundColor],void 0,n),strokeStyle:Zt([s.borderColor,r.borderColor,l.borderColor],void 0,n),lineWidth:Zt([s.borderWidth,r.borderWidth,l.borderWidth],void 0,n),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,r=e.index,o=this.chart;for(i=0,n=(o.data.datasets||[]).length;i=Math.PI?-1:p<-Math.PI?1:0))+g,y={x:Math.cos(p),y:Math.sin(p)},b={x:Math.cos(v),y:Math.sin(v)},x=p<=0&&v>=0||p<=2*Math.PI&&2*Math.PI<=v,_=p<=.5*Math.PI&&.5*Math.PI<=v||p<=2.5*Math.PI&&2.5*Math.PI<=v,k=p<=-Math.PI&&-Math.PI<=v||p<=Math.PI&&Math.PI<=v,w=p<=.5*-Math.PI&&.5*-Math.PI<=v||p<=1.5*Math.PI&&1.5*Math.PI<=v,M=f/100,S={x:k?-1:Math.min(y.x*(y.x<0?1:M),b.x*(b.x<0?1:M)),y:w?-1:Math.min(y.y*(y.y<0?1:M),b.y*(b.y<0?1:M))},D={x:x?1:Math.max(y.x*(y.x>0?1:M),b.x*(b.x>0?1:M)),y:_?1:Math.max(y.y*(y.y>0?1:M),b.y*(b.y>0?1:M))},C={width:.5*(D.x-S.x),height:.5*(D.y-S.y)};u=Math.min(s/C.width,l/C.height),d={x:-.5*(D.x+S.x),y:-.5*(D.y+S.y)}}for(e=0,i=c.length;e0&&!isNaN(t)?2*Math.PI*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,i,n,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,i=d.data.datasets.length;e(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,i=t._options,n=ut.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=$t(i.hoverBackgroundColor,n(i.backgroundColor)),e.borderColor=$t(i.hoverBorderColor,n(i.borderColor)),e.borderWidth=$t(i.hoverBorderWidth,i.borderWidth)},_resolveElementOptions:function(t,e){var i,n,a,r=this.chart,o=this.getDataset(),s=t.custom||{},l=r.options.elements.arc,u={},d={chart:r,dataIndex:e,dataset:o,datasetIndex:this.index},h=["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"];for(i=0,n=h.length;i0&&te(l[t-1]._model,s)&&(i.controlPointPreviousX=u(i.controlPointPreviousX,s.left,s.right),i.controlPointPreviousY=u(i.controlPointPreviousY,s.top,s.bottom)),t');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],o=a.data[n].custom||{},s=t.options.elements.arc;return{text:i,fillStyle:ne([o.backgroundColor,r.backgroundColor,s.backgroundColor],void 0,n),strokeStyle:ne([o.borderColor,r.borderColor,s.borderColor],void 0,n),lineWidth:ne([o.borderWidth,r.borderWidth,s.borderWidth],void 0,n),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,r=e.index,o=this.chart;for(i=0,n=(o.data.datasets||[]).length;i0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return me(t,e,{intersect:!1})},point:function(t,e){return ce(t,de(e,t))},nearest:function(t,e,i){var n=de(e,t);i.axis=i.axis||"xy";var a=ge(i.axis);return fe(t,n,i.intersect,a)},x:function(t,e,i){var n=de(e,t),a=[],r=!1;return he(t,function(t){t.inXRange(n.x)&&a.push(t),t.inRange(n.x,n.y)&&(r=!0)}),i.intersect&&!r&&(a=[]),a},y:function(t,e,i){var n=de(e,t),a=[],r=!1;return he(t,function(t){t.inYRange(n.y)&&a.push(t),t.inRange(n.x,n.y)&&(r=!0)}),i.intersect&&!r&&(a=[]),a}}};function ve(t,e){return ut.where(t,function(t){return t.position===e})}function ye(t,e){t.forEach(function(t,e){return t._tmpIndex_=e,t}),t.sort(function(t,i){var n=e?i:t,a=e?t:i;return n.weight===a.weight?n._tmpIndex_-a._tmpIndex_:n.weight-a.weight}),t.forEach(function(t){delete t._tmpIndex_})}function be(t,e){ut.each(t,function(t){e[t.position]+=t.isHorizontal()?t.height:t.width})}ot._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var xe={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,t.boxes.push(e)},removeBox:function(t,e){var i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure:function(t,e,i){for(var n,a=["fullWidth","position","weight"],r=a.length,o=0;odiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ke.default||ke,Me="$chartjs",Se="chartjs-size-monitor",De="chartjs-render-monitor",Ce="chartjs-render-animation",Pe=["animationstart","webkitAnimationStart"],Te={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function Oe(t,e){var i=ut.getStyle(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?Number(n[1]):void 0}var Ie=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Ae(t,e,i){t.addEventListener(e,i,Ie)}function Fe(t,e,i){t.removeEventListener(e,i,Ie)}function Re(t,e,i,n,a){return{type:t,chart:e,native:a||null,x:void 0!==i?i:null,y:void 0!==n?n:null}}function Le(t){var e=document.createElement("div");return e.className=t||"",e}function We(t,e,i){var n,a,r,o,s=t[Me]||(t[Me]={}),l=s.resizer=function(t){var e=Le(Se),i=Le(Se+"-expand"),n=Le(Se+"-shrink");i.appendChild(Le()),n.appendChild(Le()),e.appendChild(i),e.appendChild(n),e._reset=function(){i.scrollLeft=1e6,i.scrollTop=1e6,n.scrollLeft=1e6,n.scrollTop=1e6};var a=function(){e._reset(),t()};return Ae(i,"scroll",a.bind(i,"expand")),Ae(n,"scroll",a.bind(n,"shrink")),e}((n=function(){if(s.resizer){var n=i.options.maintainAspectRatio&&t.parentNode,a=n?n.clientWidth:0;e(Re("resize",i)),n&&n.clientWidth0){var r=t[0];r.label?i=r.label:r.xLabel?i=r.xLabel:a>0&&r.index-1?t.split("\n"):t}function qe(t){var e=ot.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Be(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Be(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Be(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Be(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Be(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Be(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Be(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Be(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Be(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function Ze(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function $e(t){return Ue([],Ge(t))}var Xe=gt.extend({initialize:function(){this._model=qe(this._options),this._lastActive=[]},getTitle:function(){var t=this._options.callbacks,e=t.beforeTitle.apply(this,arguments),i=t.title.apply(this,arguments),n=t.afterTitle.apply(this,arguments),a=[];return a=Ue(a,Ge(e)),a=Ue(a,Ge(i)),a=Ue(a,Ge(n))},getBeforeBody:function(){return $e(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var i=this,n=i._options.callbacks,a=[];return ut.each(t,function(t){var r={before:[],lines:[],after:[]};Ue(r.before,Ge(n.beforeLabel.call(i,t,e))),Ue(r.lines,n.label.call(i,t,e)),Ue(r.after,Ge(n.afterLabel.call(i,t,e))),a.push(r)}),a},getAfterBody:function(){return $e(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this._options.callbacks,e=t.beforeFooter.apply(this,arguments),i=t.footer.apply(this,arguments),n=t.afterFooter.apply(this,arguments),a=[];return a=Ue(a,Ge(e)),a=Ue(a,Ge(i)),a=Ue(a,Ge(n))},update:function(t){var e,i,n,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=qe(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},y={x:f.x,y:f.y},b={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],k=[];x=je[c.position].call(h,m,h._eventPosition);var w=[];for(e=0,i=m.length;en.width&&(a=n.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,b,v=function(t,e){var i,n,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(i=function(t){return t<=c},n=function(t){return t>c}):(i=function(t){return t<=e.width/2},n=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},i(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):n(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,b),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=y.x,g.y=y.y,g.width=b.width,g.height=b.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var i=this._chart.ctx,n=this._view,a=this.getCaretPosition(t,e,n);i.lineTo(a.x1,a.y1),i.lineTo(a.x2,a.y2),i.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,i){var n,a,r,o,s,l,u=i.caretSize,d=i.cornerRadius,h=i.xAlign,c=i.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(n=f)-u,r=n,o=s+u,l=s-u):(a=(n=f+m)+u,r=n,o=s-u,l=s+u);else if("left"===h?(n=(a=f+d+u)-u,r=a+u):"right"===h?(n=(a=f+m-d-u)-u,r=a+u):(n=(a=i.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=n,n=v}return{x1:n,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,i){var n=e.title;if(n.length){t.x=Ze(e,e._titleAlign),i.textAlign=e._titleAlign,i.textBaseline="top";var a,r,o=e.titleFontSize,s=e.titleSpacing;for(i.fillStyle=e.titleFontColor,i.font=ut.fontString(o,e._titleFontStyle,e._titleFontFamily),a=0,r=n.length;a0&&i.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var i={width:e.width,height:e.height},n={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(n,e,t,i),n.y+=e.yPadding,this.drawTitle(n,e,t),this.drawBody(n,e,t),this.drawFooter(n,e,t),t.restore())}},handleEvent:function(t){var e,i=this,n=i._options;return i._lastActive=i._lastActive||[],"mouseout"===t.type?i._active=[]:i._active=i._chart.getElementsAtEventForMode(t,n.mode,n),(e=!ut.arrayEquals(i._active,i._lastActive))&&(i._lastActive=i._active,(n.enabled||n.custom)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0),i.pivot())),e}}),Ke=je,Je=Xe;Je.positioners=Ke;var Qe=ut.valueOrDefault;function ti(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){if("xAxes"===t||"yAxes"===t){var a,r,o,s=i[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?ut.merge(e[t][a],[Ee.getScaleDefaults(r),o]):ut.merge(e[t][a],o)}else ut._merger(t,e,i,n)}})}function ei(){return ut.merge({},[].slice.call(arguments),{merger:function(t,e,i,n){var a=e[t]||{},r=i[t];"scales"===t?e[t]=ti(a,r):"scale"===t?e[t]=ut.merge(a,[Ee.getScaleDefaults(r.type),r]):ut._merger(t,e,i,n)}})}function ii(t){return"top"===t||"bottom"===t}ot._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var ni=function(t,e){return this.construct(t,e),this};ut.extend(ni.prototype,{construct:function(t,e){var i=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=ei(ot.global,ot[t.type],t.options||{}),t}(e);var n=Ve.acquireContext(t,e),a=n&&n.canvas,r=a&&a.height,o=a&&a.width;i.id=ut.uid(),i.ctx=n,i.canvas=a,i.config=e,i.width=o,i.height=r,i.aspectRatio=r?o/r:null,i.options=e.options,i._bufferedRender=!1,i.chart=i,i.controller=i,ni.instances[i.id]=i,Object.defineProperty(i,"data",{get:function(){return i.config.data},set:function(t){i.config.data=t}}),n&&a?(i.initialize(),i.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return He.notify(t,"beforeInit"),ut.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.initToolTip(),He.notify(t,"afterInit"),t},clear:function(){return ut.canvas.clear(this),this},stop:function(){return vt.cancelAnimation(this),this},resize:function(t){var e=this,i=e.options,n=e.canvas,a=i.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(ut.getMaximumWidth(n))),o=Math.max(0,Math.floor(a?r/a:ut.getMaximumHeight(n)));if((e.width!==r||e.height!==o)&&(n.width=e.width=r,n.height=e.height=o,n.style.width=r+"px",n.style.height=o+"px",ut.retinaScale(e,i.devicePixelRatio),!t)){var s={width:r,height:o};He.notify(e,"resize",[s]),i.onResize&&i.onResize(e,s),e.stop(),e.update({duration:i.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},i=t.scale;ut.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),ut.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,i=t.scales||{},n=[],a=Object.keys(i).reduce(function(t,e){return t[e]=!1,t},{});e.scales&&(n=n.concat((e.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category",dposition:"bottom"}}),(e.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear",dposition:"left"}}))),e.scale&&n.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),ut.each(n,function(e){var n=e.options,r=n.id,o=Qe(n.type,e.dtype);ii(n.position)!==ii(e.dposition)&&(n.position=e.dposition),a[r]=!0;var s=null;if(r in i&&i[r].type===o)(s=i[r]).options=n,s.ctx=t.ctx,s.chart=t;else{var l=Ee.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:n,ctx:t.ctx,chart:t}),i[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)}),ut.each(a,function(t,e){t||delete i[e]}),t.scales=i,Ee.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t=this,e=[];return ut.each(t.data.datasets,function(i,n){var a=t.getDatasetMeta(n),r=i.type||t.config.type;if(a.type&&a.type!==r&&(t.destroyDatasetMeta(n),a=t.getDatasetMeta(n)),a.type=r,a.controller)a.controller.updateIndex(n),a.controller.linkScales();else{var o=ue[a.type];if(void 0===o)throw new Error('"'+a.type+'" is not a chart type.');a.controller=new o(t,n),e.push(a.controller)}},t),e},resetElements:function(){var t=this;ut.each(t.data.datasets,function(e,i){t.getDatasetMeta(i).controller.reset()},t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(t){var e,i,n=this;if(t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]}),i=(e=n).options,ut.each(e.scales,function(t){xe.removeBox(e,t)}),i=ei(ot.global,ot[e.config.type],i),e.options=e.config.options=i,e.ensureScalesHaveIDs(),e.buildOrUpdateScales(),e.tooltip._options=i.tooltips,e.tooltip.initialize(),He._invalidate(n),!1!==He.notify(n,"beforeUpdate")){n.tooltip._data=n.data;var a=n.buildOrUpdateControllers();ut.each(n.data.datasets,function(t,e){n.getDatasetMeta(e).controller.buildOrUpdateElements()},n),n.updateLayout(),n.options.animation&&n.options.animation.duration&&ut.each(a,function(t){t.reset()}),n.updateDatasets(),n.tooltip.initialize(),n.lastActive=[],He.notify(n,"afterUpdate"),n._bufferedRender?n._bufferedRequest={duration:t.duration,easing:t.easing,lazy:t.lazy}:n.render(t)}},updateLayout:function(){!1!==He.notify(this,"beforeLayout")&&(xe.update(this,this.width,this.height),He.notify(this,"afterScaleUpdate"),He.notify(this,"afterLayout"))},updateDatasets:function(){if(!1!==He.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t=0;--i)e.isDatasetVisible(i)&&e.drawDataset(i,t);He.notify(e,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var i=this.getDatasetMeta(t),n={meta:i,index:t,easingValue:e};!1!==He.notify(this,"beforeDatasetDraw",[n])&&(i.controller.draw(e),He.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,i={tooltip:e,easingValue:t};!1!==He.notify(this,"beforeTooltipDraw",[i])&&(e.draw(),He.notify(this,"afterTooltipDraw",[i]))},getElementAtEvent:function(t){return pe.modes.single(this,t)},getElementsAtEvent:function(t){return pe.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return pe.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,i){var n=pe.modes[e];return"function"==typeof n?n(this,t,i):[]},getDatasetAtEvent:function(t){return pe.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var i=e._meta[this.id];return i||(i=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null}),i},getVisibleDatasetCount:function(){for(var t=0,e=0,i=this.data.datasets.length;e3?i[2]-i[1]:i[1]-i[0];Math.abs(n)>1&&t!==Math.floor(t)&&(n=t-Math.floor(t));var a=ut.log10(Math.abs(n)),r="";if(0!==t)if(Math.max(Math.abs(i[0]),Math.abs(i[i.length-1]))<1e-4){var o=ut.log10(Math.abs(t));r=t.toExponential(Math.floor(o)-Math.floor(a))}else{var s=-1*Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toFixed(s)}else r="0";return r},logarithmic:function(t,e,i){var n=t/Math.pow(10,Math.floor(ut.log10(t)));return 0===t?"0":1===n||2===n||5===n||0===e||e===i.length-1?t.toExponential():""}}},ui=ut.valueOrDefault,di=ut.valueAtIndexOrDefault;function hi(t){var e,i,n=[];for(e=0,i=t.length;eu&&rt.maxHeight){r--;break}r++,l=o*s}t.labelRotation=r},afterCalculateTickRotation:function(){ut.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){ut.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},i=hi(t._ticks),n=t.options,a=n.ticks,r=n.scaleLabel,o=n.gridLines,s=t._isVisible(),l=n.position,u=t.isHorizontal(),d=ut.options._parseFont,h=d(a),c=n.gridLines.tickMarkLength;if(e.width=u?t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:s&&o.drawTicks?c:0,e.height=u?s&&o.drawTicks?c:0:t.maxHeight,r.display&&s){var f=d(r),g=ut.options.toPadding(r.padding),m=f.lineHeight+g.height;u?e.height+=m:e.width+=m}if(a.display&&s){var p=ut.longestText(t.ctx,h.string,i,t.longestTextCache),v=ut.numberOfLabelLines(i),y=.5*h.size,b=t.options.ticks.padding;if(t._maxLabelLines=v,t.longestLabelWidth=p,u){var x=ut.toRadians(t.labelRotation),_=Math.cos(x),k=Math.sin(x)*p+h.lineHeight*v+y;e.height=Math.min(t.maxHeight,e.height+k+b),t.ctx.font=h.string;var w,M,S=ci(t.ctx,i[0],h.string),D=ci(t.ctx,i[i.length-1],h.string),C=t.getPixelForTick(0)-t.left,P=t.right-t.getPixelForTick(i.length-1);0!==t.labelRotation?(w="bottom"===l?_*S:_*y,M="bottom"===l?_*y:_*D):(w=S/2,M=D/2),t.paddingLeft=Math.max(w-C,0)+3,t.paddingRight=Math.max(M-P,0)+3}else a.mirror?p=0:p+=b+y,e.width=Math.min(t.maxWidth,e.width+p),t.paddingTop=h.size/2,t.paddingBottom=h.size/2}t.handleMargins(),t.width=e.width,t.height=e.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){ut.callback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ut.isNullOrUndef(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},getLabelForIndex:ut.noop,getPixelForValue:ut.noop,getValueForPixel:ut.noop,getPixelForTick:function(t){var e=this,i=e.options.offset;if(e.isHorizontal()){var n=(e.width-(e.paddingLeft+e.paddingRight))/Math.max(e._ticks.length-(i?0:1),1),a=n*t+e.paddingLeft;i&&(a+=n/2);var r=e.left+a;return r+=e.isFullWidth()?e.margins.left:0}var o=e.height-(e.paddingTop+e.paddingBottom);return e.top+t*(o/(e._ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var i=(e.width-(e.paddingLeft+e.paddingRight))*t+e.paddingLeft,n=e.left+i;return n+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,i,n=this,a=n.isHorizontal(),r=n.options.ticks.minor,o=t.length,s=!1,l=r.maxTicksLimit,u=n._tickSize()*(o-1),d=a?n.width-(n.paddingLeft+n.paddingRight):n.height-(n.paddingTop+n.PaddingBottom),h=[];for(u>d&&(s=1+Math.floor(u/d)),o>l&&(s=Math.max(s,1+Math.floor(o/l))),e=0;e1&&e%s>0&&delete i.label,h.push(i);return h},_tickSize:function(){var t=this,e=t.isHorizontal(),i=t.options.ticks.minor,n=ut.toRadians(t.labelRotation),a=Math.abs(Math.cos(n)),r=Math.abs(Math.sin(n)),o=i.autoSkipPadding||0,s=t.longestLabelWidth+o||0,l=ut.options._parseFont(i),u=t._maxLabelLines*l.lineHeight+o||0;return e?u*a>s*r?s/a:u/r:u*r0&&n>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,i=e.stepSize,n=e.maxTicksLimit;return i?t=Math.ceil(this.max/i)-Math.floor(this.min/i)+1:(t=this._computeTickLimit(),n=n||11),n&&(t=Math.min(n,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:pi,buildTicks:function(){var t=this,e=t.options.ticks,i=t.getTickLimit(),n={maxTicks:i=Math.max(2,i),min:e.min,max:e.max,precision:e.precision,stepSize:ut.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var i,n,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=ut.niceNum((g-f)/u/l)*l;if(m<1e-14&&vi(d)&&vi(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=ut.niceNum(r*m/u/l)*l),s||vi(c)?i=Math.pow(10,ut._decimalPlaces(m)):(i=Math.pow(10,c),m=Math.ceil(m*i)/i),n=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!vi(d)&&ut.almostWhole(d/m,m/1e3)&&(n=d),!vi(h)&&ut.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-n)/m,r=ut.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),n=Math.round(n*i)/i,a=Math.round(a*i)/i,o.push(vi(d)?n:d);for(var p=1;pt.max&&(t.max=n))})});t.min=isFinite(t.min)&&!isNaN(t.min)?t.min:0,t.max=isFinite(t.max)&&!isNaN(t.max)?t.max:1,this.handleTickRangeOptions()},_computeTickLimit:function(){var t;return this.isHorizontal()?Math.ceil(this.width/40):(t=ut.options._parseFont(this.options.ticks),Math.ceil(this.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e=this,i=e.start,n=+e.getRightValue(t),a=e.end-i;return e.isHorizontal()?e.left+e.width/a*(n-i):e.bottom-e.height/a*(n-i)},getValueForPixel:function(t){var e=this,i=e.isHorizontal(),n=i?e.width:e.height,a=(i?t-e.left:e.bottom-t)/n;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}}),_i=bi;xi._defaults=_i;var ki=ut.valueOrDefault;var wi={position:"left",ticks:{callback:li.formatters.logarithmic}};function Mi(t,e){return ut.isFinite(t)&&t>=0?t:e}var Si=fi.extend({determineDataLimits:function(){var t=this,e=t.options,i=t.chart,n=i.data.datasets,a=t.isHorizontal();function r(e){return a?e.xAxisID===t.id:e.yAxisID===t.id}t.min=null,t.max=null,t.minNotZero=null;var o=e.stacked;if(void 0===o&&ut.each(n,function(t,e){if(!o){var n=i.getDatasetMeta(e);i.isDatasetVisible(e)&&r(n)&&void 0!==n.stack&&(o=!0)}}),e.stacked||o){var s={};ut.each(n,function(n,a){var o=i.getDatasetMeta(a),l=[o.type,void 0===e.stacked&&void 0===o.stack?a:"",o.stack].join(".");i.isDatasetVisible(a)&&r(o)&&(void 0===s[l]&&(s[l]=[]),ut.each(n.data,function(e,i){var n=s[l],a=+t.getRightValue(e);isNaN(a)||o.data[i].hidden||a<0||(n[i]=n[i]||0,n[i]+=a)}))}),ut.each(s,function(e){if(e.length>0){var i=ut.min(e),n=ut.max(e);t.min=null===t.min?i:Math.min(t.min,i),t.max=null===t.max?n:Math.max(t.max,n)}})}else ut.each(n,function(e,n){var a=i.getDatasetMeta(n);i.isDatasetVisible(n)&&r(a)&&ut.each(e.data,function(e,i){var n=+t.getRightValue(e);isNaN(n)||a.data[i].hidden||n<0||(null===t.min?t.min=n:nt.max&&(t.max=n),0!==n&&(null===t.minNotZero||n0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(ut.log10(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,i=!t.isHorizontal(),n={min:Mi(e.min),max:Mi(e.max)},a=t.ticks=function(t,e){var i,n,a=[],r=ki(t.min,Math.pow(10,Math.floor(ut.log10(e.min)))),o=Math.floor(ut.log10(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(i=Math.floor(ut.log10(e.minNotZero)),n=Math.floor(e.minNotZero/Math.pow(10,i)),a.push(r),r=n*Math.pow(10,i)):(i=Math.floor(ut.log10(r)),n=Math.floor(r/Math.pow(10,i)));var l=i<0?Math.pow(10,Math.abs(i)):1;do{a.push(r),10==++n&&(n=1,l=++i>=0?1:l),r=Math.round(n*Math.pow(10,i)*l)/l}while(ia?{start:e-i,end:e}:{start:e,end:e+i}}function Ri(t){return 0===t||180===t?"center":t<180?"left":"right"}function Li(t,e,i,n){var a,r,o=i.y+n/2;if(ut.isArray(e))for(a=0,r=e.length;a270||t<90)&&(i.y-=e.h)}function Yi(t){return ut.isNumber(t)?t:0}var Ni=yi.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Ai(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,i=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY;ut.each(e.data.datasets,function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);ut.each(a.data,function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(i=Math.min(r,i),n=Math.max(r,n))})}}),t.min=i===Number.POSITIVE_INFINITY?0:i,t.max=n===Number.NEGATIVE_INFINITY?0:n,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Ai(this.options))},convertTicksToLabels:function(){var t=this;yi.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map(t.options.pointLabels.callback,t)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,i,n,a=ut.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=Ii(t);for(e=0;er.r&&(r.r=f.end,o.r=h),g.startr.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,i){var n=this,a=e.l/Math.sin(i.l),r=Math.max(e.r-n.width,0)/Math.sin(i.r),o=-e.t/Math.cos(i.t),s=-Math.max(e.b-(n.height-n.paddingTop),0)/Math.cos(i.b);a=Yi(a),r=Yi(r),o=Yi(o),s=Yi(s),n.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),n.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,i,n){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=i+a.drawingArea,l=a.height-a.paddingTop-n-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){return t*(2*Math.PI/Ii(this))+(this.chart.options&&this.chart.options.startAngle?this.chart.options.startAngle:0)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(null===t)return 0;var i=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*i:(t-e.min)*i},getPointPosition:function(t,e){var i=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(i)*e+this.xCenter,y:Math.sin(i)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(){var t=this.min,e=this.max;return this.getPointPositionForValue(0,this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0)},draw:function(){var t=this,e=t.options,i=e.gridLines,n=e.ticks;if(e.display){var a=t.ctx,r=this.getIndexAngle(0),o=ut.options._parseFont(n);(e.angleLines.display||e.pointLabels.display)&&function(t){var e=t.ctx,i=t.options,n=i.angleLines,a=i.gridLines,r=i.pointLabels,o=Ci(n.lineWidth,a.lineWidth),s=Ci(n.color,a.color),l=Ai(i);e.save(),e.lineWidth=o,e.strokeStyle=s,e.setLineDash&&(e.setLineDash(Ti([n.borderDash,a.borderDash,[]])),e.lineDashOffset=Ti([n.borderDashOffset,a.borderDashOffset,0]));var u=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),d=ut.options._parseFont(r);e.font=d.string,e.textBaseline="middle";for(var h=Ii(t)-1;h>=0;h--){if(n.display&&o&&s){var c=t.getPointPosition(h,u);e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(c.x,c.y),e.stroke()}if(r.display){var f=0===h?l/2:0,g=t.getPointPosition(h,u+f+5),m=Pi(r.fontColor,h,ot.global.defaultFontColor);e.fillStyle=m;var p=t.getIndexAngle(h),v=ut.toDegrees(p);e.textAlign=Ri(v),Wi(v,t._pointLabelSizes[h],g),Li(e,t.pointLabels[h]||"",g,d.lineHeight)}}e.restore()}(t),ut.each(t.ticks,function(e,s){if(s>0||n.reverse){var l=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]);if(i.display&&0!==s&&function(t,e,i,n){var a,r=t.ctx,o=e.circular,s=Ii(t),l=Pi(e.color,n-1),u=Pi(e.lineWidth,n-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,i,0,2*Math.PI);else{a=t.getPointPosition(0,i),r.moveTo(a.x,a.y);for(var d=1;d=0&&o<=s;){if(a=t[(n=o+s>>1)-1]||null,r=t[n],!a)return{lo:null,hi:r};if(r[e]i))return{lo:a,hi:r};s=n-1}}return{lo:r,hi:null}}(t,e,i),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(i-r[e])/s:0,u=(o[n]-r[n])*l;return r[n]+u}function Zi(t,e){var i=t._adapter,n=t.options.time,a=n.parser,r=a||n.format,o=e;return"function"==typeof a&&(o=a(o)),ut.isFinite(o)||(o="string"==typeof r?i.parse(o,r):i.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),ut.isFinite(o)||(o=i.parse(o))),o)}function $i(t,e){if(ut.isNullOrUndef(e))return null;var i=t.options.time,n=Zi(t,t.getRightValue(e));return null===n?n:(i.round&&(n=+t._adapter.startOf(n,i.round)),n)}function Xi(t){for(var e=ji.indexOf(t)+1,i=ji.length;e=a&&i<=r&&u.push(i);return n.min=a,n.max=r,n._unit=s.unit||function(t,e,i,n,a){var r,o;for(r=ji.length-1;r>=ji.indexOf(i);r--)if(o=ji[r],Bi[o].common&&t._adapter.diff(a,n,o)>=e.length)return o;return ji[i?ji.indexOf(i):0]}(n,u,s.minUnit,n.min,n.max),n._majorUnit=Xi(n._unit),n._table=function(t,e,i,n){if("linear"===n||!t.length)return[{time:e,pos:0},{time:i,pos:1}];var a,r,o,s,l,u=[],d=[e];for(a=0,r=t.length;ae&&s=0&&t0?o:1}}),Qi={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};Ji._defaults=Qi;var tn,en={category:gi,linear:xi,logarithmic:Si,radialLinear:Ni,time:Ji},nn=(function(t,e){t.exports=function(){var e,i;function n(){return e.apply(null,arguments)}function a(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function r(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function o(t){return void 0===t}function s(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function l(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function u(t,e){var i,n=[];for(i=0;i>>0,n=0;n0)for(i=0;i=0;return(r?i?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+n}var z=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},E={};function B(t,e,i,n){var a=n;"string"==typeof n&&(a=function(){return this[n]()}),t&&(E[t]=a),e&&(E[e[0]]=function(){return N(a.apply(this,arguments),e[1],e[2])}),i&&(E[i]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function j(t,e){return t.isValid()?(e=U(e,t.localeData()),H[e]=H[e]||function(t){var e,i,n,a=t.match(z);for(e=0,i=a.length;e=0&&V.test(t);)t=t.replace(V,n),V.lastIndex=0,i-=1;return t}var G=/\d/,q=/\d\d/,Z=/\d{3}/,$=/\d{4}/,X=/[+-]?\d{6}/,K=/\d\d?/,J=/\d\d\d\d?/,Q=/\d\d\d\d\d\d?/,tt=/\d{1,3}/,et=/\d{1,4}/,it=/[+-]?\d{1,6}/,nt=/\d+/,at=/[+-]?\d+/,rt=/Z|[+-]\d\d:?\d\d/gi,ot=/Z|[+-]\d\d(?::?\d\d)?/gi,st=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,lt={};function ut(t,e,i){lt[t]=T(e)?e:function(t,n){return t&&i?i:e}}function dt(t,e){return d(lt,t)?lt[t](e._strict,e._locale):new RegExp(ht(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,a){return e||i||n||a})))}function ht(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ct={};function ft(t,e){var i,n=e;for("string"==typeof t&&(t=[t]),s(e)&&(n=function(t,i){i[e]=k(t)}),i=0;i68?1900:2e3)};var Ct,Pt=Tt("FullYear",!0);function Tt(t,e){return function(i){return null!=i?(It(this,t,i),n.updateOffset(this,e),this):Ot(this,t)}}function Ot(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function It(t,e,i){t.isValid()&&!isNaN(i)&&("FullYear"===e&&Dt(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](i,t.month(),At(i,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](i))}function At(t,e){if(isNaN(t)||isNaN(e))return NaN;var i,n=(e%(i=12)+i)%i;return t+=(e-n)/12,1===n?Dt(t)?29:28:31-n%7%2}Ct=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=0){var i=Array.prototype.slice.call(arguments);i[0]=t+400,e=new Date(Date.UTC.apply(null,i)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Et(t,e,i){var n=7+e-i,a=(7+Ht(t,0,n).getUTCDay()-e)%7;return-a+n-1}function Bt(t,e,i,n,a){var r,o,s=(7+i-n)%7,l=Et(t,n,a),u=1+7*(e-1)+s+l;return u<=0?o=St(r=t-1)+u:u>St(t)?(r=t+1,o=u-St(t)):(r=t,o=u),{year:r,dayOfYear:o}}function jt(t,e,i){var n,a,r=Et(t.year(),e,i),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?(a=t.year()-1,n=o+Ut(a,e,i)):o>Ut(t.year(),e,i)?(n=o-Ut(t.year(),e,i),a=t.year()+1):(a=t.year(),n=o),{week:n,year:a}}function Ut(t,e,i){var n=Et(t,e,i),a=Et(t+1,e,i);return(St(t)-n+a)/7}function Gt(t,e){return t.slice(e,7).concat(t.slice(0,e))}B("w",["ww",2],"wo","week"),B("W",["WW",2],"Wo","isoWeek"),F("week","w"),F("isoWeek","W"),Y("week",5),Y("isoWeek",5),ut("w",K),ut("ww",K,q),ut("W",K),ut("WW",K,q),gt(["w","ww","W","WW"],function(t,e,i,n){e[n.substr(0,1)]=k(t)}),B("d",0,"do","day"),B("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),B("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),B("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),B("e",0,0,"weekday"),B("E",0,0,"isoWeekday"),F("day","d"),F("weekday","e"),F("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),ut("d",K),ut("e",K),ut("E",K),ut("dd",function(t,e){return e.weekdaysMinRegex(t)}),ut("ddd",function(t,e){return e.weekdaysShortRegex(t)}),ut("dddd",function(t,e){return e.weekdaysRegex(t)}),gt(["dd","ddd","dddd"],function(t,e,i,n){var a=i._locale.weekdaysParse(t,n,i._strict);null!=a?e.d=a:f(i).invalidWeekday=t}),gt(["d","e","E"],function(t,e,i,n){e[n]=k(t)});var qt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Zt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$t="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Xt=st,Kt=st,Jt=st;function Qt(){function t(t,e){return e.length-t.length}var e,i,n,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)i=c([2e3,1]).day(e),n=this.weekdaysMin(i,""),a=this.weekdaysShort(i,""),r=this.weekdays(i,""),o.push(n),s.push(a),l.push(r),u.push(n),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ht(s[e]),l[e]=ht(l[e]),u[e]=ht(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function te(){return this.hours()%12||12}function ee(t,e){B(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function ie(t,e){return e._meridiemParse}B("H",["HH",2],0,"hour"),B("h",["hh",2],0,te),B("k",["kk",2],0,function(){return this.hours()||24}),B("hmm",0,0,function(){return""+te.apply(this)+N(this.minutes(),2)}),B("hmmss",0,0,function(){return""+te.apply(this)+N(this.minutes(),2)+N(this.seconds(),2)}),B("Hmm",0,0,function(){return""+this.hours()+N(this.minutes(),2)}),B("Hmmss",0,0,function(){return""+this.hours()+N(this.minutes(),2)+N(this.seconds(),2)}),ee("a",!0),ee("A",!1),F("hour","h"),Y("hour",13),ut("a",ie),ut("A",ie),ut("H",K),ut("h",K),ut("k",K),ut("HH",K,q),ut("hh",K,q),ut("kk",K,q),ut("hmm",J),ut("hmmss",Q),ut("Hmm",J),ut("Hmmss",Q),ft(["H","HH"],bt),ft(["k","kk"],function(t,e,i){var n=k(t);e[bt]=24===n?0:n}),ft(["a","A"],function(t,e,i){i._isPm=i._locale.isPM(t),i._meridiem=t}),ft(["h","hh"],function(t,e,i){e[bt]=k(t),f(i).bigHour=!0}),ft("hmm",function(t,e,i){var n=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n)),f(i).bigHour=!0}),ft("hmmss",function(t,e,i){var n=t.length-4,a=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n,2)),e[_t]=k(t.substr(a)),f(i).bigHour=!0}),ft("Hmm",function(t,e,i){var n=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n))}),ft("Hmmss",function(t,e,i){var n=t.length-4,a=t.length-2;e[bt]=k(t.substr(0,n)),e[xt]=k(t.substr(n,2)),e[_t]=k(t.substr(a))});var ne,ae=Tt("Hours",!0),re={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Lt,week:{dow:0,doy:6},weekdays:qt,weekdaysMin:$t,weekdaysShort:Zt,meridiemParse:/[ap]\.?m?\.?/i},oe={},se={};function le(t){return t?t.toLowerCase().replace("_","-"):t}function ue(e){var i=null;if(!oe[e]&&t&&t.exports)try{i=ne._abbr;var n=_e;n("./locale/"+e),de(i)}catch(t){}return oe[e]}function de(t,e){var i;return t&&((i=o(e)?ce(t):he(t,e))?ne=i:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),ne._abbr}function he(t,e){if(null!==e){var i,n=re;if(e.abbr=t,null!=oe[t])P("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),n=oe[t]._config;else if(null!=e.parentLocale)if(null!=oe[e.parentLocale])n=oe[e.parentLocale]._config;else{if(null==(i=ue(e.parentLocale)))return se[e.parentLocale]||(se[e.parentLocale]=[]),se[e.parentLocale].push({name:t,config:e}),null;n=i._config}return oe[t]=new I(O(n,e)),se[t]&&se[t].forEach(function(t){he(t.name,t.config)}),de(t),oe[t]}return delete oe[t],null}function ce(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return ne;if(!a(t)){if(e=ue(t))return e;t=[t]}return function(t){for(var e,i,n,a,r=0;r0;){if(n=ue(a.slice(0,e).join("-")))return n;if(i&&i.length>=e&&w(a,i,!0)>=e-1)break;e--}r++}return ne}(t)}function fe(t){var e,i=t._a;return i&&-2===f(t).overflow&&(e=i[vt]<0||i[vt]>11?vt:i[yt]<1||i[yt]>At(i[pt],i[vt])?yt:i[bt]<0||i[bt]>24||24===i[bt]&&(0!==i[xt]||0!==i[_t]||0!==i[kt])?bt:i[xt]<0||i[xt]>59?xt:i[_t]<0||i[_t]>59?_t:i[kt]<0||i[kt]>999?kt:-1,f(t)._overflowDayOfYear&&(eyt)&&(e=yt),f(t)._overflowWeeks&&-1===e&&(e=wt),f(t)._overflowWeekday&&-1===e&&(e=Mt),f(t).overflow=e),t}function ge(t,e,i){return null!=t?t:null!=e?e:i}function me(t){var e,i,a,r,o,s=[];if(!t._d){for(a=function(t){var e=new Date(n.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[vt]&&function(t){var e,i,n,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,i=ge(e.GG,t._a[pt],jt(Ie(),1,4).year),n=ge(e.W,1),((a=ge(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=jt(Ie(),r,o);i=ge(e.gg,t._a[pt],u.year),n=ge(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}n<1||n>Ut(i,r,o)?f(t)._overflowWeeks=!0:null!=l?f(t)._overflowWeekday=!0:(s=Bt(i,n,a,r,o),t._a[pt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=ge(t._a[pt],a[pt]),(t._dayOfYear>St(o)||0===t._dayOfYear)&&(f(t)._overflowDayOfYear=!0),i=Ht(o,0,t._dayOfYear),t._a[vt]=i.getUTCMonth(),t._a[yt]=i.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=a[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[bt]&&0===t._a[xt]&&0===t._a[_t]&&0===t._a[kt]&&(t._nextDay=!0,t._a[bt]=0),t._d=(t._useUTC?Ht:function(t,e,i,n,a,r,o){var s;return t<100&&t>=0?(s=new Date(t+400,e,i,n,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,i,n,a,r,o),s}).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[bt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(f(t).weekdayMismatch=!0)}}var pe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ve=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ye=/Z|[+-]\d\d(?::?\d\d)?/,be=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],xe=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],ke=/^\/?Date\((\-?\d+)/i;function we(t){var e,i,n,a,r,o,s=t._i,l=pe.exec(s)||ve.exec(s);if(l){for(f(t).iso=!0,e=0,i=be.length;e0&&f(t).unusedInput.push(o),s=s.slice(s.indexOf(i)+i.length),u+=i.length),E[r]?(i?f(t).empty=!1:f(t).unusedTokens.push(r),mt(r,i,t)):t._strict&&!i&&f(t).unusedTokens.push(r);f(t).charsLeftOver=l-u,s.length>0&&f(t).unusedInput.push(s),t._a[bt]<=12&&!0===f(t).bigHour&&t._a[bt]>0&&(f(t).bigHour=void 0),f(t).parsedDateParts=t._a.slice(0),f(t).meridiem=t._meridiem,t._a[bt]=(d=t._locale,h=t._a[bt],null==(c=t._meridiem)?h:null!=d.meridiemHour?d.meridiemHour(h,c):null!=d.isPM?((g=d.isPM(c))&&h<12&&(h+=12),g||12!==h||(h=0),h):h),me(t),fe(t)}else Ce(t);else we(t);var d,h,c,g}function Te(t){var e=t._i,i=t._f;return t._locale=t._locale||ce(t._l),null===e||void 0===i&&""===e?m({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),x(e)?new b(fe(e)):(l(e)?t._d=e:a(i)?function(t){var e,i,n,a,r;if(0===t._f.length)return f(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis?this:t:m()});function Re(t,e){var i,n;if(1===e.length&&a(e[0])&&(e=e[0]),!e.length)return Ie();for(i=e[0],n=1;n=0?new Date(t+400,e,i)-si:new Date(t,e,i).valueOf()}function di(t,e,i){return t<100&&t>=0?Date.UTC(t+400,e,i)-si:Date.UTC(t,e,i)}function hi(t,e){B(0,[t,t.length],0,e)}function ci(t,e,i,n,a){var r;return null==t?jt(this,n,a).year:(r=Ut(t,n,a),e>r&&(e=r),function(t,e,i,n,a){var r=Bt(t,e,i,n,a),o=Ht(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}.call(this,t,e,i,n,a))}B(0,["gg",2],0,function(){return this.weekYear()%100}),B(0,["GG",2],0,function(){return this.isoWeekYear()%100}),hi("gggg","weekYear"),hi("ggggg","weekYear"),hi("GGGG","isoWeekYear"),hi("GGGGG","isoWeekYear"),F("weekYear","gg"),F("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),ut("G",at),ut("g",at),ut("GG",K,q),ut("gg",K,q),ut("GGGG",et,$),ut("gggg",et,$),ut("GGGGG",it,X),ut("ggggg",it,X),gt(["gggg","ggggg","GGGG","GGGGG"],function(t,e,i,n){e[n.substr(0,2)]=k(t)}),gt(["gg","GG"],function(t,e,i,a){e[a]=n.parseTwoDigitYear(t)}),B("Q",0,"Qo","quarter"),F("quarter","Q"),Y("quarter",7),ut("Q",G),ft("Q",function(t,e){e[vt]=3*(k(t)-1)}),B("D",["DD",2],"Do","date"),F("date","D"),Y("date",9),ut("D",K),ut("DD",K,q),ut("Do",function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient}),ft(["D","DD"],yt),ft("Do",function(t,e){e[yt]=k(t.match(K)[0])});var fi=Tt("Date",!0);B("DDD",["DDDD",3],"DDDo","dayOfYear"),F("dayOfYear","DDD"),Y("dayOfYear",4),ut("DDD",tt),ut("DDDD",Z),ft(["DDD","DDDD"],function(t,e,i){i._dayOfYear=k(t)}),B("m",["mm",2],0,"minute"),F("minute","m"),Y("minute",14),ut("m",K),ut("mm",K,q),ft(["m","mm"],xt);var gi=Tt("Minutes",!1);B("s",["ss",2],0,"second"),F("second","s"),Y("second",15),ut("s",K),ut("ss",K,q),ft(["s","ss"],_t);var mi,pi=Tt("Seconds",!1);for(B("S",0,0,function(){return~~(this.millisecond()/100)}),B(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),B(0,["SSS",3],0,"millisecond"),B(0,["SSSS",4],0,function(){return 10*this.millisecond()}),B(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),B(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),B(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),B(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),B(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),F("millisecond","ms"),Y("millisecond",16),ut("S",tt,G),ut("SS",tt,q),ut("SSS",tt,Z),mi="SSSS";mi.length<=9;mi+="S")ut(mi,nt);function vi(t,e){e[kt]=k(1e3*("0."+t))}for(mi="S";mi.length<=9;mi+="S")ft(mi,vi);var yi=Tt("Milliseconds",!1);B("z",0,0,"zoneAbbr"),B("zz",0,0,"zoneName");var bi=b.prototype;function xi(t){return t}bi.add=Je,bi.calendar=function(t,e){var i=t||Ie(),a=Ee(i,this).startOf("day"),r=n.calendarFormat(this,a)||"sameElse",o=e&&(T(e[r])?e[r].call(this,i):e[r]);return this.format(o||this.localeData().calendar(r,this,Ie(i)))},bi.clone=function(){return new b(this)},bi.diff=function(t,e,i){var n,a,r;if(!this.isValid())return NaN;if(!(n=Ee(t,this)).isValid())return NaN;switch(a=6e4*(n.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=ti(this,n)/12;break;case"month":r=ti(this,n);break;case"quarter":r=ti(this,n)/3;break;case"second":r=(this-n)/1e3;break;case"minute":r=(this-n)/6e4;break;case"hour":r=(this-n)/36e5;break;case"day":r=(this-n-a)/864e5;break;case"week":r=(this-n-a)/6048e5;break;default:r=this-n}return i?r:_(r)},bi.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var i=this._isUTC?di:ui;switch(t){case"year":e=i(this.year()+1,0,1)-1;break;case"quarter":e=i(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=i(this.year(),this.month()+1,1)-1;break;case"week":e=i(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=i(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=i(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=oi-li(e+(this._isUTC?0:this.utcOffset()*ri),oi)-1;break;case"minute":e=this._d.valueOf(),e+=ri-li(e,ri)-1;break;case"second":e=this._d.valueOf(),e+=ai-li(e,ai)-1}return this._d.setTime(e),n.updateOffset(this,!0),this},bi.format=function(t){t||(t=this.isUtc()?n.defaultFormatUtc:n.defaultFormat);var e=j(this,t);return this.localeData().postformat(e)},bi.from=function(t,e){return this.isValid()&&(x(t)&&t.isValid()||Ie(t).isValid())?qe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},bi.fromNow=function(t){return this.from(Ie(),t)},bi.to=function(t,e){return this.isValid()&&(x(t)&&t.isValid()||Ie(t).isValid())?qe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},bi.toNow=function(t){return this.to(Ie(),t)},bi.get=function(t){return T(this[t=R(t)])?this[t]():this},bi.invalidAt=function(){return f(this).overflow},bi.isAfter=function(t,e){var i=x(t)?t:Ie(t);return!(!this.isValid()||!i.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>i.valueOf():i.valueOf()9999?j(i,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):T(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",j(i,"Z")):j(i,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},bi.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var i="["+t+'("]',n=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(i+n+"-MM-DD[T]HH:mm:ss.SSS"+a)},bi.toJSON=function(){return this.isValid()?this.toISOString():null},bi.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},bi.unix=function(){return Math.floor(this.valueOf()/1e3)},bi.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},bi.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},bi.year=Pt,bi.isLeapYear=function(){return Dt(this.year())},bi.weekYear=function(t){return ci.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},bi.isoWeekYear=function(t){return ci.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},bi.quarter=bi.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},bi.month=Yt,bi.daysInMonth=function(){return At(this.year(),this.month())},bi.week=bi.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},bi.isoWeek=bi.isoWeeks=function(t){var e=jt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},bi.weeksInYear=function(){var t=this.localeData()._week;return Ut(this.year(),t.dow,t.doy)},bi.isoWeeksInYear=function(){return Ut(this.year(),1,4)},bi.date=fi,bi.day=bi.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},bi.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},bi.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},bi.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},bi.hour=bi.hours=ae,bi.minute=bi.minutes=gi,bi.second=bi.seconds=pi,bi.millisecond=bi.milliseconds=yi,bi.utcOffset=function(t,e,i){var a,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=He(ot,t)))return this}else Math.abs(t)<16&&!i&&(t*=60);return!this._isUTC&&e&&(a=Be(this)),this._offset=t,this._isUTC=!0,null!=a&&this.add(a,"m"),r!==t&&(!e||this._changeInProgress?Ke(this,qe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,n.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Be(this)},bi.utc=function(t){return this.utcOffset(0,t)},bi.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Be(this),"m")),this},bi.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=He(rt,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},bi.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Ie(t).utcOffset():0,(this.utcOffset()-t)%60==0)},bi.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},bi.isLocal=function(){return!!this.isValid()&&!this._isUTC},bi.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},bi.isUtc=je,bi.isUTC=je,bi.zoneAbbr=function(){return this._isUTC?"UTC":""},bi.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},bi.dates=S("dates accessor is deprecated. Use date instead.",fi),bi.months=S("months accessor is deprecated. Use month instead",Yt),bi.years=S("years accessor is deprecated. Use year instead",Pt),bi.zone=S("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}),bi.isDSTShifted=S("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var t={};if(v(t,this),(t=Te(t))._a){var e=t._isUTC?c(t._a):Ie(t._a);this._isDSTShifted=this.isValid()&&w(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted});var _i=I.prototype;function ki(t,e,i,n){var a=ce(),r=c().set(n,e);return a[i](r,t)}function wi(t,e,i){if(s(t)&&(e=t,t=void 0),t=t||"",null!=e)return ki(t,e,i,"month");var n,a=[];for(n=0;n<12;n++)a[n]=ki(t,n,i,"month");return a}function Mi(t,e,i,n){"boolean"==typeof t?(s(e)&&(i=e,e=void 0),e=e||""):(i=e=t,t=!1,s(e)&&(i=e,e=void 0),e=e||"");var a,r=ce(),o=t?r._week.dow:0;if(null!=i)return ki(e,(i+o)%7,n,"day");var l=[];for(a=0;a<7;a++)l[a]=ki(e,(a+o)%7,n,"day");return l}_i.calendar=function(t,e,i){var n=this._calendar[t]||this._calendar.sameElse;return T(n)?n.call(e,i):n},_i.longDateFormat=function(t){var e=this._longDateFormat[t],i=this._longDateFormat[t.toUpperCase()];return e||!i?e:(this._longDateFormat[t]=i.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])},_i.invalidDate=function(){return this._invalidDate},_i.ordinal=function(t){return this._ordinal.replace("%d",t)},_i.preparse=xi,_i.postformat=xi,_i.relativeTime=function(t,e,i,n){var a=this._relativeTime[i];return T(a)?a(t,e,i,n):a.replace(/%d/i,t)},_i.pastFuture=function(t,e){var i=this._relativeTime[t>0?"future":"past"];return T(i)?i(e):i.replace(/%s/i,e)},_i.set=function(t){var e,i;for(i in t)T(e=t[i])?this[i]=e:this["_"+i]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},_i.months=function(t,e){return t?a(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Ft).test(e)?"format":"standalone"][t.month()]:a(this._months)?this._months:this._months.standalone},_i.monthsShort=function(t,e){return t?a(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Ft.test(e)?"format":"standalone"][t.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},_i.monthsParse=function(t,e,i){var n,a,r;if(this._monthsParseExact)return function(t,e,i){var n,a,r,o=t.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],n=0;n<12;++n)r=c([2e3,n]),this._shortMonthsParse[n]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[n]=this.months(r,"").toLocaleLowerCase();return i?"MMM"===e?-1!==(a=Ct.call(this._shortMonthsParse,o))?a:null:-1!==(a=Ct.call(this._longMonthsParse,o))?a:null:"MMM"===e?-1!==(a=Ct.call(this._shortMonthsParse,o))?a:-1!==(a=Ct.call(this._longMonthsParse,o))?a:null:-1!==(a=Ct.call(this._longMonthsParse,o))?a:-1!==(a=Ct.call(this._shortMonthsParse,o))?a:null}.call(this,t,e,i);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),n=0;n<12;n++){if(a=c([2e3,n]),i&&!this._longMonthsParse[n]&&(this._longMonthsParse[n]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[n]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),i||this._monthsParse[n]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[n]=new RegExp(r.replace(".",""),"i")),i&&"MMMM"===e&&this._longMonthsParse[n].test(t))return n;if(i&&"MMM"===e&&this._shortMonthsParse[n].test(t))return n;if(!i&&this._monthsParse[n].test(t))return n}},_i.monthsRegex=function(t){return this._monthsParseExact?(d(this,"_monthsRegex")||Vt.call(this),t?this._monthsStrictRegex:this._monthsRegex):(d(this,"_monthsRegex")||(this._monthsRegex=zt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},_i.monthsShortRegex=function(t){return this._monthsParseExact?(d(this,"_monthsRegex")||Vt.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(d(this,"_monthsShortRegex")||(this._monthsShortRegex=Nt),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},_i.week=function(t){return jt(t,this._week.dow,this._week.doy).week},_i.firstDayOfYear=function(){return this._week.doy},_i.firstDayOfWeek=function(){return this._week.dow},_i.weekdays=function(t,e){var i=a(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?Gt(i,this._week.dow):t?i[t.day()]:i},_i.weekdaysMin=function(t){return!0===t?Gt(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},_i.weekdaysShort=function(t){return!0===t?Gt(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},_i.weekdaysParse=function(t,e,i){var n,a,r;if(this._weekdaysParseExact)return function(t,e,i){var n,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],n=0;n<7;++n)r=c([2e3,1]).day(n),this._minWeekdaysParse[n]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[n]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[n]=this.weekdays(r,"").toLocaleLowerCase();return i?"dddd"===e?-1!==(a=Ct.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Ct.call(this._minWeekdaysParse,o))?a:-1!==(a=Ct.call(this._weekdaysParse,o))?a:-1!==(a=Ct.call(this._shortWeekdaysParse,o))?a:null}.call(this,t,e,i);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),n=0;n<7;n++){if(a=c([2e3,1]).day(n),i&&!this._fullWeekdaysParse[n]&&(this._fullWeekdaysParse[n]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[n]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[n]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[n]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[n]=new RegExp(r.replace(".",""),"i")),i&&"dddd"===e&&this._fullWeekdaysParse[n].test(t))return n;if(i&&"ddd"===e&&this._shortWeekdaysParse[n].test(t))return n;if(i&&"dd"===e&&this._minWeekdaysParse[n].test(t))return n;if(!i&&this._weekdaysParse[n].test(t))return n}},_i.weekdaysRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(d(this,"_weekdaysRegex")||(this._weekdaysRegex=Xt),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},_i.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(d(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Kt),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},_i.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Qt.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(d(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Jt),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},_i.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},_i.meridiem=function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},de("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,i=1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),n.lang=S("moment.lang is deprecated. Use moment.locale instead.",de),n.langData=S("moment.langData is deprecated. Use moment.localeData instead.",ce);var Si=Math.abs;function Di(t,e,i,n){var a=qe(e,i);return t._milliseconds+=n*a._milliseconds,t._days+=n*a._days,t._months+=n*a._months,t._bubble()}function Ci(t){return t<0?Math.floor(t):Math.ceil(t)}function Pi(t){return 4800*t/146097}function Ti(t){return 146097*t/4800}function Oi(t){return function(){return this.as(t)}}var Ii=Oi("ms"),Ai=Oi("s"),Fi=Oi("m"),Ri=Oi("h"),Li=Oi("d"),Wi=Oi("w"),Yi=Oi("M"),Ni=Oi("Q"),zi=Oi("y");function Vi(t){return function(){return this.isValid()?this._data[t]:NaN}}var Hi=Vi("milliseconds"),Ei=Vi("seconds"),Bi=Vi("minutes"),ji=Vi("hours"),Ui=Vi("days"),Gi=Vi("months"),qi=Vi("years"),Zi=Math.round,$i={ss:44,s:45,m:45,h:22,d:26,M:11},Xi=Math.abs;function Ki(t){return(t>0)-(t<0)||+t}function Ji(){if(!this.isValid())return this.localeData().invalidDate();var t,e,i=Xi(this._milliseconds)/1e3,n=Xi(this._days),a=Xi(this._months);t=_(i/60),e=_(t/60),i%=60,t%=60;var r=_(a/12),o=a%=12,s=n,l=e,u=t,d=i?i.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=Ki(this._months)!==Ki(h)?"-":"",g=Ki(this._days)!==Ki(h)?"-":"",m=Ki(this._milliseconds)!==Ki(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var Qi=We.prototype;return Qi.isValid=function(){return this._isValid},Qi.abs=function(){var t=this._data;return this._milliseconds=Si(this._milliseconds),this._days=Si(this._days),this._months=Si(this._months),t.milliseconds=Si(t.milliseconds),t.seconds=Si(t.seconds),t.minutes=Si(t.minutes),t.hours=Si(t.hours),t.months=Si(t.months),t.years=Si(t.years),this},Qi.add=function(t,e){return Di(this,t,e,1)},Qi.subtract=function(t,e){return Di(this,t,e,-1)},Qi.as=function(t){if(!this.isValid())return NaN;var e,i,n=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+n/864e5,i=this._months+Pi(e),t){case"month":return i;case"quarter":return i/3;case"year":return i/12}else switch(e=this._days+Math.round(Ti(this._months)),t){case"week":return e/7+n/6048e5;case"day":return e+n/864e5;case"hour":return 24*e+n/36e5;case"minute":return 1440*e+n/6e4;case"second":return 86400*e+n/1e3;case"millisecond":return Math.floor(864e5*e)+n;default:throw new Error("Unknown unit "+t)}},Qi.asMilliseconds=Ii,Qi.asSeconds=Ai,Qi.asMinutes=Fi,Qi.asHours=Ri,Qi.asDays=Li,Qi.asWeeks=Wi,Qi.asMonths=Yi,Qi.asQuarters=Ni,Qi.asYears=zi,Qi.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},Qi._bubble=function(){var t,e,i,n,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Ci(Ti(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=_(r/1e3),l.seconds=t%60,e=_(t/60),l.minutes=e%60,i=_(e/60),l.hours=i%24,o+=_(i/24),a=_(Pi(o)),s+=a,o-=Ci(Ti(a)),n=_(s/12),s%=12,l.days=o,l.months=s,l.years=n,this},Qi.clone=function(){return qe(this)},Qi.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},Qi.milliseconds=Hi,Qi.seconds=Ei,Qi.minutes=Bi,Qi.hours=ji,Qi.days=Ui,Qi.weeks=function(){return _(this.days()/7)},Qi.months=Gi,Qi.years=qi,Qi.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),i=function(t,e,i){var n=qe(t).abs(),a=Zi(n.as("s")),r=Zi(n.as("m")),o=Zi(n.as("h")),s=Zi(n.as("d")),l=Zi(n.as("M")),u=Zi(n.as("y")),d=a<=$i.ss&&["s",a]||a<$i.s&&["ss",a]||r<=1&&["m"]||r<$i.m&&["mm",r]||o<=1&&["h"]||o<$i.h&&["hh",o]||s<=1&&["d"]||s<$i.d&&["dd",s]||l<=1&&["M"]||l<$i.M&&["MM",l]||u<=1&&["y"]||["yy",u];return d[2]=e,d[3]=+t>0,d[4]=i,function(t,e,i,n,a){return a.relativeTime(e||1,!!i,t,n)}.apply(null,d)}(this,!t,e);return t&&(i=e.pastFuture(+this,i)),e.postformat(i)},Qi.toISOString=Ji,Qi.toString=Ji,Qi.toJSON=Ji,Qi.locale=ei,Qi.localeData=ni,Qi.toIsoString=S("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Ji),Qi.lang=ii,B("X",0,0,"unix"),B("x",0,0,"valueOf"),ut("x",at),ut("X",/[+-]?\d+(\.\d{1,3})?/),ft("X",function(t,e,i){i._d=new Date(1e3*parseFloat(t,10))}),ft("x",function(t,e,i){i._d=new Date(k(t))}),n.version="2.24.0",e=Ie,n.fn=bi,n.min=function(){return Re("isBefore",[].slice.call(arguments,0))},n.max=function(){return Re("isAfter",[].slice.call(arguments,0))},n.now=function(){return Date.now?Date.now():+new Date},n.utc=c,n.unix=function(t){return Ie(1e3*t)},n.months=function(t,e){return wi(t,e,"months")},n.isDate=l,n.locale=de,n.invalid=m,n.duration=qe,n.isMoment=x,n.weekdays=function(t,e,i){return Mi(t,e,i,"weekdays")},n.parseZone=function(){return Ie.apply(null,arguments).parseZone()},n.localeData=ce,n.isDuration=Ye,n.monthsShort=function(t,e){return wi(t,e,"monthsShort")},n.weekdaysMin=function(t,e,i){return Mi(t,e,i,"weekdaysMin")},n.defineLocale=he,n.updateLocale=function(t,e){if(null!=e){var i,n,a=re;null!=(n=ue(t))&&(a=n._config),e=O(a,e),(i=new I(e)).parentLocale=oe[t],oe[t]=i,de(t)}else null!=oe[t]&&(null!=oe[t].parentLocale?oe[t]=oe[t].parentLocale:null!=oe[t]&&delete oe[t]);return oe[t]},n.locales=function(){return D(oe)},n.weekdaysShort=function(t,e,i){return Mi(t,e,i,"weekdaysShort")},n.normalizeUnits=R,n.relativeTimeRounding=function(t){return void 0===t?Zi:"function"==typeof t&&(Zi=t,!0)},n.relativeTimeThreshold=function(t,e){return void 0!==$i[t]&&(void 0===e?$i[t]:($i[t]=e,"s"===t&&($i.ss=e-1),!0))},n.calendarFormat=function(t,e){var i=t.diff(e,"days",!0);return i<-6?"sameElse":i<-1?"lastWeek":i<0?"lastDay":i<1?"sameDay":i<2?"nextDay":i<7?"nextWeek":"sameElse"},n.prototype=bi,n.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},n}()}(tn={exports:{}},tn.exports),tn.exports),an={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};si._date.override("function"==typeof nn?{_id:"moment",formats:function(){return an},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=nn(t,e):t instanceof nn||(t=nn(t)),t.isValid()?t.valueOf():null},format:function(t,e){return nn(t).format(e)},add:function(t,e,i){return nn(t).add(e,i).valueOf()},diff:function(t,e,i){return nn.duration(nn(t).diff(nn(e))).as(i)},startOf:function(t,e,i){return t=nn(t),"isoWeek"===e?t.isoWeekday(i).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return nn(t).endOf(e).valueOf()},_create:function(t){return nn(t)}}:{}),ot._set("global",{plugins:{filler:{propagate:!0}}});var rn={dataset:function(t){var e=t.fill,i=t.chart,n=i.getDatasetMeta(e),a=n&&i.isDatasetVisible(e)&&n.dataset._children||[],r=a.length||0;return r?function(t,e){return e=i)&&n;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function sn(t){var e,i=t.el._model||{},n=t.el._scale||{},a=t.fill,r=null;if(isFinite(a))return null;if("start"===a?r=void 0===i.scaleBottom?n.bottom:i.scaleBottom:"end"===a?r=void 0===i.scaleTop?n.top:i.scaleTop:void 0!==i.scaleZero?r=i.scaleZero:n.getBasePosition?r=n.getBasePosition():n.getBasePixel&&(r=n.getBasePixel()),null!=r){if(void 0!==r.x&&void 0!==r.y)return r;if(ut.isFinite(r))return{x:(e=n.isHorizontal())?r:null,y:e?null:r}}return null}function ln(t,e,i){var n,a=t[e].fill,r=[e];if(!i)return a;for(;!1!==a&&-1===r.indexOf(a);){if(!isFinite(a))return a;if(!(n=t[a]))return!1;if(n.visible)return a;r.push(a),a=n.fill}return!1}function un(t){var e=t.fill,i="dataset";return!1===e?null:(isFinite(e)||(i="boundary"),rn[i](t))}function dn(t){return t&&!t.skip}function hn(t,e,i,n,a){var r;if(n&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r0;--r)ut.canvas.lineTo(t,i[r],i[r-1],!0)}}var cn={id:"filler",afterDatasetsUpdate:function(t,e){var i,n,a,r,o=(t.data.datasets||[]).length,s=e.propagate,l=[];for(n=0;ne?e:t.boxWidth}ot._set("global",{legend:{display:!0,position:"top",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var i=e.datasetIndex,n=this.chart,a=n.getDatasetMeta(i);a.hidden=null===a.hidden?!n.data.datasets[i].hidden:null,n.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data;return ut.isArray(e.datasets)?e.datasets.map(function(e,i){return{text:e.label,fillStyle:ut.isArray(e.backgroundColor)?e.backgroundColor[0]:e.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:e.borderCapStyle,lineDash:e.borderDash,lineDashOffset:e.borderDashOffset,lineJoin:e.borderJoinStyle,lineWidth:e.borderWidth,strokeStyle:e.borderColor,pointStyle:e.pointStyle,datasetIndex:i}},this):[]}}},legendCallback:function(t){var e=[];e.push('
    ');for(var i=0;i'),t.data.datasets[i].label&&e.push(t.data.datasets[i].label),e.push("");return e.push("
"),e.join("")}});var pn=gt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1},beforeUpdate:fn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:fn,beforeSetDimensions:fn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:fn,beforeBuildLabels:fn,buildLabels:function(){var t=this,e=t.options.labels||{},i=ut.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter(function(i){return e.filter(i,t.chart.data)})),t.options.reverse&&i.reverse(),t.legendItems=i},afterBuildLabels:fn,beforeFit:fn,fit:function(){var t=this,e=t.options,i=e.labels,n=e.display,a=t.ctx,r=ut.options._parseFont(i),o=r.size,s=t.legendHitBoxes=[],l=t.minSize,u=t.isHorizontal();if(u?(l.width=t.maxWidth,l.height=n?10:0):(l.width=n?10:0,l.height=t.maxHeight),n)if(a.font=r.string,u){var d=t.lineWidths=[0],h=0;a.textAlign="left",a.textBaseline="top",ut.each(t.legendItems,function(t,e){var n=mn(i,o)+o/2+a.measureText(t.text).width;(0===e||d[d.length-1]+n+i.padding>l.width)&&(h+=o+i.padding,d[d.length-(e>0?0:1)]=i.padding),s[e]={left:0,top:0,width:n,height:o},d[d.length-1]+=n+i.padding}),l.height+=h}else{var c=i.padding,f=t.columnWidths=[],g=i.padding,m=0,p=0,v=o+c;ut.each(t.legendItems,function(t,e){var n=mn(i,o)+o/2+a.measureText(t.text).width;e>0&&p+v>l.height-c&&(g+=m+i.padding,f.push(m),m=0,p=0),m=Math.max(m,n),p+=v,s[e]={left:0,top:0,width:n,height:o}}),g+=m,f.push(m),l.width+=g}t.width=l.width,t.height=l.height},afterFit:fn,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,i=e.labels,n=ot.global,a=n.defaultColor,r=n.elements.line,o=t.width,s=t.lineWidths;if(e.display){var l,u=t.ctx,d=gn(i.fontColor,n.defaultFontColor),h=ut.options._parseFont(i),c=h.size;u.textAlign="left",u.textBaseline="middle",u.lineWidth=.5,u.strokeStyle=d,u.fillStyle=d,u.font=h.string;var f=mn(i,c),g=t.legendHitBoxes,m=t.isHorizontal();l=m?{x:t.left+(o-s[0])/2+i.padding,y:t.top+i.padding,line:0}:{x:t.left+i.padding,y:t.top+i.padding,line:0};var p=c+i.padding;ut.each(t.legendItems,function(n,d){var h=u.measureText(n.text).width,v=f+c/2+h,y=l.x,b=l.y;m?d>0&&y+v+i.padding>t.left+t.minSize.width&&(b=l.y+=p,l.line++,y=l.x=t.left+(o-s[l.line])/2+i.padding):d>0&&b+p>t.top+t.minSize.height&&(y=l.x=y+t.columnWidths[l.line]+i.padding,b=l.y=t.top+i.padding,l.line++),function(t,i,n){if(!(isNaN(f)||f<=0)){u.save();var o=gn(n.lineWidth,r.borderWidth);if(u.fillStyle=gn(n.fillStyle,a),u.lineCap=gn(n.lineCap,r.borderCapStyle),u.lineDashOffset=gn(n.lineDashOffset,r.borderDashOffset),u.lineJoin=gn(n.lineJoin,r.borderJoinStyle),u.lineWidth=o,u.strokeStyle=gn(n.strokeStyle,a),u.setLineDash&&u.setLineDash(gn(n.lineDash,r.borderDash)),e.labels&&e.labels.usePointStyle){var s=f*Math.SQRT2/2,l=t+f/2,d=i+c/2;ut.canvas.drawPoint(u,n.pointStyle,s,l,d)}else 0!==o&&u.strokeRect(t,i,f,c),u.fillRect(t,i,f,c);u.restore()}}(y,b,n),g[d].left=y,g[d].top=b,function(t,e,i,n){var a=c/2,r=f+a+t,o=e+a;u.fillText(i.text,r,o),i.hidden&&(u.beginPath(),u.lineWidth=2,u.moveTo(r,o),u.lineTo(r+n,o),u.stroke())}(y,b,n,h),m?l.x+=v+i.padding:l.y+=p})}},_getLegendItemAt:function(t,e){var i,n,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,i=0;i=(n=a[i]).left&&t<=n.left+n.width&&e>=n.top&&e<=n.top+n.height)return r.legendItems[i];return null},handleEvent:function(t){var e,i=this,n=i.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!n.onHover&&!n.onLeave)return}else{if("click"!==a)return;if(!n.onClick)return}e=i._getLegendItemAt(t.x,t.y),"click"===a?e&&n.onClick&&n.onClick.call(i,t.native,e):(n.onLeave&&e!==i._hoveredItem&&(i._hoveredItem&&n.onLeave.call(i,t.native,i._hoveredItem),i._hoveredItem=e),n.onHover&&e&&n.onHover.call(i,t.native,e))}});function vn(t,e){var i=new pn({ctx:t.ctx,options:e,chart:t});xe.configure(t,i,e),xe.addBox(t,i),t.legend=i}var yn={id:"legend",_element:pn,beforeInit:function(t){var e=t.options.legend;e&&vn(t,e)},beforeUpdate:function(t){var e=t.options.legend,i=t.legend;e?(ut.mergeIf(e,ot.global.legend),i?(xe.configure(t,i,e),i.options=e):vn(t,e)):i&&(xe.removeBox(t,i),delete t.legend)},afterEvent:function(t,e){var i=t.legend;i&&i.handleEvent(e)}},bn=ut.noop;ot._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var xn=gt.extend({initialize:function(t){ut.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:bn,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:bn,beforeSetDimensions:bn,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:bn,beforeBuildLabels:bn,buildLabels:bn,afterBuildLabels:bn,beforeFit:bn,fit:function(){var t=this,e=t.options,i=e.display,n=t.minSize,a=ut.isArray(e.text)?e.text.length:1,r=ut.options._parseFont(e),o=i?a*r.lineHeight+2*e.padding:0;t.isHorizontal()?(n.width=t.maxWidth,n.height=o):(n.width=o,n.height=t.maxHeight),t.width=n.width,t.height=n.height},afterFit:bn,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,i=t.options;if(i.display){var n,a,r,o=ut.options._parseFont(i),s=o.lineHeight,l=s/2+i.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=ut.valueOrDefault(i.fontColor,ot.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,n=f-h):(a="left"===i.position?h+l:f-l,r=d+(c-d)/2,n=c-d,u=Math.PI*("left"===i.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=i.text;if(ut.isArray(g))for(var m=0,p=0;p=0;n--){var a=t[n];if(e(a))return a}},ut.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},ut.almostEquals=function(t,e,i){return Math.abs(t-e)t},ut.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},ut.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},ut.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0==(t=+t)||isNaN(t)?t:t>0?1:-1},ut.log10=Math.log10?function(t){return Math.log10(t)}:function(t){var e=Math.log(t)*Math.LOG10E,i=Math.round(e);return t===Math.pow(10,i)?i:e},ut.toRadians=function(t){return t*(Math.PI/180)},ut.toDegrees=function(t){return t*(180/Math.PI)},ut._decimalPlaces=function(t){if(ut.isFinite(t)){for(var e=1,i=0;Math.round(t*e)/e!==t;)e*=10,i++;return i}},ut.getAngleFromPoint=function(t,e){var i=e.x-t.x,n=e.y-t.y,a=Math.sqrt(i*i+n*n),r=Math.atan2(n,i);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},ut.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},ut.aliasPixel=function(t){return t%2==0?0:.5},ut._alignPixel=function(t,e,i){var n=t.currentDevicePixelRatio,a=i/2;return Math.round((e-a)*n)/n+a},ut.splineCurve=function(t,e,i,n){var a=t.skip?e:t,r=e,o=i.skip?e:i,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=n*(u=isNaN(u)?0:u),c=n*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},ut.EPSILON=Number.EPSILON||1e-14,ut.splineCurveMonotone=function(t){var e,i,n,a,r,o,s,l,u,d=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),h=d.length;for(e=0;e0?d[e-1]:null,(a=e0?d[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},ut.previousItem=function(t,e,i){return i?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},ut.niceNum=function(t,e){var i=Math.floor(ut.log10(t)),n=t/Math.pow(10,i);return(e?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10)*Math.pow(10,i)},ut.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},ut.getRelativePosition=function(t,e){var i,n,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(i=s[0].clientX,n=s[0].clientY):(i=a.clientX,n=a.clientY);var l=parseFloat(ut.getStyle(r,"padding-left")),u=parseFloat(ut.getStyle(r,"padding-top")),d=parseFloat(ut.getStyle(r,"padding-right")),h=parseFloat(ut.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:i=Math.round((i-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:n=Math.round((n-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},ut.getConstraintWidth=function(t){return i(t,"max-width","clientWidth")},ut.getConstraintHeight=function(t){return i(t,"max-height","clientHeight")},ut._calculatePadding=function(t,e,i){return(e=ut.getStyle(t,e)).indexOf("%")>-1?i*parseInt(e,10)/100:parseInt(e,10)},ut._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},ut.getMaximumWidth=function(t){var e=ut._getParentNode(t);if(!e)return t.clientWidth;var i=e.clientWidth,n=i-ut._calculatePadding(e,"padding-left",i)-ut._calculatePadding(e,"padding-right",i),a=ut.getConstraintWidth(t);return isNaN(a)?n:Math.min(n,a)},ut.getMaximumHeight=function(t){var e=ut._getParentNode(t);if(!e)return t.clientHeight;var i=e.clientHeight,n=i-ut._calculatePadding(e,"padding-top",i)-ut._calculatePadding(e,"padding-bottom",i),a=ut.getConstraintHeight(t);return isNaN(a)?n:Math.min(n,a)},ut.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},ut.retinaScale=function(t,e){var i=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==i){var n=t.canvas,a=t.height,r=t.width;n.height=a*i,n.width=r*i,t.ctx.scale(i,i),n.style.height||n.style.width||(n.style.height=a+"px",n.style.width=r+"px")}},ut.fontString=function(t,e,i){return e+" "+t+"px "+i},ut.longestText=function(t,e,i,n){var a=(n=n||{}).data=n.data||{},r=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(a=n.data={},r=n.garbageCollect=[],n.font=e),t.font=e;var o=0;ut.each(i,function(e){null!=e&&!0!==ut.isArray(e)?o=ut.measureText(t,a,r,o,e):ut.isArray(e)&&ut.each(e,function(e){null==e||ut.isArray(e)||(o=ut.measureText(t,a,r,o,e))})});var s=r.length/2;if(s>i.length){for(var l=0;ln&&(n=r),n},ut.numberOfLabelLines=function(t){var e=1;return ut.each(t,function(t){ut.isArray(t)&&t.length>e&&(e=t.length)}),e},ut.color=G?function(t){return t instanceof CanvasGradient&&(t=ot.global.defaultColor),G(t)}:function(t){return console.error("Color.js not found!"),t},ut.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:ut.color(t).saturate(.5).darken(.1).rgbString()}}(),ai._adapters=si,ai.Animation=pt,ai.animationService=vt,ai.controllers=ue,ai.DatasetController=kt,ai.defaults=ot,ai.Element=gt,ai.elements=Nt,ai.Interaction=pe,ai.layouts=xe,ai.platform=Ve,ai.plugins=He,ai.Scale=fi,ai.scaleService=Ee,ai.Ticks=li,ai.Tooltip=Je,ai.helpers.each(en,function(t,e){ai.scaleService.registerScaleType(e,t,t._defaults)}),kn)kn.hasOwnProperty(Dn)&&ai.plugins.register(kn[Dn]);ai.platform.initialize();var Cn=ai;return"undefined"!=typeof window&&(window.Chart=ai),ai.Chart=ai,ai.Legend=kn.legend._element,ai.Title=kn.title._element,ai.pluginService=ai.plugins,ai.PluginBase=ai.Element.extend({}),ai.canvasHelpers=ai.helpers.canvas,ai.layoutService=ai.layouts,ai.LinearScaleBase=yi,ai.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],function(t){ai[t]=function(e,i){return new ai(e,ai.helpers.merge(i||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}}),Cn}); diff --git a/flask__webservers/chart_js/static/chart_js_2.9.3/Chart.bundle.min.js b/flask__webservers/chart_js/static/chart_js_2.9.3/Chart.bundle.min.js new file mode 100644 index 000000000..55d9eb03f --- /dev/null +++ b/flask__webservers/chart_js/static/chart_js_2.9.3/Chart.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Chart.js v2.9.3 + * https://www.chartjs.org + * (c) 2019 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Chart=e()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},i=e((function(t){var e={};for(var i in n)n.hasOwnProperty(i)&&(e[n[i]]=i);var a=t.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var r in a)if(a.hasOwnProperty(r)){if(!("channels"in a[r]))throw new Error("missing channels property: "+r);if(!("labels"in a[r]))throw new Error("missing channel labels property: "+r);if(a[r].labels.length!==a[r].channels)throw new Error("channel and label counts mismatch: "+r);var o=a[r].channels,s=a[r].labels;delete a[r].channels,delete a[r].labels,Object.defineProperty(a[r],"channels",{value:o}),Object.defineProperty(a[r],"labels",{value:s})}a.rgb.hsl=function(t){var e,n,i=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o;return s===o?e=0:i===s?e=(a-r)/l:a===s?e=2+(r-i)/l:r===s&&(e=4+(i-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),n=(o+s)/2,[e,100*(s===o?0:n<=.5?l/(s+o):l/(2-s-o)),100*n]},a.rgb.hsv=function(t){var e,n,i,a,r,o=t[0]/255,s=t[1]/255,l=t[2]/255,u=Math.max(o,s,l),d=u-Math.min(o,s,l),h=function(t){return(u-t)/6/d+.5};return 0===d?a=r=0:(r=d/u,e=h(o),n=h(s),i=h(l),o===u?a=i-n:s===u?a=1/3+e-i:l===u&&(a=2/3+n-e),a<0?a+=1:a>1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a1&&(e=Array.prototype.slice.call(arguments));var n=t(e);if("object"==typeof n)for(var i=n.length,a=0;a1&&(e=Array.prototype.slice.call(arguments)),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(i)}))}));var l=s,u={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},d={getRgba:h,getHsla:c,getRgb:function(t){var e=h(t);return e&&e.slice(0,3)},getHsl:function(t){var e=c(t);return e&&e.slice(0,3)},getHwb:f,getAlpha:function(t){var e=h(t);if(e)return e[3];if(e=c(t))return e[3];if(e=f(t))return e[3]},hexString:function(t,e){e=void 0!==e&&3===t.length?e:t[3];return"#"+b(t[0])+b(t[1])+b(t[2])+(e>=0&&e<1?b(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return g(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:g,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return m(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"},percentaString:m,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return p(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:p,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return y[t.slice(0,3)]}};function h(t){if(t){var e=[0,0,0],n=1,i=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(i){a=(i=i[1])[3];for(var r=0;rn?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=t,i=void 0===e?.5:e,a=2*i-1,r=this.alpha()-n.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*n.red(),o*this.green()+s*n.green(),o*this.blue()+s*n.blue()).alpha(this.alpha()*i+n.alpha()*(1-i))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new _,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},_.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},_.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},_.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i=0;a--)e.call(n,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),-i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n))},easeOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),i*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/n)+1)},easeInOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:2==(t/=.5)?1:(n||(n=.45),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),t<1?i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*-.5:i*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-D.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*D.easeInBounce(2*t):.5*D.easeOutBounce(2*t-1)+.5}},C={effects:D};S.easingEffects=D;var P=Math.PI,T=P/180,O=2*P,A=P/2,F=P/4,I=2*P/3,L={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,n,i,a,r){if(r){var o=Math.min(r,a/2,i/2),s=e+o,l=n+o,u=e+i-o,d=n+a-o;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,n,i,a=this.animations,r=0;r=n?(H.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},Q=H.options.resolve,tt=["push","pop","shift","splice","unshift"];function et(t,e){var n=t._chartjs;if(n){var i=n.listeners,a=i.indexOf(e);-1!==a&&i.splice(a,1),i.length>0||(tt.forEach((function(e){delete t[e]})),delete t._chartjs)}}var nt=function(t,e){this.initialize(t,e)};H.extend(nt.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements(),n._type=n.getMeta().type},updateIndex:function(t){this.index=t},linkScales:function(){var t=this.getMeta(),e=this.chart,n=e.scales,i=this.getDataset(),a=e.options.scales;null!==t.xAxisID&&t.xAxisID in n&&!i.xAxisID||(t.xAxisID=i.xAxisID||a.xAxes[0].id),null!==t.yAxisID&&t.yAxisID in n&&!i.yAxisID||(t.yAxisID=i.yAxisID||a.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&et(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,n=this.getMeta(),i=this.getDataset().data||[],a=n.data;for(t=0,e=i.length;tn&&this.insertElements(n,i-n)},insertElements:function(t,e){for(var n=0;na?(r=a/e.innerRadius,t.arc(o,s,e.innerRadius-a,i+r,n-r,!0)):t.arc(o,s,a,i+Math.PI/2,n-Math.PI/2),t.closePath(),t.clip()}function ot(t,e,n){var i="inner"===e.borderAlign;i?(t.lineWidth=2*e.borderWidth,t.lineJoin="round"):(t.lineWidth=e.borderWidth,t.lineJoin="bevel"),n.fullCircles&&function(t,e,n,i){var a,r=n.endAngle;for(i&&(n.endAngle=n.startAngle+at,rt(t,n),n.endAngle=r,n.endAngle===n.startAngle&&n.fullCircles&&(n.endAngle+=at,n.fullCircles--)),t.beginPath(),t.arc(n.x,n.y,n.innerRadius,n.startAngle+at,n.startAngle,!0),a=0;as;)a-=at;for(;a=o&&a<=s,u=r>=n.innerRadius&&r<=n.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t,e=this._chart.ctx,n=this._view,i="inner"===n.borderAlign?.33:0,a={x:n.x,y:n.y,innerRadius:n.innerRadius,outerRadius:Math.max(n.outerRadius-i,0),pixelMargin:i,startAngle:n.startAngle,endAngle:n.endAngle,fullCircles:Math.floor(n.circumference/at)};if(e.save(),e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+at,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),t=0;tt.x&&(e=bt(e,"left","right")):t.basen?n:i,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>n?n:r,l:l.left||o<0?0:o>e?e:o}}function xt(t,e,n){var i=null===e,a=null===n,r=!(!t||i&&a)&&vt(t);return r&&(i||e>=r.left&&e<=r.right)&&(a||n>=r.top&&n<=r.bottom)}W._set("global",{elements:{rectangle:{backgroundColor:mt,borderColor:mt,borderSkipped:"bottom",borderWidth:0}}});var _t=$.extend({_type:"rectangle",draw:function(){var t=this._chart.ctx,e=this._view,n=function(t){var e=vt(t),n=e.right-e.left,i=e.bottom-e.top,a=yt(t,n/2,i/2);return{outer:{x:e.left,y:e.top,w:n,h:i},inner:{x:e.left+a.l,y:e.top+a.t,w:n-a.l-a.r,h:i-a.t-a.b}}}(e),i=n.outer,a=n.inner;t.fillStyle=e.backgroundColor,t.fillRect(i.x,i.y,i.w,i.h),i.w===a.w&&i.h===a.h||(t.save(),t.beginPath(),t.rect(i.x,i.y,i.w,i.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return xt(this._view,t,e)},inLabelRange:function(t,e){var n=this._view;return pt(n)?xt(n,t,null):xt(n,null,e)},inXRange:function(t){return xt(this._view,t,null)},inYRange:function(t){return xt(this._view,null,t)},getCenterPoint:function(){var t,e,n=this._view;return pt(n)?(t=n.x,e=(n.y+n.base)/2):(t=(n.x+n.base)/2,e=n.y),{x:t,y:e}},getArea:function(){var t=this._view;return pt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),wt={},kt=st,Mt=dt,St=gt,Dt=_t;wt.Arc=kt,wt.Line=Mt,wt.Point=St,wt.Rectangle=Dt;var Ct=H._deprecated,Pt=H.valueOrDefault;function Tt(t,e,n){var i,a,r=n.barThickness,o=e.stackCount,s=e.pixels[t],l=H.isNullOrUndef(r)?function(t,e){var n,i,a,r,o=t._length;for(a=1,r=e.length;a0?Math.min(o,Math.abs(i-n)):o,n=i;return o}(e.scale,e.pixels):-1;return H.isNullOrUndef(r)?(i=l*n.categoryPercentage,a=n.barPercentage):(i=r*o,a=1),{chunk:i/o,ratio:a,start:s-i/2}}W._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),W._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var Ot=it.extend({dataElementType:wt.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,e,n=this;it.prototype.initialize.apply(n,arguments),(t=n.getMeta()).stack=n.getDataset().stack,t.bar=!0,e=n._getIndexScale().options,Ct("bar chart",e.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Ct("bar chart",e.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Ct("bar chart",e.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Ct("bar chart",n._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Ct("bar chart",e.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(t){var e,n,i=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,n=i.length;e=0&&m.min>=0?m.min:m.max,x=void 0===m.start?m.end:m.max>=0&&m.min>=0?m.max-m.min:m.min-m.max,_=g.length;if(v||void 0===v&&void 0!==b)for(i=0;i<_&&(a=g[i]).index!==t;++i)a.stack===b&&(r=void 0===(u=h._parseValue(f[a.index].data[e])).start?u.end:u.min>=0&&u.max>=0?u.max:u.min,(m.min<0&&r<0||m.max>=0&&r>0)&&(y+=r));return o=h.getPixelForValue(y),l=(s=h.getPixelForValue(y+x))-o,void 0!==p&&Math.abs(l)=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t=Rt?-Nt:b<-Rt?Nt:0)+p,x=Math.cos(b),_=Math.sin(b),w=Math.cos(y),k=Math.sin(y),M=b<=0&&y>=0||y>=Nt,S=b<=Wt&&y>=Wt||y>=Nt+Wt,D=b<=-Wt&&y>=-Wt||y>=Rt+Wt,C=b===-Rt||y>=Rt?-1:Math.min(x,x*m,w,w*m),P=D?-1:Math.min(_,_*m,k,k*m),T=M?1:Math.max(x,x*m,w,w*m),O=S?1:Math.max(_,_*m,k,k*m);u=(T-C)/2,d=(O-P)/2,h=-(T+C)/2,c=-(O+P)/2}for(i=0,a=g.length;i0&&!isNaN(t)?Nt*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,n,i,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,n=d.data.datasets.length;e(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Lt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Lt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Lt(n.hoverBorderWidth,n.borderWidth)},_getRingWeightOffset:function(t){for(var e=0,n=0;n0&&Ht(l[t-1]._model,s)&&(n.controlPointPreviousX=u(n.controlPointPreviousX,s.left,s.right),n.controlPointPreviousY=u(n.controlPointPreviousY,s.top,s.bottom)),t0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return ae(t,e,{intersect:!1})},point:function(t,e){return ee(t,Qt(e,t))},nearest:function(t,e,n){var i=Qt(e,t);n.axis=n.axis||"xy";var a=ie(n.axis);return ne(t,i,n.intersect,a)},x:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inXRange(i.x)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a},y:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inYRange(i.y)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a}}},oe=H.extend;function se(t,e){return H.where(t,(function(t){return t.pos===e}))}function le(t,e){return t.sort((function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i.index-a.index:i.weight-a.weight}))}function ue(t,e,n,i){return Math.max(t[n],e[n])+Math.max(t[i],e[i])}function de(t,e,n){var i,a,r=n.box,o=t.maxPadding;if(n.size&&(t[n.pos]-=n.size),n.size=n.horizontal?r.height:r.width,t[n.pos]+=n.size,r.getPadding){var s=r.getPadding();o.top=Math.max(o.top,s.top),o.left=Math.max(o.left,s.left),o.bottom=Math.max(o.bottom,s.bottom),o.right=Math.max(o.right,s.right)}if(i=e.outerWidth-ue(o,t,"left","right"),a=e.outerHeight-ue(o,t,"top","bottom"),i!==t.w||a!==t.h)return t.w=i,t.h=a,n.horizontal?i!==t.w:a!==t.h}function he(t,e){var n=e.maxPadding;function i(t){var i={left:0,top:0,right:0,bottom:0};return t.forEach((function(t){i[t]=Math.max(e[t],n[t])})),i}return i(t?["left","right"]:["top","bottom"])}function ce(t,e,n){var i,a,r,o,s,l,u=[];for(i=0,a=t.length;idiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ge.default||ge,ve="$chartjs",be="chartjs-size-monitor",ye="chartjs-render-monitor",xe="chartjs-render-animation",_e=["animationstart","webkitAnimationStart"],we={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function ke(t,e){var n=H.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}var Me=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Se(t,e,n){t.addEventListener(e,n,Me)}function De(t,e,n){t.removeEventListener(e,n,Me)}function Ce(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function Pe(t){var e=document.createElement("div");return e.className=t||"",e}function Te(t,e,n){var i,a,r,o,s=t[ve]||(t[ve]={}),l=s.resizer=function(t){var e=Pe(be),n=Pe(be+"-expand"),i=Pe(be+"-shrink");n.appendChild(Pe()),i.appendChild(Pe()),e.appendChild(n),e.appendChild(i),e._reset=function(){n.scrollLeft=1e6,n.scrollTop=1e6,i.scrollLeft=1e6,i.scrollTop=1e6};var a=function(){e._reset(),t()};return Se(n,"scroll",a.bind(n,"expand")),Se(i,"scroll",a.bind(i,"shrink")),e}((i=function(){if(s.resizer){var i=n.options.maintainAspectRatio&&t.parentNode,a=i?i.clientWidth:0;e(Ce("resize",n)),i&&i.clientWidth0){var r=t[0];r.label?n=r.label:r.xLabel?n=r.xLabel:a>0&&r.index-1?t.split("\n"):t}function Ve(t){var e=W.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,rtl:t.rtl,textDirection:t.textDirection,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Ne(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Ne(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Ne(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Ne(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Ne(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Ne(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Ne(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Ne(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Ne(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function He(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function Be(t){return ze([],Ee(t))}var je=$.extend({initialize:function(){this._model=Ve(this._options),this._lastActive=[]},getTitle:function(){var t=this,e=t._options,n=e.callbacks,i=n.beforeTitle.apply(t,arguments),a=n.title.apply(t,arguments),r=n.afterTitle.apply(t,arguments),o=[];return o=ze(o,Ee(i)),o=ze(o,Ee(a)),o=ze(o,Ee(r))},getBeforeBody:function(){return Be(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var n=this,i=n._options.callbacks,a=[];return H.each(t,(function(t){var r={before:[],lines:[],after:[]};ze(r.before,Ee(i.beforeLabel.call(n,t,e))),ze(r.lines,i.label.call(n,t,e)),ze(r.after,Ee(i.afterLabel.call(n,t,e))),a.push(r)})),a},getAfterBody:function(){return Be(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this,e=t._options.callbacks,n=e.beforeFooter.apply(t,arguments),i=e.footer.apply(t,arguments),a=e.afterFooter.apply(t,arguments),r=[];return r=ze(r,Ee(n)),r=ze(r,Ee(i)),r=ze(r,Ee(a))},update:function(t){var e,n,i,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=Ve(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},y={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],w=[];x=Ye[c.position].call(h,m,h._eventPosition);var k=[];for(e=0,n=m.length;ei.width&&(a=i.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,y,v=function(t,e){var n,i,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(n=function(t){return t<=c},i=function(t){return t>c}):(n=function(t){return t<=e.width/2},i=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},n(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):i(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,y),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=y.width,g.height=y.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var n=this._chart.ctx,i=this._view,a=this.getCaretPosition(t,e,i);n.lineTo(a.x1,a.y1),n.lineTo(a.x2,a.y2),n.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,n){var i,a,r,o,s,l,u=n.caretSize,d=n.cornerRadius,h=n.xAlign,c=n.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(i=f)-u,r=i,o=s+u,l=s-u):(a=(i=f+m)+u,r=i,o=s-u,l=s+u);else if("left"===h?(i=(a=f+d+u)-u,r=a+u):"right"===h?(i=(a=f+m-d-u)-u,r=a+u):(i=(a=n.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=i,i=v}return{x1:i,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,n){var i,a,r,o=e.title,s=o.length;if(s){var l=We(e.rtl,e.x,e.width);for(t.x=He(e,e._titleAlign),n.textAlign=l.textAlign(e._titleAlign),n.textBaseline="middle",i=e.titleFontSize,a=e.titleSpacing,n.fillStyle=e.titleFontColor,n.font=H.fontString(i,e._titleFontStyle,e._titleFontFamily),r=0;r0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,H.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),H.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!H.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ue=Ye,Ge=je;Ge.positioners=Ue;var qe=H.valueOrDefault;function Ze(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?H.merge(e[t][a],[Re.getScaleDefaults(r),o]):H.merge(e[t][a],o)}else H._merger(t,e,n,i)}})}function $e(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||{},r=n[t];"scales"===t?e[t]=Ze(a,r):"scale"===t?e[t]=H.merge(a,[Re.getScaleDefaults(r.type),r]):H._merger(t,e,n,i)}})}function Xe(t){var e=t.options;H.each(t.scales,(function(e){me.removeBox(t,e)})),e=$e(W.global,W[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Ke(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(H.findIndex(t,a)>=0);return i}function Je(t){return"top"===t||"bottom"===t}function Qe(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}W._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var tn=function(t,e){return this.construct(t,e),this};H.extend(tn.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=$e(W.global,W[t.type],t.options||{}),t}(e);var i=Ie.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=H.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,tn.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Le.notify(t,"beforeInit"),H.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Le.notify(t,"afterInit"),t},clear:function(){return H.canvas.clear(this),this},stop:function(){return J.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(H.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:H.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",H.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Le.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;H.each(e.xAxes,(function(t,n){t.id||(t.id=Ke(e.xAxes,"x-axis-",n))})),H.each(e.yAxes,(function(t,n){t.id||(t.id=Ke(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),H.each(i,(function(e){var i=e.options,r=i.id,o=qe(i.type,e.dtype);Je(i.position)!==Je(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Re.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),H.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Re.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t=0;--n)this.drawDataset(e[n],t);Le.notify(this,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n={meta:t,index:t.index,easingValue:e};!1!==Le.notify(this,"beforeDatasetDraw",[n])&&(t.controller.draw(e),Le.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,n={tooltip:e,easingValue:t};!1!==Le.notify(this,"beforeTooltipDraw",[n])&&(e.draw(),Le.notify(this,"afterTooltipDraw",[n]))},getElementAtEvent:function(t){return re.modes.single(this,t)},getElementsAtEvent:function(t){return re.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return re.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,n){var i=re.modes[e];return"function"==typeof i?i(this,t,n):[]},getDatasetAtEvent:function(t){return re.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var n=e._meta[this.id];return n||(n=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e.order||0,index:t}),n},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e3?n[2]-n[1]:n[1]-n[0];Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));var a=H.log10(Math.abs(i)),r="";if(0!==t)if(Math.max(Math.abs(n[0]),Math.abs(n[n.length-1]))<1e-4){var o=H.log10(Math.abs(t)),s=Math.floor(o)-Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toExponential(s)}else{var l=-1*Math.floor(a);l=Math.max(Math.min(l,20),0),r=t.toFixed(l)}else r="0";return r},logarithmic:function(t,e,n){var i=t/Math.pow(10,Math.floor(H.log10(t)));return 0===t?"0":1===i||2===i||5===i||0===e||e===n.length-1?t.toExponential():""}}},sn=H.isArray,ln=H.isNullOrUndef,un=H.valueOrDefault,dn=H.valueAtIndexOrDefault;function hn(t,e,n){var i,a=t.getTicks().length,r=Math.min(e,a-1),o=t.getPixelForTick(r),s=t._startPixel,l=t._endPixel;if(!(n&&(i=1===a?Math.max(o-s,l-o):0===e?(t.getPixelForTick(1)-o)/2:(o-t.getPixelForTick(r-1))/2,(o+=rl+1e-6)))return o}function cn(t,e,n,i){var a,r,o,s,l,u,d,h,c,f,g,m,p,v=n.length,b=[],y=[],x=[];for(a=0;ae){for(n=0;n=c||d<=1||!s.isHorizontal()?s.labelRotation=h:(e=(t=s._getLabelSizes()).widest.width,n=t.highest.height-t.highest.offset,i=Math.min(s.maxWidth,s.chart.width-e),e+6>(a=l.offset?s.maxWidth/d:i/(d-1))&&(a=i/(d-(l.offset?.5:1)),r=s.maxHeight-fn(l.gridLines)-u.padding-gn(l.scaleLabel),o=Math.sqrt(e*e+n*n),f=H.toDegrees(Math.min(Math.asin(Math.min((t.highest.height+6)/a,1)),Math.asin(Math.min(r/o,1))-Math.asin(n/o))),f=Math.max(h,Math.min(c,f))),s.labelRotation=f)},afterCalculateTickRotation:function(){H.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){H.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},n=t.chart,i=t.options,a=i.ticks,r=i.scaleLabel,o=i.gridLines,s=t._isVisible(),l="bottom"===i.position,u=t.isHorizontal();if(u?e.width=t.maxWidth:s&&(e.width=fn(o)+gn(r)),u?s&&(e.height=fn(o)+gn(r)):e.height=t.maxHeight,a.display&&s){var d=pn(a),h=t._getLabelSizes(),c=h.first,f=h.last,g=h.widest,m=h.highest,p=.4*d.minor.lineHeight,v=a.padding;if(u){var b=0!==t.labelRotation,y=H.toRadians(t.labelRotation),x=Math.cos(y),_=Math.sin(y),w=_*g.width+x*(m.height-(b?m.offset:0))+(b?0:p);e.height=Math.min(t.maxHeight,e.height+w+v);var k,M,S=t.getPixelForTick(0)-t.left,D=t.right-t.getPixelForTick(t.getTicks().length-1);b?(k=l?x*c.width+_*c.offset:_*(c.height-c.offset),M=l?_*(f.height-f.offset):x*f.width+_*f.offset):(k=c.width/2,M=f.width/2),t.paddingLeft=Math.max((k-S)*t.width/(t.width-S),0)+3,t.paddingRight=Math.max((M-D)*t.width/(t.width-D),0)+3}else{var C=a.mirror?0:g.width+v+p;e.width=Math.min(t.maxWidth,e.width+C),t.paddingTop=c.height/2,t.paddingBottom=f.height/2}}t.handleMargins(),u?(t.width=t._length=n.width-t.margins.left-t.margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=n.height-t.margins.top-t.margins.bottom)},handleMargins:function(){var t=this;t.margins&&(t.margins.left=Math.max(t.paddingLeft,t.margins.left),t.margins.top=Math.max(t.paddingTop,t.margins.top),t.margins.right=Math.max(t.paddingRight,t.margins.right),t.margins.bottom=Math.max(t.paddingBottom,t.margins.bottom))},afterFit:function(){H.callback(this.options.afterFit,[this])},isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ln(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},_convertTicksToLabels:function(t){var e,n,i,a=this;for(a.ticks=t.map((function(t){return t.value})),a.beforeTickToLabelConversion(),e=a.convertTicksToLabels(t)||a.ticks,a.afterTickToLabelConversion(),n=0,i=t.length;nn-1?null:this.getPixelForDecimal(t*i+(e?i/2:0))},getPixelForDecimal:function(t){return this._reversePixels&&(t=1-t),this._startPixel+t*this._length},getDecimalForPixel:function(t){var e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,n,i,a,r=this.options.ticks,o=this._length,s=r.maxTicksLimit||o/this._tickSize()+1,l=r.major.enabled?function(t){var e,n,i=[];for(e=0,n=t.length;es)return function(t,e,n){var i,a,r=0,o=e[0];for(n=Math.ceil(n),i=0;iu)return r;return Math.max(u,1)}(l,t,0,s),u>0){for(e=0,n=u-1;e1?(h-d)/(u-1):null,bn(t,i,H.isNullOrUndef(a)?0:d-a,d),bn(t,i,h,H.isNullOrUndef(a)?t.length:h+a),vn(t)}return bn(t,i),vn(t)},_tickSize:function(){var t=this.options.ticks,e=H.toRadians(this.labelRotation),n=Math.abs(Math.cos(e)),i=Math.abs(Math.sin(e)),a=this._getLabelSizes(),r=t.autoSkipPadding||0,o=a?a.widest.width+r:0,s=a?a.highest.height+r:0;return this.isHorizontal()?s*n>o*i?o/n:s/i:s*i=0&&(o=t),void 0!==r&&(t=n.indexOf(r))>=0&&(s=t),e.minIndex=o,e.maxIndex=s,e.min=n[o],e.max=n[s]},buildTicks:function(){var t=this._getLabels(),e=this.minIndex,n=this.maxIndex;this.ticks=0===e&&n===t.length-1?t:t.slice(e,n+1)},getLabelForIndex:function(t,e){var n=this.chart;return n.getDatasetMeta(e).controller._getValueScaleId()===this.id?this.getRightValue(n.data.datasets[e].data[t]):this._getLabels()[t]},_configure:function(){var t=this,e=t.options.offset,n=t.ticks;xn.prototype._configure.call(t),t.isHorizontal()||(t._reversePixels=!t._reversePixels),n&&(t._startValue=t.minIndex-(e?.5:0),t._valueRange=Math.max(n.length-(e?0:1),1))},getPixelForValue:function(t,e,n){var i,a,r,o=this;return _n(e)||_n(n)||(t=o.chart.data.datasets[n].data[e]),_n(t)||(i=o.isHorizontal()?t.x:t.y),(void 0!==i||void 0!==t&&isNaN(e))&&(a=o._getLabels(),t=H.valueOrDefault(i,t),e=-1!==(r=a.indexOf(t))?r:e,isNaN(e)&&(e=t)),o.getPixelForDecimal((e-o._startValue)/o._valueRange)},getPixelForTick:function(t){var e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t],t+this.minIndex)},getValueForPixel:function(t){var e=Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange);return Math.min(Math.max(e,0),this.ticks.length-1)},getBasePixel:function(){return this.bottom}}),kn={position:"bottom"};wn._defaults=kn;var Mn=H.noop,Sn=H.isNullOrUndef;var Dn=xn.extend({getRightValue:function(t){return"string"==typeof t?+t:xn.prototype.getRightValue.call(this,t)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var n=H.sign(t.min),i=H.sign(t.max);n<0&&i<0?t.max=0:n>0&&i>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,n=e.stepSize,i=e.maxTicksLimit;return n?t=Math.ceil(this.max/n)-Math.floor(this.min/n)+1:(t=this._computeTickLimit(),i=i||11),i&&(t=Math.min(i,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Mn,buildTicks:function(){var t=this,e=t.options.ticks,n=t.getTickLimit(),i={maxTicks:n=Math.max(2,n),min:e.min,max:e.max,precision:e.precision,stepSize:H.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var n,i,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=H.niceNum((g-f)/u/l)*l;if(m<1e-14&&Sn(d)&&Sn(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=H.niceNum(r*m/u/l)*l),s||Sn(c)?n=Math.pow(10,H._decimalPlaces(m)):(n=Math.pow(10,c),m=Math.ceil(m*n)/n),i=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!Sn(d)&&H.almostWhole(d/m,m/1e3)&&(i=d),!Sn(h)&&H.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-i)/m,r=H.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),i=Math.round(i*n)/n,a=Math.round(a*n)/n,o.push(Sn(d)?i:d);for(var p=1;pe.length-1?null:this.getPixelForValue(e[t])}}),An=Cn;On._defaults=An;var Fn=H.valueOrDefault,In=H.math.log10;var Ln={position:"left",ticks:{callback:on.formatters.logarithmic}};function Rn(t,e){return H.isFinite(t)&&t>=0?t:e}var Nn=xn.extend({determineDataLimits:function(){var t,e,n,i,a,r,o=this,s=o.options,l=o.chart,u=l.data.datasets,d=o.isHorizontal();function h(t){return d?t.xAxisID===o.id:t.yAxisID===o.id}o.min=Number.POSITIVE_INFINITY,o.max=Number.NEGATIVE_INFINITY,o.minNotZero=Number.POSITIVE_INFINITY;var c=s.stacked;if(void 0===c)for(t=0;t0){var e=H.min(t),n=H.max(t);o.min=Math.min(o.min,e),o.max=Math.max(o.max,n)}}))}else for(t=0;t0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(In(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,n=!t.isHorizontal(),i={min:Rn(e.min),max:Rn(e.max)},a=t.ticks=function(t,e){var n,i,a=[],r=Fn(t.min,Math.pow(10,Math.floor(In(e.min)))),o=Math.floor(In(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(n=Math.floor(In(e.minNotZero)),i=Math.floor(e.minNotZero/Math.pow(10,n)),a.push(r),r=i*Math.pow(10,n)):(n=Math.floor(In(r)),i=Math.floor(r/Math.pow(10,n)));var l=n<0?Math.pow(10,Math.abs(n)):1;do{a.push(r),10===++i&&(i=1,l=++n>=0?1:l),r=Math.round(i*Math.pow(10,n)*l)/l}while(ne.length-1?null:this.getPixelForValue(e[t])},_getFirstTickValue:function(t){var e=Math.floor(In(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},_configure:function(){var t=this,e=t.min,n=0;xn.prototype._configure.call(t),0===e&&(e=t._getFirstTickValue(t.minNotZero),n=Fn(t.options.ticks.fontSize,W.global.defaultFontSize)/t._length),t._startValue=In(e),t._valueOffset=n,t._valueRange=(In(t.max)-In(e))/(1-n)},getPixelForValue:function(t){var e=this,n=0;return(t=+e.getRightValue(t))>e.min&&t>0&&(n=(In(t)-e._startValue)/e._valueRange+e._valueOffset),e.getPixelForDecimal(n)},getValueForPixel:function(t){var e=this,n=e.getDecimalForPixel(t);return 0===n&&0===e.min?0:Math.pow(10,e._startValue+(n-e._valueOffset)*e._valueRange)}}),Wn=Ln;Nn._defaults=Wn;var Yn=H.valueOrDefault,zn=H.valueAtIndexOrDefault,En=H.options.resolve,Vn={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:on.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function Hn(t){var e=t.ticks;return e.display&&t.display?Yn(e.fontSize,W.global.defaultFontSize)+2*e.backdropPaddingY:0}function Bn(t,e,n,i,a){return t===i||t===a?{start:e-n/2,end:e+n/2}:ta?{start:e-n,end:e}:{start:e,end:e+n}}function jn(t){return 0===t||180===t?"center":t<180?"left":"right"}function Un(t,e,n,i){var a,r,o=n.y+i/2;if(H.isArray(e))for(a=0,r=e.length;a270||t<90)&&(n.y-=e.h)}function qn(t){return H.isNumber(t)?t:0}var Zn=Dn.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Hn(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,n=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;H.each(e.data.datasets,(function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);H.each(a.data,(function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(n=Math.min(r,n),i=Math.max(r,i))}))}})),t.min=n===Number.POSITIVE_INFINITY?0:n,t.max=i===Number.NEGATIVE_INFINITY?0:i,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Hn(this.options))},convertTicksToLabels:function(){var t=this;Dn.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map((function(){var e=H.callback(t.options.pointLabels.callback,arguments,t);return e||0===e?e:""}))},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,n,i,a=H.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=t.chart.data.labels.length;for(e=0;er.r&&(r.r=f.end,o.r=h),g.startr.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,n){var i=this,a=e.l/Math.sin(n.l),r=Math.max(e.r-i.width,0)/Math.sin(n.r),o=-e.t/Math.cos(n.t),s=-Math.max(e.b-(i.height-i.paddingTop),0)/Math.cos(n.b);a=qn(a),r=qn(r),o=qn(o),s=qn(s),i.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),i.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,n,i){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=n+a.drawingArea,l=a.height-a.paddingTop-i-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){var e=this.chart,n=(t*(360/e.data.labels.length)+((e.options||{}).startAngle||0))%360;return(n<0?n+360:n)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(H.isNullOrUndef(t))return NaN;var n=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*n:(t-e.min)*n},getPointPosition:function(t,e){var n=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(n)*e+this.xCenter,y:Math.sin(n)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(t){var e=this.min,n=this.max;return this.getPointPositionForValue(t||0,this.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0)},_drawGrid:function(){var t,e,n,i=this,a=i.ctx,r=i.options,o=r.gridLines,s=r.angleLines,l=Yn(s.lineWidth,o.lineWidth),u=Yn(s.color,o.color);if(r.pointLabels.display&&function(t){var e=t.ctx,n=t.options,i=n.pointLabels,a=Hn(n),r=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),o=H.options._parseFont(i);e.save(),e.font=o.string,e.textBaseline="middle";for(var s=t.chart.data.labels.length-1;s>=0;s--){var l=0===s?a/2:0,u=t.getPointPosition(s,r+l+5),d=zn(i.fontColor,s,W.global.defaultFontColor);e.fillStyle=d;var h=t.getIndexAngle(s),c=H.toDegrees(h);e.textAlign=jn(c),Gn(c,t._pointLabelSizes[s],u),Un(e,t.pointLabels[s],u,o.lineHeight)}e.restore()}(i),o.display&&H.each(i.ticks,(function(t,n){0!==n&&(e=i.getDistanceFromCenterForValue(i.ticksAsNumbers[n]),function(t,e,n,i){var a,r=t.ctx,o=e.circular,s=t.chart.data.labels.length,l=zn(e.color,i-1),u=zn(e.lineWidth,i-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,n,0,2*Math.PI);else{a=t.getPointPosition(0,n),r.moveTo(a.x,a.y);for(var d=1;d=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=H.options._parseFont(n),s=Yn(n.fontColor,W.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",H.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:H.noop}),$n=Vn;Zn._defaults=$n;var Xn=H._deprecated,Kn=H.options.resolve,Jn=H.valueOrDefault,Qn=Number.MIN_SAFE_INTEGER||-9007199254740991,ti=Number.MAX_SAFE_INTEGER||9007199254740991,ei={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ni=Object.keys(ei);function ii(t,e){return t-e}function ai(t){return H.valueOrDefault(t.time.min,t.ticks.min)}function ri(t){return H.valueOrDefault(t.time.max,t.ticks.max)}function oi(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]n))return{lo:a,hi:r};s=i-1}}return{lo:r,hi:null}}(t,e,n),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(n-r[e])/s:0,u=(o[i]-r[i])*l;return r[i]+u}function si(t,e){var n=t._adapter,i=t.options.time,a=i.parser,r=a||i.format,o=e;return"function"==typeof a&&(o=a(o)),H.isFinite(o)||(o="string"==typeof r?n.parse(o,r):n.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),H.isFinite(o)||(o=n.parse(o))),o)}function li(t,e){if(H.isNullOrUndef(e))return null;var n=t.options.time,i=si(t,t.getRightValue(e));return null===i?i:(n.round&&(i=+t._adapter.startOf(i,n.round)),i)}function ui(t,e,n,i){var a,r,o,s=ni.length;for(a=ni.indexOf(t);a=0&&(e[r].major=!0);return e}(t,r,o,n):r}var hi=xn.extend({initialize:function(){this.mergeTicksOptions(),xn.prototype.initialize.call(this)},update:function(){var t=this,e=t.options,n=e.time||(e.time={}),i=t._adapter=new rn._date(e.adapters.date);return Xn("time scale",n.format,"time.format","time.parser"),Xn("time scale",n.min,"time.min","ticks.min"),Xn("time scale",n.max,"time.max","ticks.max"),H.mergeIf(n.displayFormats,i.formats()),xn.prototype.update.apply(t,arguments)},getRightValue:function(t){return t&&void 0!==t.t&&(t=t.t),xn.prototype.getRightValue.call(this,t)},determineDataLimits:function(){var t,e,n,i,a,r,o,s=this,l=s.chart,u=s._adapter,d=s.options,h=d.time.unit||"day",c=ti,f=Qn,g=[],m=[],p=[],v=s._getLabels();for(t=0,n=v.length;t1?function(t){var e,n,i,a={},r=[];for(e=0,n=t.length;e1e5*u)throw e+" and "+n+" are too far apart with stepSize of "+u+" "+l;for(a=h;a=a&&n<=r&&d.push(n);return i.min=a,i.max=r,i._unit=l.unit||(s.autoSkip?ui(l.minUnit,i.min,i.max,h):function(t,e,n,i,a){var r,o;for(r=ni.length-1;r>=ni.indexOf(n);r--)if(o=ni[r],ei[o].common&&t._adapter.diff(a,i,o)>=e-1)return o;return ni[n?ni.indexOf(n):0]}(i,d.length,l.minUnit,i.min,i.max)),i._majorUnit=s.major.enabled&&"year"!==i._unit?function(t){for(var e=ni.indexOf(t)+1,n=ni.length;ee&&s=0&&t0?s:1}}),ci={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};hi._defaults=ci;var fi={category:wn,linear:On,logarithmic:Nn,radialLinear:Zn,time:hi},gi=e((function(e,n){e.exports=function(){var n,i;function a(){return n.apply(null,arguments)}function r(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function o(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function s(t){return void 0===t}function l(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function u(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function d(t,e){var n,i=[];for(n=0;n>>0,i=0;i0)for(n=0;n=0?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}var E=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},B={};function j(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(B[t]=a),e&&(B[e[0]]=function(){return z(a.apply(this,arguments),e[1],e[2])}),n&&(B[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function U(t,e){return t.isValid()?(e=G(e,t.localeData()),H[e]=H[e]||function(t){var e,n,i,a=t.match(E);for(e=0,n=a.length;e=0&&V.test(t);)t=t.replace(V,i),V.lastIndex=0,n-=1;return t}var q=/\d/,Z=/\d\d/,$=/\d{3}/,X=/\d{4}/,K=/[+-]?\d{6}/,J=/\d\d?/,Q=/\d\d\d\d?/,tt=/\d\d\d\d\d\d?/,et=/\d{1,3}/,nt=/\d{1,4}/,it=/[+-]?\d{1,6}/,at=/\d+/,rt=/[+-]?\d+/,ot=/Z|[+-]\d\d:?\d\d/gi,st=/Z|[+-]\d\d(?::?\d\d)?/gi,lt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,ut={};function dt(t,e,n){ut[t]=O(e)?e:function(t,i){return t&&n?n:e}}function ht(t,e){return h(ut,t)?ut[t](e._strict,e._locale):new RegExp(ct(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(t,e,n,i,a){return e||n||i||a}))))}function ct(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ft={};function gt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n68?1900:2e3)};var Pt,Tt=Ot("FullYear",!0);function Ot(t,e){return function(n){return null!=n?(Ft(this,t,n),a.updateOffset(this,e),this):At(this,t)}}function At(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function Ft(t,e,n){t.isValid()&&!isNaN(n)&&("FullYear"===e&&Ct(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](n,t.month(),It(n,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](n))}function It(t,e){if(isNaN(t)||isNaN(e))return NaN;var n=function(t,e){return(t%e+e)%e}(e,12);return t+=(e-n)/12,1===n?Ct(t)?29:28:31-n%7%2}Pt=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=0?(s=new Date(t+400,e,n,i,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,n,i,a,r,o),s}function jt(t){var e;if(t<100&&t>=0){var n=Array.prototype.slice.call(arguments);n[0]=t+400,e=new Date(Date.UTC.apply(null,n)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Ut(t,e,n){var i=7+e-n;return-(7+jt(t,0,i).getUTCDay()-e)%7+i-1}function Gt(t,e,n,i,a){var r,o,s=1+7*(e-1)+(7+n-i)%7+Ut(t,i,a);return s<=0?o=Dt(r=t-1)+s:s>Dt(t)?(r=t+1,o=s-Dt(t)):(r=t,o=s),{year:r,dayOfYear:o}}function qt(t,e,n){var i,a,r=Ut(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?i=o+Zt(a=t.year()-1,e,n):o>Zt(t.year(),e,n)?(i=o-Zt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Zt(t,e,n){var i=Ut(t,e,n),a=Ut(t+1,e,n);return(Dt(t)-i+a)/7}function $t(t,e){return t.slice(e,7).concat(t.slice(0,e))}j("w",["ww",2],"wo","week"),j("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),dt("w",J),dt("ww",J,Z),dt("W",J),dt("WW",J,Z),mt(["w","ww","W","WW"],(function(t,e,n,i){e[i.substr(0,1)]=k(t)})),j("d",0,"do","day"),j("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),j("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),j("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),j("e",0,0,"weekday"),j("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),dt("d",J),dt("e",J),dt("E",J),dt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),dt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),dt("dddd",(function(t,e){return e.weekdaysRegex(t)})),mt(["dd","ddd","dddd"],(function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t})),mt(["d","e","E"],(function(t,e,n,i){e[i]=k(t)}));var Xt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Kt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Qt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null}var te=lt,ee=lt,ne=lt;function ie(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ct(s[e]),l[e]=ct(l[e]),u[e]=ct(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function ae(){return this.hours()%12||12}function re(t,e){j(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function oe(t,e){return e._meridiemParse}j("H",["HH",2],0,"hour"),j("h",["hh",2],0,ae),j("k",["kk",2],0,(function(){return this.hours()||24})),j("hmm",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)})),j("hmmss",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)+z(this.seconds(),2)})),j("Hmm",0,0,(function(){return""+this.hours()+z(this.minutes(),2)})),j("Hmmss",0,0,(function(){return""+this.hours()+z(this.minutes(),2)+z(this.seconds(),2)})),re("a",!0),re("A",!1),L("hour","h"),Y("hour",13),dt("a",oe),dt("A",oe),dt("H",J),dt("h",J),dt("k",J),dt("HH",J,Z),dt("hh",J,Z),dt("kk",J,Z),dt("hmm",Q),dt("hmmss",tt),dt("Hmm",Q),dt("Hmmss",tt),gt(["H","HH"],xt),gt(["k","kk"],(function(t,e,n){var i=k(t);e[xt]=24===i?0:i})),gt(["a","A"],(function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t})),gt(["h","hh"],(function(t,e,n){e[xt]=k(t),g(n).bigHour=!0})),gt("hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i)),g(n).bigHour=!0})),gt("hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a)),g(n).bigHour=!0})),gt("Hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i))})),gt("Hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a))}));var se,le=Ot("Hours",!0),ue={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Xt,weekdaysMin:Jt,weekdaysShort:Kt,meridiemParse:/[ap]\.?m?\.?/i},de={},he={};function ce(t){return t?t.toLowerCase().replace("_","-"):t}function fe(n){var i=null;if(!de[n]&&e&&e.exports)try{i=se._abbr,t(),ge(i)}catch(t){}return de[n]}function ge(t,e){var n;return t&&((n=s(e)?pe(t):me(t,e))?se=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),se._abbr}function me(t,e){if(null!==e){var n,i=ue;if(e.abbr=t,null!=de[t])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),i=de[t]._config;else if(null!=e.parentLocale)if(null!=de[e.parentLocale])i=de[e.parentLocale]._config;else{if(null==(n=fe(e.parentLocale)))return he[e.parentLocale]||(he[e.parentLocale]=[]),he[e.parentLocale].push({name:t,config:e}),null;i=n._config}return de[t]=new F(A(i,e)),he[t]&&he[t].forEach((function(t){me(t.name,t.config)})),ge(t),de[t]}return delete de[t],null}function pe(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return se;if(!r(t)){if(e=fe(t))return e;t=[t]}return function(t){for(var e,n,i,a,r=0;r0;){if(i=fe(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&M(a,n,!0)>=e-1)break;e--}r++}return se}(t)}function ve(t){var e,n=t._a;return n&&-2===g(t).overflow&&(e=n[bt]<0||n[bt]>11?bt:n[yt]<1||n[yt]>It(n[vt],n[bt])?yt:n[xt]<0||n[xt]>24||24===n[xt]&&(0!==n[_t]||0!==n[wt]||0!==n[kt])?xt:n[_t]<0||n[_t]>59?_t:n[wt]<0||n[wt]>59?wt:n[kt]<0||n[kt]>999?kt:-1,g(t)._overflowDayOfYear&&(eyt)&&(e=yt),g(t)._overflowWeeks&&-1===e&&(e=Mt),g(t)._overflowWeekday&&-1===e&&(e=St),g(t).overflow=e),t}function be(t,e,n){return null!=t?t:null!=e?e:n}function ye(t){var e,n,i,r,o,s=[];if(!t._d){for(i=function(t){var e=new Date(a.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[bt]&&function(t){var e,n,i,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,n=be(e.GG,t._a[vt],qt(Le(),1,4).year),i=be(e.W,1),((a=be(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=qt(Le(),r,o);n=be(e.gg,t._a[vt],u.year),i=be(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Zt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=Gt(n,i,a,r,o),t._a[vt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=be(t._a[vt],i[vt]),(t._dayOfYear>Dt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),n=jt(o,0,t._dayOfYear),t._a[bt]=n.getUTCMonth(),t._a[yt]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=i[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[xt]&&0===t._a[_t]&&0===t._a[wt]&&0===t._a[kt]&&(t._nextDay=!0,t._a[xt]=0),t._d=(t._useUTC?jt:Bt).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[xt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(g(t).weekdayMismatch=!0)}}var xe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_e=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,we=/Z|[+-]\d\d(?::?\d\d)?/,ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Me=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Se=/^\/?Date\((\-?\d+)/i;function De(t){var e,n,i,a,r,o,s=t._i,l=xe.exec(s)||_e.exec(s);if(l){for(g(t).iso=!0,e=0,n=ke.length;e0&&g(t).unusedInput.push(o),s=s.slice(s.indexOf(n)+n.length),u+=n.length),B[r]?(n?g(t).empty=!1:g(t).unusedTokens.push(r),pt(r,n,t)):t._strict&&!n&&g(t).unusedTokens.push(r);g(t).charsLeftOver=l-u,s.length>0&&g(t).unusedInput.push(s),t._a[xt]<=12&&!0===g(t).bigHour&&t._a[xt]>0&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[xt]=function(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?((i=t.isPM(n))&&e<12&&(e+=12),i||12!==e||(e=0),e):e}(t._locale,t._a[xt],t._meridiem),ye(t),ve(t)}else Oe(t);else De(t)}function Fe(t){var e=t._i,n=t._f;return t._locale=t._locale||pe(t._l),null===e||void 0===n&&""===e?p({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new x(ve(e)):(u(e)?t._d=e:r(n)?function(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis?this:t:p()}));function We(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Le();for(n=e[0],i=1;i=0?new Date(t+400,e,n)-hn:new Date(t,e,n).valueOf()}function gn(t,e,n){return t<100&&t>=0?Date.UTC(t+400,e,n)-hn:Date.UTC(t,e,n)}function mn(t,e){j(0,[t,t.length],0,e)}function pn(t,e,n,i,a){var r;return null==t?qt(this,i,a).year:(e>(r=Zt(t,i,a))&&(e=r),vn.call(this,t,e,n,i,a))}function vn(t,e,n,i,a){var r=Gt(t,e,n,i,a),o=jt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}j(0,["gg",2],0,(function(){return this.weekYear()%100})),j(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),mn("gggg","weekYear"),mn("ggggg","weekYear"),mn("GGGG","isoWeekYear"),mn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),dt("G",rt),dt("g",rt),dt("GG",J,Z),dt("gg",J,Z),dt("GGGG",nt,X),dt("gggg",nt,X),dt("GGGGG",it,K),dt("ggggg",it,K),mt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,n,i){e[i.substr(0,2)]=k(t)})),mt(["gg","GG"],(function(t,e,n,i){e[i]=a.parseTwoDigitYear(t)})),j("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),dt("Q",q),gt("Q",(function(t,e){e[bt]=3*(k(t)-1)})),j("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),dt("D",J),dt("DD",J,Z),dt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),gt(["D","DD"],yt),gt("Do",(function(t,e){e[yt]=k(t.match(J)[0])}));var bn=Ot("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),dt("DDD",et),dt("DDDD",$),gt(["DDD","DDDD"],(function(t,e,n){n._dayOfYear=k(t)})),j("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),dt("m",J),dt("mm",J,Z),gt(["m","mm"],_t);var yn=Ot("Minutes",!1);j("s",["ss",2],0,"second"),L("second","s"),Y("second",15),dt("s",J),dt("ss",J,Z),gt(["s","ss"],wt);var xn,_n=Ot("Seconds",!1);for(j("S",0,0,(function(){return~~(this.millisecond()/100)})),j(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),j(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),j(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),j(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),j(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),j(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),L("millisecond","ms"),Y("millisecond",16),dt("S",et,q),dt("SS",et,Z),dt("SSS",et,$),xn="SSSS";xn.length<=9;xn+="S")dt(xn,at);function wn(t,e){e[kt]=k(1e3*("0."+t))}for(xn="S";xn.length<=9;xn+="S")gt(xn,wn);var kn=Ot("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var Mn=x.prototype;function Sn(t){return t}Mn.add=en,Mn.calendar=function(t,e){var n=t||Le(),i=Ue(n,this).startOf("day"),r=a.calendarFormat(this,i)||"sameElse",o=e&&(O(e[r])?e[r].call(this,n):e[r]);return this.format(o||this.localeData().calendar(r,this,Le(n)))},Mn.clone=function(){return new x(this)},Mn.diff=function(t,e,n){var i,a,r;if(!this.isValid())return NaN;if(!(i=Ue(t,this)).isValid())return NaN;switch(a=6e4*(i.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=an(this,i)/12;break;case"month":r=an(this,i);break;case"quarter":r=an(this,i)/3;break;case"second":r=(this-i)/1e3;break;case"minute":r=(this-i)/6e4;break;case"hour":r=(this-i)/36e5;break;case"day":r=(this-i-a)/864e5;break;case"week":r=(this-i-a)/6048e5;break;default:r=this-i}return n?r:w(r)},Mn.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=dn-cn(e+(this._isUTC?0:this.utcOffset()*un),dn)-1;break;case"minute":e=this._d.valueOf(),e+=un-cn(e,un)-1;break;case"second":e=this._d.valueOf(),e+=ln-cn(e,ln)-1}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.format=function(t){t||(t=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var e=U(this,t);return this.localeData().postformat(e)},Mn.from=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.fromNow=function(t){return this.from(Le(),t)},Mn.to=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.toNow=function(t){return this.to(Le(),t)},Mn.get=function(t){return O(this[t=R(t)])?this[t]():this},Mn.invalidAt=function(){return g(this).overflow},Mn.isAfter=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()9999?U(n,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(n+i+"-MM-DD[T]HH:mm:ss.SSS"+a)},Mn.toJSON=function(){return this.isValid()?this.toISOString():null},Mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Mn.unix=function(){return Math.floor(this.valueOf()/1e3)},Mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},Mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Mn.year=Tt,Mn.isLeapYear=function(){return Ct(this.year())},Mn.weekYear=function(t){return pn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Mn.isoWeekYear=function(t){return pn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},Mn.quarter=Mn.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},Mn.month=zt,Mn.daysInMonth=function(){return It(this.year(),this.month())},Mn.week=Mn.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},Mn.isoWeek=Mn.isoWeeks=function(t){var e=qt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},Mn.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},Mn.isoWeeksInYear=function(){return Zt(this.year(),1,4)},Mn.date=bn,Mn.day=Mn.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},Mn.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},Mn.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},Mn.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},Mn.hour=Mn.hours=le,Mn.minute=Mn.minutes=yn,Mn.second=Mn.seconds=_n,Mn.millisecond=Mn.milliseconds=kn,Mn.utcOffset=function(t,e,n){var i,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=je(st,t)))return this}else Math.abs(t)<16&&!n&&(t*=60);return!this._isUTC&&e&&(i=Ge(this)),this._offset=t,this._isUTC=!0,null!=i&&this.add(i,"m"),r!==t&&(!e||this._changeInProgress?tn(this,Xe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Ge(this)},Mn.utc=function(t){return this.utcOffset(0,t)},Mn.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ge(this),"m")),this},Mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=je(ot,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},Mn.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Le(t).utcOffset():0,(this.utcOffset()-t)%60==0)},Mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mn.isUtc=qe,Mn.isUTC=qe,Mn.zoneAbbr=function(){return this._isUTC?"UTC":""},Mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mn.dates=D("dates accessor is deprecated. Use date instead.",bn),Mn.months=D("months accessor is deprecated. Use month instead",zt),Mn.years=D("years accessor is deprecated. Use year instead",Tt),Mn.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()})),Mn.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),(t=Fe(t))._a){var e=t._isUTC?f(t._a):Le(t._a);this._isDSTShifted=this.isValid()&&M(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var Dn=F.prototype;function Cn(t,e,n,i){var a=pe(),r=f().set(i,e);return a[n](r,t)}function Pn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return Cn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=Cn(t,i,n,"month");return a}function Tn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(n=e=t,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a,r=pe(),o=t?r._week.dow:0;if(null!=n)return Cn(e,(n+o)%7,i,"day");var s=[];for(a=0;a<7;a++)s[a]=Cn(e,(a+o)%7,i,"day");return s}Dn.calendar=function(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return O(i)?i.call(e,n):i},Dn.longDateFormat=function(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,(function(t){return t.slice(1)})),this._longDateFormat[t])},Dn.invalidDate=function(){return this._invalidDate},Dn.ordinal=function(t){return this._ordinal.replace("%d",t)},Dn.preparse=Sn,Dn.postformat=Sn,Dn.relativeTime=function(t,e,n,i){var a=this._relativeTime[n];return O(a)?a(t,e,n,i):a.replace(/%d/i,t)},Dn.pastFuture=function(t,e){var n=this._relativeTime[t>0?"future":"past"];return O(n)?n(e):n.replace(/%s/i,e)},Dn.set=function(t){var e,n;for(n in t)O(e=t[n])?this[n]=e:this["_"+n]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Dn.months=function(t,e){return t?r(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Lt).test(e)?"format":"standalone"][t.month()]:r(this._months)?this._months:this._months.standalone},Dn.monthsShort=function(t,e){return t?r(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Lt.test(e)?"format":"standalone"][t.month()]:r(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Dn.monthsParse=function(t,e,n){var i,a,r;if(this._monthsParseExact)return Wt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;i<12;i++){if(a=f([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}},Dn.monthsRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=Vt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},Dn.monthsShortRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=Et),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},Dn.week=function(t){return qt(t,this._week.dow,this._week.doy).week},Dn.firstDayOfYear=function(){return this._week.doy},Dn.firstDayOfWeek=function(){return this._week.dow},Dn.weekdays=function(t,e){var n=r(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?$t(n,this._week.dow):t?n[t.day()]:n},Dn.weekdaysMin=function(t){return!0===t?$t(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},Dn.weekdaysShort=function(t){return!0===t?$t(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},Dn.weekdaysParse=function(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Qt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}},Dn.weekdaysRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},Dn.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Dn.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ne),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Dn.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},Dn.meridiem=function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},ge("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),a.lang=D("moment.lang is deprecated. Use moment.locale instead.",ge),a.langData=D("moment.langData is deprecated. Use moment.localeData instead.",pe);var On=Math.abs;function An(t,e,n,i){var a=Xe(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Fn(t){return t<0?Math.floor(t):Math.ceil(t)}function In(t){return 4800*t/146097}function Ln(t){return 146097*t/4800}function Rn(t){return function(){return this.as(t)}}var Nn=Rn("ms"),Wn=Rn("s"),Yn=Rn("m"),zn=Rn("h"),En=Rn("d"),Vn=Rn("w"),Hn=Rn("M"),Bn=Rn("Q"),jn=Rn("y");function Un(t){return function(){return this.isValid()?this._data[t]:NaN}}var Gn=Un("milliseconds"),qn=Un("seconds"),Zn=Un("minutes"),$n=Un("hours"),Xn=Un("days"),Kn=Un("months"),Jn=Un("years"),Qn=Math.round,ti={ss:44,s:45,m:45,h:22,d:26,M:11};function ei(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}var ni=Math.abs;function ii(t){return(t>0)-(t<0)||+t}function ai(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n=ni(this._milliseconds)/1e3,i=ni(this._days),a=ni(this._months);t=w(n/60),e=w(t/60),n%=60,t%=60;var r=w(a/12),o=a%=12,s=i,l=e,u=t,d=n?n.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=ii(this._months)!==ii(h)?"-":"",g=ii(this._days)!==ii(h)?"-":"",m=ii(this._milliseconds)!==ii(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var ri=ze.prototype;return ri.isValid=function(){return this._isValid},ri.abs=function(){var t=this._data;return this._milliseconds=On(this._milliseconds),this._days=On(this._days),this._months=On(this._months),t.milliseconds=On(t.milliseconds),t.seconds=On(t.seconds),t.minutes=On(t.minutes),t.hours=On(t.hours),t.months=On(t.months),t.years=On(t.years),this},ri.add=function(t,e){return An(this,t,e,1)},ri.subtract=function(t,e){return An(this,t,e,-1)},ri.as=function(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+i/864e5,n=this._months+In(e),t){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(e=this._days+Math.round(Ln(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}},ri.asMilliseconds=Nn,ri.asSeconds=Wn,ri.asMinutes=Yn,ri.asHours=zn,ri.asDays=En,ri.asWeeks=Vn,ri.asMonths=Hn,ri.asQuarters=Bn,ri.asYears=jn,ri.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},ri._bubble=function(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Fn(Ln(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=w(r/1e3),l.seconds=t%60,e=w(t/60),l.minutes=e%60,n=w(e/60),l.hours=n%24,o+=w(n/24),a=w(In(o)),s+=a,o-=Fn(Ln(a)),i=w(s/12),s%=12,l.days=o,l.months=s,l.years=i,this},ri.clone=function(){return Xe(this)},ri.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},ri.milliseconds=Gn,ri.seconds=qn,ri.minutes=Zn,ri.hours=$n,ri.days=Xn,ri.weeks=function(){return w(this.days()/7)},ri.months=Kn,ri.years=Jn,ri.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=function(t,e,n){var i=Xe(t).abs(),a=Qn(i.as("s")),r=Qn(i.as("m")),o=Qn(i.as("h")),s=Qn(i.as("d")),l=Qn(i.as("M")),u=Qn(i.as("y")),d=a<=ti.ss&&["s",a]||a0,d[4]=n,ei.apply(null,d)}(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)},ri.toISOString=ai,ri.toString=ai,ri.toJSON=ai,ri.locale=rn,ri.localeData=sn,ri.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ai),ri.lang=on,j("X",0,0,"unix"),j("x",0,0,"valueOf"),dt("x",rt),dt("X",/[+-]?\d+(\.\d{1,3})?/),gt("X",(function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))})),gt("x",(function(t,e,n){n._d=new Date(k(t))})),a.version="2.24.0",n=Le,a.fn=Mn,a.min=function(){return We("isBefore",[].slice.call(arguments,0))},a.max=function(){return We("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=f,a.unix=function(t){return Le(1e3*t)},a.months=function(t,e){return Pn(t,e,"months")},a.isDate=u,a.locale=ge,a.invalid=p,a.duration=Xe,a.isMoment=_,a.weekdays=function(t,e,n){return Tn(t,e,n,"weekdays")},a.parseZone=function(){return Le.apply(null,arguments).parseZone()},a.localeData=pe,a.isDuration=Ee,a.monthsShort=function(t,e){return Pn(t,e,"monthsShort")},a.weekdaysMin=function(t,e,n){return Tn(t,e,n,"weekdaysMin")},a.defineLocale=me,a.updateLocale=function(t,e){if(null!=e){var n,i,a=ue;null!=(i=fe(t))&&(a=i._config),e=A(a,e),(n=new F(e)).parentLocale=de[t],de[t]=n,ge(t)}else null!=de[t]&&(null!=de[t].parentLocale?de[t]=de[t].parentLocale:null!=de[t]&&delete de[t]);return de[t]},a.locales=function(){return C(de)},a.weekdaysShort=function(t,e,n){return Tn(t,e,n,"weekdaysShort")},a.normalizeUnits=R,a.relativeTimeRounding=function(t){return void 0===t?Qn:"function"==typeof t&&(Qn=t,!0)},a.relativeTimeThreshold=function(t,e){return void 0!==ti[t]&&(void 0===e?ti[t]:(ti[t]=e,"s"===t&&(ti.ss=e-1),!0))},a.calendarFormat=function(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},a.prototype=Mn,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()})),mi={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};rn._date.override("function"==typeof gi?{_id:"moment",formats:function(){return mi},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=gi(t,e):t instanceof gi||(t=gi(t)),t.isValid()?t.valueOf():null},format:function(t,e){return gi(t).format(e)},add:function(t,e,n){return gi(t).add(e,n).valueOf()},diff:function(t,e,n){return gi(t).diff(gi(e),n)},startOf:function(t,e,n){return t=gi(t),"isoWeek"===e?t.isoWeekday(n).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return gi(t).endOf(e).valueOf()},_create:function(t){return gi(t)}}:{}),W._set("global",{plugins:{filler:{propagate:!0}}});var pi={dataset:function(t){var e=t.fill,n=t.chart,i=n.getDatasetMeta(e),a=i&&n.isDatasetVisible(e)&&i.dataset._children||[],r=a.length||0;return r?function(t,e){return e=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function bi(t){return(t.el._scale||{}).getPointPositionForValue?function(t){var e,n,i,a,r,o=t.el._scale,s=o.options,l=o.chart.data.labels.length,u=t.fill,d=[];if(!l)return null;for(e=s.ticks.reverse?o.max:o.min,n=s.ticks.reverse?o.min:o.max,i=o.getPointPositionForValue(0,e),a=0;a0;--r)H.canvas.lineTo(t,n[r],n[r-1],!0);else for(o=n[0].cx,s=n[0].cy,l=Math.sqrt(Math.pow(n[0].x-o,2)+Math.pow(n[0].y-s,2)),r=a-1;r>0;--r)t.arc(o,s,l,n[r].angle,n[r-1].angle,!0)}}function ki(t,e,n,i,a,r){var o,s,l,u,d,h,c,f,g=e.length,m=i.spanGaps,p=[],v=[],b=0,y=0;for(t.beginPath(),o=0,s=g;o=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||W.global.defaultColor,o&&s&&r.length&&(H.canvas.clipArea(u,t.chartArea),ki(u,r,o,a,s,i._loop),H.canvas.unclipArea(u)))}},Si=H.rtl.getRtlAdapter,Di=H.noop,Ci=H.valueOrDefault;function Pi(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}W._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;el.width)&&(h+=o+n.padding,d[d.length-(e>0?0:1)]=0),s[e]={left:0,top:0,width:i,height:o},d[d.length-1]+=i+n.padding})),l.height+=h}else{var c=n.padding,f=t.columnWidths=[],g=t.columnHeights=[],m=n.padding,p=0,v=0;H.each(t.legendItems,(function(t,e){var i=Pi(n,o)+o/2+a.measureText(t.text).width;e>0&&v+o+2*c>l.height&&(m+=p+n.padding,f.push(p),g.push(v),p=0,v=0),p=Math.max(p,i),v+=o+c,s[e]={left:0,top:0,width:i,height:o}})),m+=p,f.push(p),g.push(v),l.width+=m}t.width=l.width,t.height=l.height}else t.width=l.width=t.height=l.height=0},afterFit:Di,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,n=e.labels,i=W.global,a=i.defaultColor,r=i.elements.line,o=t.height,s=t.columnHeights,l=t.width,u=t.lineWidths;if(e.display){var d,h=Si(e.rtl,t.left,t.minSize.width),c=t.ctx,f=Ci(n.fontColor,i.defaultFontColor),g=H.options._parseFont(n),m=g.size;c.textAlign=h.textAlign("left"),c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=g.string;var p=Pi(n,m),v=t.legendHitBoxes,b=function(t,i){switch(e.align){case"start":return n.padding;case"end":return t-i;default:return(t-i+n.padding)/2}},y=t.isHorizontal();d=y?{x:t.left+b(l,u[0]),y:t.top+n.padding,line:0}:{x:t.left+n.padding,y:t.top+b(o,s[0]),line:0},H.rtl.overrideTextDirection(t.ctx,e.textDirection);var x=m+n.padding;H.each(t.legendItems,(function(e,i){var f=c.measureText(e.text).width,g=p+m/2+f,_=d.x,w=d.y;h.setWidth(t.minSize.width),y?i>0&&_+g+n.padding>t.left+t.minSize.width&&(w=d.y+=x,d.line++,_=d.x=t.left+b(l,u[d.line])):i>0&&w+x>t.top+t.minSize.height&&(_=d.x=_+t.columnWidths[d.line]+n.padding,d.line++,w=d.y=t.top+b(o,s[d.line]));var k=h.x(_);!function(t,e,i){if(!(isNaN(p)||p<=0)){c.save();var o=Ci(i.lineWidth,r.borderWidth);if(c.fillStyle=Ci(i.fillStyle,a),c.lineCap=Ci(i.lineCap,r.borderCapStyle),c.lineDashOffset=Ci(i.lineDashOffset,r.borderDashOffset),c.lineJoin=Ci(i.lineJoin,r.borderJoinStyle),c.lineWidth=o,c.strokeStyle=Ci(i.strokeStyle,a),c.setLineDash&&c.setLineDash(Ci(i.lineDash,r.borderDash)),n&&n.usePointStyle){var s=p*Math.SQRT2/2,l=h.xPlus(t,p/2),u=e+m/2;H.canvas.drawPoint(c,i.pointStyle,s,l,u,i.rotation)}else c.fillRect(h.leftForLtr(t,p),e,p,m),0!==o&&c.strokeRect(h.leftForLtr(t,p),e,p,m);c.restore()}}(k,w,e),v[i].left=h.leftForLtr(k,v[i].width),v[i].top=w,function(t,e,n,i){var a=m/2,r=h.xPlus(t,p+a),o=e+a;c.fillText(n.text,r,o),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(r,o),c.lineTo(h.xPlus(r,i),o),c.stroke())}(k,w,e,f),y?d.x+=g+n.padding:d.y+=x})),H.rtl.restoreTextDirection(t.ctx,e.textDirection)}},_getLegendItemAt:function(t,e){var n,i,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,n=0;n=(i=a[n]).left&&t<=i.left+i.width&&e>=i.top&&e<=i.top+i.height)return r.legendItems[n];return null},handleEvent:function(t){var e,n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover&&!i.onLeave)return}else{if("click"!==a)return;if(!i.onClick)return}e=n._getLegendItemAt(t.x,t.y),"click"===a?e&&i.onClick&&i.onClick.call(n,t.native,e):(i.onLeave&&e!==n._hoveredItem&&(n._hoveredItem&&i.onLeave.call(n,t.native,n._hoveredItem),n._hoveredItem=e),i.onHover&&e&&i.onHover.call(n,t.native,e))}});function Oi(t,e){var n=new Ti({ctx:t.ctx,options:e,chart:t});me.configure(t,n,e),me.addBox(t,n),t.legend=n}var Ai={id:"legend",_element:Ti,beforeInit:function(t){var e=t.options.legend;e&&Oi(t,e)},beforeUpdate:function(t){var e=t.options.legend,n=t.legend;e?(H.mergeIf(e,W.global.legend),n?(me.configure(t,n,e),n.options=e):Oi(t,e)):n&&(me.removeBox(t,n),delete t.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}},Fi=H.noop;W._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Ii=$.extend({initialize:function(t){H.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:Fi,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:Fi,beforeSetDimensions:Fi,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:Fi,beforeBuildLabels:Fi,buildLabels:Fi,afterBuildLabels:Fi,beforeFit:Fi,fit:function(){var t,e=this,n=e.options,i=e.minSize={},a=e.isHorizontal();n.display?(t=(H.isArray(n.text)?n.text.length:1)*H.options._parseFont(n).lineHeight+2*n.padding,e.width=i.width=a?e.maxWidth:t,e.height=i.height=a?t:e.maxHeight):e.width=i.width=e.height=i.height=0},afterFit:Fi,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,n=t.options;if(n.display){var i,a,r,o=H.options._parseFont(n),s=o.lineHeight,l=s/2+n.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=H.valueOrDefault(n.fontColor,W.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,i=f-h):(a="left"===n.position?h+l:f-l,r=d+(c-d)/2,i=c-d,u=Math.PI*("left"===n.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=n.text;if(H.isArray(g))for(var m=0,p=0;p=0;i--){var a=t[i];if(e(a))return a}},H.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},H.almostEquals=function(t,e,n){return Math.abs(t-e)=t},H.max=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.max(t,e)}),Number.NEGATIVE_INFINITY)},H.min=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.min(t,e)}),Number.POSITIVE_INFINITY)},H.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},H.toRadians=function(t){return t*(Math.PI/180)},H.toDegrees=function(t){return t*(180/Math.PI)},H._decimalPlaces=function(t){if(H.isFinite(t)){for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}},H.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},H.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},H.aliasPixel=function(t){return t%2==0?0:.5},H._alignPixel=function(t,e,n){var i=t.currentDevicePixelRatio,a=n/2;return Math.round((e-a)*i)/i+a},H.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=i*(u=isNaN(u)?0:u),c=i*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},H.EPSILON=Number.EPSILON||1e-14,H.splineCurveMonotone=function(t){var e,n,i,a,r,o,s,l,u,d=(t||[]).map((function(t){return{model:t._model,deltaK:0,mK:0}})),h=d.length;for(e=0;e0?d[e-1]:null,(a=e0?d[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},H.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},H.niceNum=function(t,e){var n=Math.floor(H.log10(t)),i=t/Math.pow(10,n);return(e?i<1.5?1:i<3?2:i<7?5:10:i<=1?1:i<=2?2:i<=5?5:10)*Math.pow(10,n)},H.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},H.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(n=s[0].clientX,i=s[0].clientY):(n=a.clientX,i=a.clientY);var l=parseFloat(H.getStyle(r,"padding-left")),u=parseFloat(H.getStyle(r,"padding-top")),d=parseFloat(H.getStyle(r,"padding-right")),h=parseFloat(H.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:n=Math.round((n-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:i=Math.round((i-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},H.getConstraintWidth=function(t){return n(t,"max-width","clientWidth")},H.getConstraintHeight=function(t){return n(t,"max-height","clientHeight")},H._calculatePadding=function(t,e,n){return(e=H.getStyle(t,e)).indexOf("%")>-1?n*parseInt(e,10)/100:parseInt(e,10)},H._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},H.getMaximumWidth=function(t){var e=H._getParentNode(t);if(!e)return t.clientWidth;var n=e.clientWidth,i=n-H._calculatePadding(e,"padding-left",n)-H._calculatePadding(e,"padding-right",n),a=H.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},H.getMaximumHeight=function(t){var e=H._getParentNode(t);if(!e)return t.clientHeight;var n=e.clientHeight,i=n-H._calculatePadding(e,"padding-top",n)-H._calculatePadding(e,"padding-bottom",n),a=H.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},H.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},H.retinaScale=function(t,e){var n=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==n){var i=t.canvas,a=t.height,r=t.width;i.height=a*n,i.width=r*n,t.ctx.scale(n,n),i.style.height||i.style.width||(i.style.height=a+"px",i.style.width=r+"px")}},H.fontString=function(t,e,n){return e+" "+t+"px "+n},H.longestText=function(t,e,n,i){var a=(i=i||{}).data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var o,s,l,u,d,h=0,c=n.length;for(o=0;on.length){for(o=0;oi&&(i=r),i},H.numberOfLabelLines=function(t){var e=1;return H.each(t,(function(t){H.isArray(t)&&t.length>e&&(e=t.length)})),e},H.color=k?function(t){return t instanceof CanvasGradient&&(t=W.global.defaultColor),k(t)}:function(t){return console.error("Color.js not found!"),t},H.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:H.color(t).saturate(.5).darken(.1).rgbString()}}(),en._adapters=rn,en.Animation=K,en.animationService=J,en.controllers=Jt,en.DatasetController=it,en.defaults=W,en.Element=$,en.elements=wt,en.Interaction=re,en.layouts=me,en.platform=Ie,en.plugins=Le,en.Scale=xn,en.scaleService=Re,en.Ticks=on,en.Tooltip=Ge,en.helpers.each(fi,(function(t,e){en.scaleService.registerScaleType(e,t,t._defaults)})),Ri)Ri.hasOwnProperty(zi)&&en.plugins.register(Ri[zi]);en.platform.initialize();var Ei=en;return"undefined"!=typeof window&&(window.Chart=en),en.Chart=en,en.Legend=Ri.legend._element,en.Title=Ri.title._element,en.pluginService=en.plugins,en.PluginBase=en.Element.extend({}),en.canvasHelpers=en.helpers.canvas,en.layoutService=en.layouts,en.LinearScaleBase=Dn,en.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],(function(t){en[t]=function(e,n){return new en(e,en.helpers.merge(n||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}})),Ei})); diff --git a/flask__webservers/cookies/main.py b/flask__webservers/cookies/main.py new file mode 100644 index 000000000..e6ddcd2d3 --- /dev/null +++ b/flask__webservers/cookies/main.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +from flask import Flask, request, make_response, jsonify + + +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/get-cookies") +def get_cookies(): + return jsonify(request.cookies) + + +@app.route("/set-cookies", methods=["POST"]) +def set_cookies(): + rs = make_response(jsonify(dict(ok=True))) + + for k, v in request.args.items(): + rs.set_cookie(k, v) + + return rs + + +if __name__ == "__main__": + app.debug = True + app.run( + port=5001, + ) diff --git a/flask__webservers/cookies/test.py b/flask__webservers/cookies/test.py new file mode 100644 index 000000000..5395d9602 --- /dev/null +++ b/flask__webservers/cookies/test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests + + +session = requests.Session() + + +rs = session.get("http://127.0.0.1:5001/get-cookies") +print(rs, rs.url) +print(rs.headers) +print(rs.cookies) +print(rs.json()) +""" + http://127.0.0.1:5001/get-cookies +{'Content-Type': 'application/json', 'Content-Length': '3', 'Server': 'Werkzeug/0.15.4 Python/3.7.3', 'Date': 'Wed, 24 Feb 2021 13:28:00 GMT'} + +{} +""" + +print() + +rs = session.post("http://127.0.0.1:5001/set-cookies", params=dict(a=123, b=3)) +print(rs, rs.url) +print(rs.headers) +print(rs.cookies) +print(rs.json()) +""" + http://127.0.0.1:5001/set-cookies?a=123&b=3 +{'Content-Type': 'application/json', 'Content-Length': '17', 'Set-Cookie': 'a=123; Path=/, b=3; Path=/', 'Server': 'Werkzeug/0.15.4 Python/3.7.3', 'Date': 'Wed, 24 Feb 2021 13:28:00 GMT'} +, ]> +{'ok': True} +""" + +print() + +rs = session.get("http://127.0.0.1:5001/get-cookies") +print(rs, rs.url) +print(rs.headers) +print(rs.cookies) +print(rs.json()) +""" + http://127.0.0.1:5001/get-cookies +{'Content-Type': 'application/json', 'Content-Length': '30', 'Server': 'Werkzeug/0.15.4 Python/3.7.3', 'Date': 'Wed, 24 Feb 2021 13:28:00 GMT'} + +{'a': '123', 'b': '3'} +""" diff --git a/flask__webservers/cors__hello_world.py b/flask__webservers/cors__hello_world.py new file mode 100644 index 000000000..f771cddde --- /dev/null +++ b/flask__webservers/cors__hello_world.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install flask==2.3.3 +from flask import Flask + +# pip install flask-cors==4.0.0 +from flask_cors import CORS + + +app = Flask(__name__) +CORS(app) + + +@app.route("/") +def index() -> str: + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=50000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/custom_error_page/main.py b/flask__webservers/custom_error_page/main.py new file mode 100644 index 000000000..64c67879b --- /dev/null +++ b/flask__webservers/custom_error_page/main.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://flask.palletsprojects.com/en/3.0.x/errorhandling/ + + +import logging + +from flask import Flask, render_template +from werkzeug.exceptions import HTTPException + + +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + +@app.errorhandler(404) +def page_not_found(e): + # note that we set the 404 status explicitly + return render_template('errors/404.html'), 404 + + +@app.errorhandler(Exception) +def handle_exception(e): + # pass through HTTP errors + if isinstance(e, HTTPException): + return e + + # now you're handling non-HTTP exceptions only + return render_template("errors/500.html", e=e), 500 + + +@app.route("/") +def index(): + return render_template('index.html') + + +@app.route("/500") +def do_500() -> None: + 1/0 + + +if __name__ == "__main__": + app.run(port=5000) diff --git a/flask__webservers/custom_error_page/static/images/404.jpg b/flask__webservers/custom_error_page/static/images/404.jpg new file mode 100644 index 000000000..76a486e4a Binary files /dev/null and b/flask__webservers/custom_error_page/static/images/404.jpg differ diff --git a/flask__webservers/custom_error_page/static/images/500.jpg b/flask__webservers/custom_error_page/static/images/500.jpg new file mode 100644 index 000000000..f9edf60a6 Binary files /dev/null and b/flask__webservers/custom_error_page/static/images/500.jpg differ diff --git a/flask__webservers/custom_error_page/templates/base.html b/flask__webservers/custom_error_page/templates/base.html new file mode 100644 index 000000000..67f2c8af6 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/base.html @@ -0,0 +1,11 @@ + + + + + + {% block title %}{% endblock %} + + + {% block content %}{% endblock %} + + diff --git a/flask__webservers/custom_error_page/templates/errors/404.html b/flask__webservers/custom_error_page/templates/errors/404.html new file mode 100644 index 000000000..40823bc07 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/errors/404.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block title %}Page Not Found{% endblock %} + +{% block content %} +

Page Not Found

+

What you were looking for is just not there.

+ +

go somewhere nice

+{% endblock %} diff --git a/flask__webservers/custom_error_page/templates/errors/500.html b/flask__webservers/custom_error_page/templates/errors/500.html new file mode 100644 index 000000000..01bd13206 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/errors/500.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block title %}Internal Server Error{% endblock %} + +{% block content %} +

Internal Server Error

+ +

Something unexpected happened... Please try again later

+

Error: {{ e }}

+{% endblock %} diff --git a/flask__webservers/custom_error_page/templates/index.html b/flask__webservers/custom_error_page/templates/index.html new file mode 100644 index 000000000..01cc057a1 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/index.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}Start page{% endblock %} + +{% block content %} +

Start page

+

Go 404

+

Go 500

+{% endblock %} diff --git a/flask__webservers/filter-sort-paginate-on-server/items.json b/flask__webservers/filter-sort-paginate-on-server/items.json new file mode 100644 index 000000000..bc8789bd8 --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/items.json @@ -0,0 +1,302 @@ +[ + { + "id": 1, + "name": "Preston", + "description": "Magna commodo deserunt culpa anim anim exercitation reprehenderit officia occaecat consectetur nisi est ut. Tempor minim aute nostrud sunt do labore cillum. Consequat dolore do pariatur pariatur fugiat nulla anim exercitation culpa nisi qui dolor dolore magna.\r\n", + "command": "Autograte-prestonherrera@autograte.com" + }, + { + "id": 2, + "name": "Fay", + "description": "Elit laboris quis sint magna labore magna ullamco anim excepteur. Duis incididunt excepteur ut labore aliquip do. Aute sint do excepteur aliquip mollit proident fugiat sint. Mollit labore ex esse consectetur incididunt commodo in. Occaecat commodo eiusmod ad enim eiusmod consectetur anim cupidatat do duis est elit. Nisi occaecat est dolor dolor officia anim irure excepteur magna aute mollit.\r\n", + "command": "Liquidoc-fayherrera@liquidoc.com" + }, + { + "id": 3, + "name": "Dena", + "description": "Deserunt consequat Lorem adipisicing duis minim. Irure pariatur occaecat voluptate consectetur pariatur sint commodo excepteur incididunt esse amet id. Elit commodo nulla enim aliqua mollit ullamco duis anim consequat. Culpa sit ex nostrud fugiat reprehenderit fugiat. Incididunt enim ullamco esse velit consectetur. Esse cillum anim ullamco occaecat commodo enim aute laboris.\r\n", + "command": "Lingoage-denaherrera@lingoage.com" + }, + { + "id": 4, + "name": "Jacobs", + "description": "Voluptate anim amet dolor deserunt nostrud cillum magna velit enim qui. Dolore magna aliquip nisi eu exercitation eu eu Lorem. Ea occaecat culpa sit qui cupidatat veniam. Exercitation minim minim anim ad nulla veniam et velit. Nostrud laborum labore culpa do ipsum et cupidatat. Cupidatat officia adipisicing sunt aliquip id nisi deserunt.\r\n", + "command": "Imant-jacobsherrera@imant.com" + }, + { + "id": 5, + "name": "Richards", + "description": "Et eu laboris aute ipsum. Cupidatat consectetur dolor sint elit. Et quis laborum deserunt deserunt do cillum ipsum proident sunt laboris enim veniam. Sint id fugiat eu quis est dolore ut minim duis. Labore voluptate nulla adipisicing culpa proident.\r\n", + "command": "Octocore-richardsherrera@octocore.com" + }, + { + "id": 6, + "name": "Connie", + "description": "Officia culpa culpa proident fugiat dolor esse irure incididunt officia occaecat mollit amet. Ea exercitation reprehenderit proident laboris sint aute eiusmod voluptate. Mollit cupidatat exercitation laboris nulla sunt non ex.\r\n", + "command": "Circum-connieherrera@circum.com" + }, + { + "id": 7, + "name": "Lilia", + "description": "Aliquip sunt non do et nisi incididunt minim et. Nisi culpa elit in exercitation velit dolor cupidatat veniam consectetur mollit aliqua culpa. Ea irure commodo et cillum id. Sint pariatur eu cupidatat occaecat tempor excepteur do eiusmod sint.\r\n", + "command": "Petigems-liliaherrera@petigems.com" + }, + { + "id": 8, + "name": "Serena", + "description": "Lorem aliqua ad ut ut. Minim dolore incididunt sit enim incididunt consectetur. Ad cillum non sit sunt elit quis reprehenderit qui deserunt. Exercitation voluptate consectetur ullamco Lorem ipsum ut pariatur ea velit anim ex elit. Cupidatat proident reprehenderit ad aute in anim anim veniam ullamco mollit qui.\r\n", + "command": "Slumberia-serenaherrera@slumberia.com" + }, + { + "id": 9, + "name": "Cherie", + "description": "Labore incididunt mollit exercitation deserunt nostrud sint qui eu. Quis laboris labore ipsum cupidatat ea. Laboris consectetur ea Lorem deserunt tempor reprehenderit aliquip ipsum nostrud ex nisi veniam.\r\n", + "command": "Futurize-cherieherrera@futurize.com" + }, + { + "id": 10, + "name": "Lindsey", + "description": "Excepteur sit pariatur cupidatat veniam ullamco quis magna eu voluptate labore eiusmod magna tempor. Sint do sit in cillum id et ullamco aliquip nostrud occaecat. Consequat amet in nostrud adipisicing magna laborum duis labore fugiat culpa ut laborum. Non aliqua ea ex non. Nostrud quis ullamco nisi Lorem reprehenderit proident culpa fugiat.\r\n", + "command": "Aquoavo-lindseyherrera@aquoavo.com" + }, + { + "id": 11, + "name": "Nicole", + "description": "In est magna laboris velit sint ea. Quis tempor deserunt quis voluptate in proident adipisicing dolor exercitation laborum. Sit consequat reprehenderit cillum ut non ex aute sint. Lorem ea eu fugiat adipisicing nostrud cillum duis dolore incididunt nostrud nisi veniam aliqua occaecat.\r\n", + "command": "Strozen-nicoleherrera@strozen.com" + }, + { + "id": 12, + "name": "Joyner", + "description": "Dolore enim sunt adipisicing et reprehenderit. Incididunt laborum elit commodo duis sint Lorem fugiat dolore in cupidatat sunt commodo. Adipisicing do officia eiusmod adipisicing amet eu reprehenderit non incididunt excepteur ipsum sit. Et duis irure pariatur tempor laborum ad minim id enim fugiat exercitation aute mollit eu. Sint velit ipsum sint pariatur dolor aliquip sint irure adipisicing eu. Deserunt non eu dolor mollit irure elit consectetur voluptate sint ut dolore cillum do ullamco. Magna amet cillum est culpa aliquip occaecat eiusmod non est laborum non officia aliqua non.\r\n", + "command": "Rubadub-joynerherrera@rubadub.com" + }, + { + "id": 13, + "name": "Isabel", + "description": "Ad culpa esse mollit quis laborum deserunt consequat ad aliqua qui irure reprehenderit est mollit. Irure mollit cillum nostrud non. Ullamco laboris deserunt est et. Irure consectetur pariatur laboris in sunt aliqua ex reprehenderit duis laboris aliquip deserunt laborum. Pariatur ex veniam nulla ipsum amet excepteur qui dolor commodo. Incididunt aliqua nostrud et dolore nisi cupidatat dolore eu minim deserunt aute velit. Duis occaecat et duis commodo ex magna officia pariatur consectetur exercitation irure ullamco proident consectetur.\r\n", + "command": "Assurity-isabelherrera@assurity.com" + }, + { + "id": 14, + "name": "Jayne", + "description": "Ad cupidatat adipisicing occaecat ea magna mollit ex esse nostrud velit officia. Aliqua commodo enim commodo adipisicing cillum est in quis. Mollit sint ea adipisicing enim ea labore et. Cillum aliquip dolore elit aute sit sint incididunt exercitation dolore voluptate ut. Aliqua nostrud consectetur minim reprehenderit eu aliquip elit nulla consectetur nisi reprehenderit ullamco. Nostrud consectetur sunt in nostrud nostrud consectetur officia aliqua esse Lorem.\r\n", + "command": "Playce-jayneherrera@playce.com" + }, + { + "id": 15, + "name": "Tonia", + "description": "Eiusmod ex elit ullamco exercitation consectetur enim aliquip id qui dolor. Occaecat id et sit aute deserunt duis eiusmod mollit labore adipisicing. Voluptate consectetur dolore deserunt elit Lorem. Reprehenderit ex esse proident anim sunt qui pariatur in sit culpa.\r\n", + "command": "Fossiel-toniaherrera@fossiel.com" + }, + { + "id": 16, + "name": "Teresa", + "description": "Duis nostrud fugiat in duis laboris non irure quis. Non tempor reprehenderit eu incididunt occaecat exercitation incididunt excepteur deserunt cillum nulla deserunt elit sunt. Minim occaecat amet nostrud dolore Lorem labore incididunt tempor proident. Fugiat reprehenderit culpa quis pariatur eiusmod eu do aute eu sunt.\r\n", + "command": "Zosis-teresaherrera@zosis.com" + }, + { + "id": 17, + "name": "Nelda", + "description": "Nulla labore eiusmod aute aute mollit laborum mollit aliquip. Non qui id incididunt enim qui minim cupidatat aute proident est in nostrud. Id consequat eiusmod officia ex veniam elit occaecat veniam commodo eiusmod ullamco tempor. Pariatur exercitation sint sint anim nulla. Deserunt dolore enim cupidatat exercitation mollit elit magna magna Lorem mollit.\r\n", + "command": "Uneeq-neldaherrera@uneeq.com" + }, + { + "id": 18, + "name": "Farley", + "description": "Qui sit veniam nostrud consequat deserunt fugiat laboris occaecat. Laboris nisi sit velit velit aliqua ea labore amet sunt ullamco cillum. Laborum Lorem exercitation laboris consequat exercitation occaecat cupidatat ex elit officia aliqua deserunt culpa ea.\r\n", + "command": "Shopabout-farleyherrera@shopabout.com" + }, + { + "id": 19, + "name": "Porter", + "description": "Ipsum anim pariatur eiusmod sint sunt labore. Reprehenderit adipisicing laborum occaecat anim labore laboris mollit eiusmod minim et. Nisi dolor in dolor est nostrud. Commodo qui laborum ipsum occaecat esse est. Veniam velit mollit aute qui.\r\n", + "command": "Artiq-porterherrera@artiq.com" + }, + { + "id": 20, + "name": "Teri", + "description": "Lorem aute nostrud ipsum excepteur ipsum Lorem nulla eiusmod do. Eiusmod tempor nostrud commodo enim consectetur qui ut irure excepteur do duis reprehenderit amet tempor. Et in laboris exercitation anim sunt voluptate est cillum ullamco mollit reprehenderit anim ut.\r\n", + "command": "Omnigog-teriherrera@omnigog.com" + }, + { + "id": 21, + "name": "Miranda", + "description": "Ut dolore fugiat ex anim minim amet magna commodo mollit dolor excepteur reprehenderit laboris dolor. Amet consectetur anim reprehenderit do. Consectetur eu sit mollit culpa elit irure. Id exercitation ea nostrud cillum nostrud fugiat fugiat pariatur nulla culpa labore. Pariatur exercitation minim laboris cillum esse ullamco ut occaecat est. Excepteur officia officia officia consectetur tempor esse do. Nisi sit eiusmod ex anim ullamco non labore culpa.\r\n", + "command": "Comverges-mirandaherrera@comverges.com" + }, + { + "id": 22, + "name": "Hutchinson", + "description": "Et cupidatat aliquip culpa qui. Labore in sunt ut deserunt irure exercitation amet cupidatat aliqua nostrud velit sit aliquip ea. Ad aute consequat eiusmod non irure voluptate incididunt officia ipsum aliqua anim occaecat irure. Officia quis consectetur ea pariatur incididunt voluptate occaecat. Velit exercitation id mollit minim proident nostrud mollit adipisicing sint dolore.\r\n", + "command": "Zolavo-hutchinsonherrera@zolavo.com" + }, + { + "id": 23, + "name": "Woods", + "description": "Sunt sint laborum enim irure officia. Occaecat dolor velit esse adipisicing reprehenderit culpa laboris aute deserunt laboris exercitation. Aliqua elit ipsum reprehenderit laboris eu ad. Dolore mollit ullamco aute dolore dolore mollit dolore cillum incididunt aliquip qui tempor. Veniam ut labore in magna ullamco reprehenderit proident labore ea fugiat Lorem exercitation. Esse anim sint duis id eiusmod non eiusmod Lorem et. Pariatur sit cupidatat aliquip id reprehenderit minim nisi enim ipsum dolor cillum.\r\n", + "command": "Magnina-woodsherrera@magnina.com" + }, + { + "id": 24, + "name": "Minnie", + "description": "Velit elit cupidatat culpa culpa in commodo labore excepteur eu. Irure et dolore nulla adipisicing. Magna consectetur nisi do est veniam fugiat.\r\n", + "command": "Grupoli-minnieherrera@grupoli.com" + }, + { + "id": 25, + "name": "Faulkner", + "description": "Sunt aliquip quis quis consequat mollit aliqua laboris qui elit id duis et. Cupidatat officia excepteur anim non deserunt velit. Deserunt voluptate ex velit officia sit dolore minim tempor et. Et in dolor laboris dolore. Elit exercitation aliqua amet sint ut et cillum tempor proident incididunt sit. Dolore elit nisi proident anim exercitation tempor quis. Enim Lorem in laboris ipsum proident pariatur consectetur.\r\n", + "command": "Plasmosis-faulknerherrera@plasmosis.com" + }, + { + "id": 26, + "name": "Maude", + "description": "Consectetur esse Lorem id officia reprehenderit quis. Labore magna nisi deserunt sint officia cillum. Incididunt aliqua veniam officia incididunt elit mollit aliqua qui reprehenderit cillum anim. Qui tempor eu ex anim tempor amet minim dolore elit ipsum Lorem quis cupidatat. Reprehenderit velit quis aliquip Lorem incididunt.\r\n", + "command": "Jamnation-maudeherrera@jamnation.com" + }, + { + "id": 27, + "name": "Roberson", + "description": "Mollit sint aliqua veniam velit consectetur consequat. Ex fugiat labore reprehenderit in cupidatat nostrud adipisicing. Do sit nulla do nisi voluptate nostrud nostrud magna laboris.\r\n", + "command": "Kyagoro-robersonherrera@kyagoro.ru" + }, + { + "id": 28, + "name": "Debra", + "description": "Ex sit laboris duis velit culpa cupidatat nisi consectetur reprehenderit quis. Amet ea dolore irure nulla proident deserunt sint sint adipisicing pariatur sint elit. Non commodo aute Lorem cupidatat sit labore excepteur velit nostrud ut ea. Pariatur non qui sunt mollit amet veniam deserunt ullamco laborum est sint. Et minim fugiat irure dolor aliqua ut proident amet adipisicing culpa minim.\r\n", + "command": "Magneato-debraherrera@magneato.ru" + }, + { + "id": 29, + "name": "Pacheco", + "description": "Pariatur excepteur proident anim cillum dolore consectetur ipsum deserunt cupidatat cillum pariatur sunt officia laborum. Dolor voluptate culpa et elit pariatur ea. Voluptate velit ex commodo sunt exercitation ea nostrud quis elit est velit amet. Laboris est magna incididunt non sit minim culpa anim non proident occaecat irure veniam non. Ut quis laboris aliquip do tempor dolore ipsum reprehenderit deserunt deserunt. Do labore consectetur eu quis excepteur nulla anim consectetur ut dolore elit.\r\n", + "command": "Keengen-pachecoherrera@keengen.com" + }, + { + "id": 30, + "name": "Wyatt", + "description": "Commodo ex sint et amet tempor veniam culpa pariatur Lorem proident ut sint. Commodo anim occaecat anim in minim occaecat velit. Anim id id ea ex aliqua deserunt minim duis tempor ullamco voluptate esse. Duis mollit esse minim excepteur et consequat. Laborum voluptate culpa deserunt anim et nulla enim qui anim laborum anim exercitation. In dolor elit culpa velit ullamco aute anim Lorem excepteur esse magna Lorem exercitation.\r\n", + "command": "Newcube-wyattherrera@newcube.com" + }, + { + "id": 31, + "name": "Laurel", + "description": "Laborum exercitation aliqua occaecat labore. In velit ea fugiat elit eu quis sint duis elit officia irure enim proident tempor. Minim do quis mollit elit. Ipsum tempor laborum exercitation mollit ut qui tempor laboris magna fugiat qui sit aliqua quis. Elit velit aliquip ipsum esse quis anim pariatur anim irure minim. Veniam laborum id commodo et mollit Lorem aliqua laborum ipsum magna. Occaecat amet fugiat proident in.\r\n", + "command": "Escenta-laurelherrera@escenta.org" + }, + { + "id": 32, + "name": "Hopper", + "description": "Do consequat magna in quis quis. Veniam esse tempor velit ex irure. Minim reprehenderit excepteur sint mollit sit. Incididunt nisi velit minim ad anim nisi incididunt mollit culpa proident.\r\n", + "command": "Portalis-hopperherrera@portalis.com" + }, + { + "id": 33, + "name": "Guzman", + "description": "Lorem in ipsum elit eiusmod. Nostrud officia veniam voluptate culpa anim eu ex qui duis do quis ea quis aliquip. Do dolore aliqua qui enim sunt do exercitation non non esse nostrud eu. Aliqua enim consectetur sunt minim non. Eu anim sunt ipsum id aliquip labore laboris exercitation. Commodo cupidatat exercitation occaecat ea anim sint pariatur incididunt velit minim eu.\r\n", + "command": "Jimbies-guzmanherrera@jimbies.ru" + }, + { + "id": 34, + "name": "Amber", + "description": "Nostrud reprehenderit culpa veniam eu ipsum et id nostrud. Deserunt do aliquip exercitation enim ullamco incididunt fugiat in nostrud exercitation anim. Dolor cillum nisi nostrud ipsum reprehenderit excepteur quis adipisicing mollit voluptate consequat ea occaecat. Minim mollit non in fugiat magna voluptate.\r\n", + "command": "Zaggles-amberherrera@zaggles.ru" + }, + { + "id": 35, + "name": "Ruthie", + "description": "Deserunt nostrud dolor labore laboris. Elit pariatur non cillum minim veniam. Nisi incididunt cillum in consectetur enim nulla ea dolore pariatur. Velit magna in ex exercitation voluptate eiusmod velit eiusmod nisi in elit nisi cillum. Non do labore aute eiusmod ut et nulla commodo.\r\n", + "command": "Boilicon-ruthieherrera@boilicon.org" + }, + { + "id": 36, + "name": "Billie", + "description": "Consectetur minim pariatur duis dolor cupidatat laboris occaecat nisi veniam reprehenderit laborum veniam et. Officia ullamco fugiat proident cupidatat fugiat ut culpa minim sint. Elit eiusmod id labore voluptate sit tempor ea cupidatat aute voluptate anim. Do laboris dolor elit duis nulla aliquip incididunt amet duis magna dolore esse. Eiusmod eiusmod nisi enim aute aliquip do ea laborum duis voluptate nostrud deserunt officia.\r\n", + "command": "Geekko-billieherrera@geekko.com" + }, + { + "id": 37, + "name": "Lindsay", + "description": "Ea laboris exercitation tempor amet mollit in ea excepteur ut excepteur quis. Sit ipsum ipsum ullamco aliquip anim cillum veniam. Do incididunt excepteur duis ullamco aute quis elit minim ipsum.\r\n", + "command": "Rocklogic-lindsayherrera@rocklogic.org" + }, + { + "id": 38, + "name": "Mercer", + "description": "Mollit labore aliqua tempor pariatur veniam duis enim laborum. Ipsum pariatur ut do ullamco velit ea nisi adipisicing ullamco est ea elit laboris pariatur. Ex consequat do deserunt pariatur mollit ut ipsum incididunt aute sit proident reprehenderit eu.\r\n", + "command": "Satiance-mercerherrera@satiance.org" + }, + { + "id": 39, + "name": "Benson", + "description": "Proident est aliqua eu nisi tempor ut veniam irure. Sunt sit ex enim elit consequat eu officia cillum occaecat esse. Lorem id magna et veniam et ullamco adipisicing nostrud irure Lorem. Velit proident occaecat eiusmod ullamco nisi commodo. Amet velit ex commodo qui id exercitation amet fugiat reprehenderit velit magna consequat quis. Laborum minim exercitation esse veniam ad do qui duis mollit adipisicing voluptate nostrud cupidatat cupidatat. Reprehenderit ipsum ex nostrud do ex sit excepteur mollit aute.\r\n", + "command": "Lyria-bensonherrera@lyria.com" + }, + { + "id": 40, + "name": "Melisa", + "description": "Mollit esse sunt ullamco excepteur cillum non cupidatat laborum consequat enim ea. Amet quis incididunt cupidatat incididunt excepteur voluptate sit. Culpa proident laboris occaecat fugiat labore id eu aute magna ea irure commodo laborum voluptate. Ut excepteur esse sint culpa reprehenderit sit. Officia tempor elit dolore ex sint culpa. Nostrud ipsum et dolore aliquip magna laboris aliquip commodo qui consequat in commodo amet. Velit proident non esse reprehenderit excepteur consectetur eu irure.\r\n", + "command": "Genmy-melisaherrera@genmy.com" + }, + { + "id": 41, + "name": "Amber", + "description": "Et tempor cupidatat consequat veniam minim labore quis duis incididunt quis anim. Ut incididunt fugiat pariatur ipsum occaecat dolore ea aute id nisi commodo. Amet eiusmod proident dolore Lorem. Ut consequat officia sint nostrud est in occaecat esse.\r\n", + "command": "Asimiline-ellenherrera@asimiline.org" + }, + { + "id": 42, + "name": "Jocelyn", + "description": "Est anim mollit aliqua qui est cillum. Est voluptate eu laboris ea adipisicing occaecat nostrud aliquip exercitation cillum deserunt sit cillum. Cillum sunt fugiat sit veniam dolore velit minim cillum occaecat ut ex aliqua ipsum.\r\n", + "command": "Insurety-jocelynherrera@insurety.org" + }, + { + "id": 43, + "name": "Mccullough", + "description": "Ullamco aliqua et id et minim. Est amet deserunt adipisicing cupidatat ipsum quis tempor. Irure deserunt amet aliquip officia id. Quis deserunt dolore aute quis aliqua irure voluptate ex qui ad. Sint fugiat do nisi sunt ad laborum officia reprehenderit. Non commodo velit amet enim mollit occaecat tempor quis eiusmod ipsum ex commodo sunt.\r\n", + "command": "Straloy-mcculloughherrera@straloy.com" + }, + { + "id": 44, + "name": "Lisa", + "description": "Nostrud fugiat in commodo ex tempor. Veniam excepteur mollit eiusmod ut labore quis sint qui nisi. Voluptate elit labore nostrud est pariatur do amet commodo consectetur.\r\n", + "command": "Flyboyz-lisaherrera@flyboyz.com" + }, + { + "id": 45, + "name": "Abigail", + "description": "Deserunt nisi irure ex culpa ut eu commodo tempor proident qui. Magna nostrud amet qui ipsum et ipsum. Est cillum ullamco voluptate labore laborum quis ut et consectetur reprehenderit sint. Sint nostrud fugiat consectetur mollit nisi excepteur veniam excepteur tempor adipisicing dolor amet. Proident commodo exercitation dolore reprehenderit quis quis in aliquip est veniam sit quis ipsum nisi. Laborum ut pariatur in enim cupidatat enim deserunt adipisicing irure ut tempor non consequat aute. Ea cupidatat excepteur sunt eiusmod.\r\n", + "command": "Overfork-abigailherrera@overfork.com" + }, + { + "id": 46, + "name": "Estrada", + "description": "Aute qui nisi adipisicing qui ad laborum sit duis deserunt sint in consequat. Dolor irure mollit eu nostrud officia. Sit amet irure ea nisi in mollit amet ad laborum ea nulla ex tempor. Voluptate labore est exercitation proident ullamco magna magna pariatur. Fugiat enim eu officia ut ullamco quis officia proident commodo. Laborum incididunt consectetur eiusmod nulla qui. Anim commodo eiusmod sunt fugiat ea amet ex et in pariatur mollit mollit.\r\n", + "command": "Typhonica-estradaherrera@typhonica.com" + }, + { + "id": 47, + "name": "Amber", + "description": "Aute quis cillum id nostrud labore duis aliquip duis commodo officia irure amet Lorem. Esse voluptate amet cillum adipisicing amet esse nulla deserunt. Aliquip eu quis pariatur reprehenderit et occaecat excepteur ut do consequat consequat reprehenderit et. Aliqua minim exercitation consectetur laboris enim minim ex voluptate quis. Laboris pariatur dolore eiusmod Lorem nulla tempor reprehenderit quis nisi enim aute. Non velit sit pariatur officia minim laboris minim irure sunt irure ea ex anim culpa.\r\n", + "command": "Vinch-susanaherrera@vinch.com" + }, + { + "id": 48, + "name": "Joseph", + "description": "Elit et exercitation et officia ad mollit consectetur id laborum consectetur adipisicing sint quis. Aliqua deserunt eiusmod voluptate ex. Ipsum nulla aliquip occaecat in minim do in adipisicing sunt in minim.\r\n", + "command": "Jumpstack-josephherrera@jumpstack.ru" + }, + { + "id": 49, + "name": "Johnnie", + "description": "Aliqua Lorem in ea ea duis. Consectetur ex laborum non nulla enim magna elit qui exercitation commodo consectetur. Consequat irure mollit veniam consectetur tempor enim aliquip amet nulla aliquip excepteur reprehenderit. Reprehenderit excepteur eu nulla cillum excepteur nulla velit consequat anim. Amet ut aute et ut aliqua. Consequat nostrud duis in eiusmod quis consectetur ad nostrud nostrud do exercitation occaecat dolore qui. Tempor sint ad do consequat.\r\n", + "command": "Biohab-johnnieherrera@biohab.ru" + }, + { + "id": 50, + "name": "Amber", + "description": "Aliquip aliqua nostrud veniam est. Laborum ut et nostrud proident qui proident. Esse quis amet in culpa sint tempor proident consectetur velit incididunt reprehenderit. Eu sint exercitation qui do ullamco.\r\n", + "command": "Luxuria-estesherrera@luxuria.ru" + } +] \ No newline at end of file diff --git a/flask__webservers/filter-sort-paginate-on-server/main.py b/flask__webservers/filter-sort-paginate-on-server/main.py new file mode 100644 index 000000000..314282a7a --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/main.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# NOTE: https://datatables.net/manual/server-side + + +import json +import re +from functools import cmp_to_key +from pathlib import Path +from typing import Any + +from flask import Flask, render_template, jsonify, Response, request + +# pip install querystring-parser==1.2.4 +from querystring_parser import parser + + +def cmp(a: Any, b: Any) -> int: + return (a > b) - (a < b) + + +ITEMS = json.load(open("items.json", encoding="utf-8")) + + +app = Flask(__name__) + + +@app.route("/") +def index() -> str: + return render_template( + "index.html", + title=Path(__file__).resolve().parent.name, + ) + + +@app.route("/api/get_items") +def api_get_items() -> Response: + args: dict[str, Any] = parser.parse(request.query_string) + + search_value: str = args["search"]["value"] + has_search_regex: str = args["search"]["regex"] == "true" + + # TODO: Фильтрация может быть по отдельным столбцам в поле search + # { + # 0: {'data': 'id', 'name': 'id', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 1: {'data': 'name', 'name': 'name', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 2: {'data': 'description', 'name': 'description', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 3: {'data': 'command', 'name': 'command', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}} + # } + # columns: dict[int, dict[str, str]] = args["columns"] + print("columns:", args["columns"]) + + filtered_items: list[dict[str, Any]] = [] + for item in ITEMS: + if search_value: + values: list[str] = [str(x).lower() for x in item.values()] + if not any( + ( + re.search(search_value, value) + if has_search_regex + else search_value.lower() in value + ) + for value in values + ): + continue + + filtered_items.append(item) + + number_of_filtered = len(filtered_items) + + order: dict[int, dict[str, str]] = args.get("order", dict()) + print("order:", order) + if order: + def cmp2(item1: dict[str, str], item2: dict[str, str]) -> int: + # NOTE: https://stackoverflow.com/a/62381089/5909792 + total_result: int | None = None + for order_column in order.values(): + name: str = order_column["name"] + result = cmp(item1[name], item2[name]) + if order_column["dir"] == "desc": + result = -result + + if total_result is None: + total_result = result + else: + total_result = total_result or result + + return total_result + + filtered_items.sort(key=cmp_to_key(cmp2)) + + start: int = int(args["start"]) + length: int = int(args["length"]) + if length > 0: + filtered_items = filtered_items[start : start + length] + + return jsonify( + { + "draw": int(args["draw"]), + "recordsTotal": len(ITEMS), + "recordsFiltered": number_of_filtered, + "data": filtered_items, + } + ) + + +if __name__ == "__main__": + app.run() diff --git a/flask__webservers/filter-sort-paginate-on-server/static/index.js b/flask__webservers/filter-sort-paginate-on-server/static/index.js new file mode 100644 index 000000000..498ec7edf --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/static/index.js @@ -0,0 +1,22 @@ +$(function() { + new DataTable('table', { + ajax: 'api/get_items', + rowId: 'id', + serverSide: true, + processing: true, + lengthMenu: [ + [5, 10, 25, 50, -1], + ["5 records", "10 records", "25 records", "50 records", "All records"] + ], + columns: [ + { name: 'id', data: 'id', title: 'Id', }, + { name: 'name', data: 'name', title: 'Name' }, + { name: 'description', data: 'description', title: 'Description', }, + { name: 'command', data: 'command', title: 'Command', } + ], + order: [ + // Сортировка по убыванию id + [0, "desc"], + ], + }); +}); diff --git a/flask__webservers/filter-sort-paginate-on-server/templates/index.html b/flask__webservers/filter-sort-paginate-on-server/templates/index.html new file mode 100644 index 000000000..911c791f4 --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/templates/index.html @@ -0,0 +1,18 @@ + + + + + + + {{ title }} + + + + + + +
+ + + + diff --git a/flask__webservers/flask_debugtoolbar__examples/app.py b/flask__webservers/flask_debugtoolbar__examples/app.py new file mode 100644 index 000000000..85a51fbea --- /dev/null +++ b/flask__webservers/flask_debugtoolbar__examples/app.py @@ -0,0 +1,60 @@ +# Run using: `FLASK_ENV=development flask run` + +from flask import Flask, render_template, redirect, url_for +from flask_sqlalchemy import SQLAlchemy +from flask_debugtoolbar import DebugToolbarExtension + + +app = Flask(__name__) +app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = True +# app.config['DEBUG_TB_PANELS'] = ( +# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel', +# 'flask_debugtoolbar.panels.logger.LoggingPanel', +# 'flask_debugtoolbar.panels.timer.TimerDebugPanel', +# ) +# app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' ) +app.config["SECRET_KEY"] = "asd" + +# TODO: This can be removed once flask_sqlalchemy 3.0 ships +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.db" +db = SQLAlchemy(app) + + +class ExampleModel(db.Model): + __tablename__ = "examples" + value = db.Column(db.String(100), primary_key=True) + + +@app.before_first_request +def setup() -> None: + db.create_all() + + +@app.route("/") +def index(): + app.logger.info("Hello there") + ExampleModel.query.get(1) + return render_template("index.html") + + +@app.route("/redirect") +def redirect_example(): + response = redirect(url_for("index")) + response.set_cookie("test_cookie", "1") + return response + + +if __name__ == "__main__": + app.debug = True + + toolbar = DebugToolbarExtension(app) + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=5000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/flask_debugtoolbar__examples/hello_world.py b/flask__webservers/flask_debugtoolbar__examples/hello_world.py new file mode 100644 index 000000000..d6846fb15 --- /dev/null +++ b/flask__webservers/flask_debugtoolbar__examples/hello_world.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging + +from flask import Flask +from flask_debugtoolbar import DebugToolbarExtension + + +app = Flask(__name__) + +# set a 'SECRET_KEY' to enable the Flask session cookies +app.config["SECRET_KEY"] = "" + + +@app.route("/") +def index() -> str: + # NOTE: Need tab body: "Could not insert debug toolbar. tag not found in response." + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + if app.debug: + logging.basicConfig(level=logging.DEBUG) + + toolbar = DebugToolbarExtension(app) + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=5000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/flask_debugtoolbar__examples/templates/index.html b/flask__webservers/flask_debugtoolbar__examples/templates/index.html new file mode 100644 index 000000000..3ed98a698 --- /dev/null +++ b/flask__webservers/flask_debugtoolbar__examples/templates/index.html @@ -0,0 +1,9 @@ + + + Flask-debug-toolbar + + +

Flask-debug-toolbar

+ Redirect example + + \ No newline at end of file diff --git a/flask__webservers/generate_qrcode/main.py b/flask__webservers/generate_qrcode/main.py index 80f7b34a2..01e022eb4 100644 --- a/flask__webservers/generate_qrcode/main.py +++ b/flask__webservers/generate_qrcode/main.py @@ -1,27 +1,41 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import hashlib +import logging import os -from flask import Flask, request, jsonify, render_template_string, send_from_directory, url_for, redirect +from flask import ( + Flask, + request, + jsonify, + render_template_string, + send_from_directory, + url_for, + redirect, +) + +# pip install qrcode +import qrcode -UPLOAD_FOLDER = 'images' + +UPLOAD_FOLDER = "images" app = Flask(__name__) -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True) -import logging logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -84,43 +98,44 @@ def index(): -''') +""" + ) -@app.route("/generate_qrcode", methods=['GET', 'POST']) +@app.route("/generate_qrcode", methods=["GET", "POST"]) def generate_qrcode(): - print('request.args:', request.args) - print('request.form:', request.form) + print("request.args:", request.args) + print("request.form:", request.form) print() - text = request.args.get('text', None) + text = request.args.get("text", None) if not text: - text = request.form.get('text') + text = request.form.get("text") - download = 'download' in request.args - is_redirect = 'redirect' in request.args - as_image = 'as_image' in request.args + download = "download" in request.args + is_redirect = "redirect" in request.args + as_image = "as_image" in request.args if not text: - return render_template_string("""\ + return render_template_string( + """\ Example:
{{ example_uri }}
{{ example_uri }}&download
{{ example_uri }}&redirect
{{ example_uri }}&as_image - """, example_uri='/generate_qrcode?text=Hello World!') + """, + example_uri="/generate_qrcode?text=Hello World!", + ) - import hashlib algo = hashlib.md5(text.encode()) hash_data = algo.hexdigest() - file_name = hash_data + '.png' - upload_folder = app.config['UPLOAD_FOLDER'] + file_name = hash_data + ".png" + upload_folder = app.config["UPLOAD_FOLDER"] abs_file_name = os.path.join(upload_folder, file_name) if not os.path.exists(abs_file_name): - # pip install qrcode - import qrcode img = qrcode.make(text) img.save(abs_file_name) @@ -141,28 +156,26 @@ def generate_qrcode(): # Вернется json c ссылкой на картинку return jsonify({ # url_for составляет путь для функции images, которая возвращает картинку с сервера - 'file_name': uri, + "file_name": uri, }) -@app.route('/' + UPLOAD_FOLDER + '/') +@app.route("/" + UPLOAD_FOLDER + "/") def images(file_name): - download = 'download' in request.args - return send_from_directory(app.config['UPLOAD_FOLDER'], file_name, as_attachment=download) + download = "download" in request.args + return send_from_directory( + app.config["UPLOAD_FOLDER"], file_name, as_attachment=download + ) if __name__ == "__main__": app.debug = True # Localhost - app.run( - port=5001, - threaded=True, - ) + app.run(port=5001) # # Public IP # app.run( # host='0.0.0.0', - # port=5000, - # threaded=True, + # port=5000 # ) diff --git a/flask__webservers/generate_table/main.py b/flask__webservers/generate_table/main.py index a3faef338..3c8f6c18c 100644 --- a/flask__webservers/generate_table/main.py +++ b/flask__webservers/generate_table/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, render_template_string + + app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) @@ -24,9 +26,10 @@ def index(): items.append(row) - items[0][0] = '' + items[0][0] = "" - return render_template_string("""\ + return render_template_string( + """\ @@ -72,24 +75,18 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/get_URL__parameter_argument_query.py b/flask__webservers/get_URL__parameter_argument_query.py index f659b31eb..1145a004d 100644 --- a/flask__webservers/get_URL__parameter_argument_query.py +++ b/flask__webservers/get_URL__parameter_argument_query.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -47,13 +48,13 @@ def print_args(): print(args) text = '' - text += '' - text += '
URL ARGUMENTS:' + text += "URL ARGUMENTS:" for k, v in args.items(): - text += f'
{k}{v}
' + text += f"{k}{v}" + text += "" return text -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/get_exif_info/main.py b/flask__webservers/get_exif_info/main.py index b425cc5d4..702ecf392 100644 --- a/flask__webservers/get_exif_info/main.py +++ b/flask__webservers/get_exif_info/main.py @@ -1,26 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import base64 +import logging + +from flask import Flask, jsonify, render_template_string, redirect, request + +# pip install exifread +import exifread # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/print_exif/main.py def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags - # pip install exifread - import exifread tags = exifread.process_file(file_object_or_file_name) tags_by_value = dict() if not tags: - print('Not tags') + print("Not tags") return tags_by_value - print('Tags ({}):'.format(len(tags))) + print(f"Tags ({len(tags)}):") for tag, value in tags.items(): # Process value @@ -29,9 +36,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -43,19 +50,18 @@ def get_exif_tags(file_object_or_file_name, as_category=True): except: # Example tag JPEGThumbnail if type(value) == bytes: - import base64 value = base64.b64encode(value).decode() - print(' "{}": {}'.format(tag, value)) + print(f' "{tag}": {value}') if not as_category: tags_by_value[tag] = value else: # Fill categories_by_tag - if ' ' in tag: - ''.split() - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + "".split() + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -70,16 +76,15 @@ def get_exif_tags(file_object_or_file_name, as_category=True): return tags_by_value -from flask import Flask, jsonify, render_template_string, redirect, request app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -211,37 +216,30 @@ def index(): -''') +""" + ) -@app.route("/get_exif", methods=['POST']) +@app.route("/get_exif", methods=["POST"]) def get_exif(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] categories_by_tag = get_exif_tags(file.stream) return jsonify(categories_by_tag) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/get_size_upload_file/main.py b/flask__webservers/get_size_upload_file/main.py index 797ea23ec..761086f62 100644 --- a/flask__webservers/get_size_upload_file/main.py +++ b/flask__webservers/get_size_upload_file/main.py @@ -1,30 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, request, redirect, render_template_string, jsonify -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) +from flask import Flask, request, redirect, render_template_string, jsonify + +# pip install humanize +from humanize import naturalsize as sizeof_fmt -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/human_byte_size.py -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - num /= 1024.0 +app = Flask(__name__) - return "%3.1f %s" % (num, 'TB') +logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -91,42 +87,35 @@ def index(): -''') +""" + ) -@app.route('/get_file_size', methods=['POST']) +@app.route("/get_file_size", methods=["POST"]) def get_file_size(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") length = 0 - file = request.files['file'] + file = request.files["file"] if file: data = file.stream.read() length = len(data) - return jsonify({'length': length, 'length_human': sizeof_fmt(length)}) + return jsonify({"length": length, "length_human": sizeof_fmt(length)}) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/get_size_upload_file_with_progress/main.py b/flask__webservers/get_size_upload_file_with_progress/main.py index 5da512b07..35ae0b78b 100644 --- a/flask__webservers/get_size_upload_file_with_progress/main.py +++ b/flask__webservers/get_size_upload_file_with_progress/main.py @@ -1,30 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, request, redirect, render_template_string, jsonify -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) +from flask import Flask, request, redirect, render_template_string, jsonify + +# pip install humanize +from humanize import naturalsize as sizeof_fmt -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/human_byte_size.py -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - num /= 1024.0 +app = Flask(__name__) - return "%3.1f %s" % (num, 'TB') +logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -122,42 +118,35 @@ def index(): -''') +""" + ) -@app.route('/get_file_size', methods=['POST']) +@app.route("/get_file_size", methods=["POST"]) def get_file_size(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") length = 0 - file = request.files['file'] + file = request.files["file"] if file: data = file.stream.read() length = len(data) - return jsonify({'length': length, 'length_human': sizeof_fmt(length)}) + return jsonify({"length": length, "length_human": sizeof_fmt(length)}) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/get_upload_and_image_process/commands.py b/flask__webservers/get_upload_and_image_process/commands.py index c25dacbbf..ae059f2f0 100644 --- a/flask__webservers/get_upload_and_image_process/commands.py +++ b/flask__webservers/get_upload_and_image_process/commands.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageOps, ImageFilter @@ -10,12 +10,12 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/4f6d5b013d218cfe5b964af17a2540e2e49adca0/pil_example/invert/main.py def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) @@ -34,8 +34,14 @@ def invert_gray(img): def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), + Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), + Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -43,15 +49,15 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image def jackal_jpg(img): data_io = io.BytesIO() - img.save(data_io, format='JPEG', quality=1) + img.save(data_io, format="JPEG", quality=1) return Image.open(data_io) diff --git a/flask__webservers/get_upload_and_image_process/main.py b/flask__webservers/get_upload_and_image_process/main.py index 6a69f428b..6c7b48f64 100644 --- a/flask__webservers/get_upload_and_image_process/main.py +++ b/flask__webservers/get_upload_and_image_process/main.py @@ -1,34 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import base64 +import logging +import io + +import requests + +from flask import Flask, jsonify, render_template_string, redirect, request +from PIL import Image + # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/4516206d6e29608a732b7c096cd557b11c7ce67b/telegram_bot__image_process_bot/commands.py from commands import invert, gray, invert_gray, pixelate, jackal_jpg, thumbnail, blur + + COMMANDS = { - 'invert': invert, - 'gray': gray, - 'invert_gray': invert_gray, - 'pixelate': pixelate, - 'pixelate16': lambda img: pixelate(img, 16), - 'pixelate32': lambda img: pixelate(img, 32), - 'pixelate48': lambda img: pixelate(img, 48), - 'jackal_jpg': jackal_jpg, - 'thumbnail32': lambda img: thumbnail(img, (32, 32)), - 'thumbnail64': lambda img: thumbnail(img, (64, 64)), - 'thumbnail128': lambda img: thumbnail(img, (128, 129)), - 'blur': blur, - 'blur5': lambda img: blur(img, 5) + "invert": invert, + "gray": gray, + "invert_gray": invert_gray, + "pixelate": pixelate, + "pixelate16": lambda img: pixelate(img, 16), + "pixelate32": lambda img: pixelate(img, 32), + "pixelate48": lambda img: pixelate(img, 48), + "jackal_jpg": jackal_jpg, + "thumbnail32": lambda img: thumbnail(img, (32, 32)), + "thumbnail64": lambda img: thumbnail(img, (64, 64)), + "thumbnail128": lambda img: thumbnail(img, (128, 129)), + "blur": blur, + "blur5": lambda img: blur(img, 5), } # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/img_to_base64_html/main.py -def img_to_base64_html(file_name__or__bytes__or__file_object): +def img_to_base64_html(file_name__or__bytes__or__file_object) -> str: arg = file_name__or__bytes__or__file_object if type(arg) == str: - with open(arg, mode='rb') as f: + with open(arg, mode="rb") as f: img_bytes = f.read() elif type(arg) == bytes: @@ -37,22 +48,15 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): else: img_bytes = arg.read() - import io bytes_io = io.BytesIO(img_bytes) - - from PIL import Image img = Image.open(bytes_io) - import base64 - img_base64 = base64.b64encode(img_bytes).decode('utf-8') + img_base64 = base64.b64encode(img_bytes).decode("utf-8") # print(img_base64) - return 'data:image/{};base64,'.format(img.format.lower()) + img_base64 + return f"data:image/{img.format.lower()};base64,{img_base64}" -import requests - -from flask import Flask, jsonify, render_template_string, redirect, request app = Flask(__name__) # http://flask.pocoo.org/docs/0.12/config/#config @@ -60,28 +64,28 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): # ensure that independent of the hash seed of the dictionary the return value will be consistent to not trash external # HTTP caches. You can override the default behavior by changing this variable. This is not recommended but might give # you a performance improvement on the cost of cacheability. -app.config['JSON_SORT_KEYS'] = False +app.config["JSON_SORT_KEYS"] = False -import logging logging.basicConfig(level=logging.DEBUG) -LAST_IMAGE = 'last_image.jpg' +LAST_IMAGE = "last_image.jpg" -def save_last_image(file_data): - with open(LAST_IMAGE, 'wb') as f: +def save_last_image(file_data) -> None: + with open(LAST_IMAGE, "wb") as f: f.write(file_data) -def load_last_image(): - with open(LAST_IMAGE, 'rb') as f: +def load_last_image() -> bytes: + with open(LAST_IMAGE, "rb") as f: return f.read() -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -276,88 +280,78 @@ def index(): -''', commands=COMMANDS) +""", + commands=COMMANDS, + ) -@app.route("/upload_file", methods=['POST']) +@app.route("/upload_file", methods=["POST"]) def upload_file(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] file_data = file.stream.read() save_last_image(file_data) return jsonify({ - 'img_original': img_to_base64_html(file_data), - 'img_process': None, + "img_original": img_to_base64_html(file_data), + "img_process": None, }) -@app.route("/upload_url", methods=['POST']) +@app.route("/upload_url", methods=["POST"]) def upload_url(): print(request.form) - if 'url' not in request.form: - return redirect('/') + if "url" not in request.form: + return redirect("/") - url = request.form['url'] + url = request.form["url"] file_data = requests.get(url).content save_last_image(file_data) return jsonify({ - 'img_original': img_to_base64_html(file_data), - 'img_process': None, + "img_original": img_to_base64_html(file_data), + "img_process": None, }) -@app.route("/image_process", methods=['POST']) +@app.route("/image_process", methods=["POST"]) def image_process(): print(request.form) - command = request.form['command'] + command = request.form["command"] img_original = load_last_image() - - import io data_io = io.BytesIO(img_original) - - from PIL import Image - img = Image.open(data_io).convert('RGB') + img = Image.open(data_io).convert("RGB") # Получение и вызов функции result = COMMANDS[command](img) data_io = io.BytesIO() - result.save(data_io, 'JPEG') + result.save(data_io, "JPEG") img_process = data_io.getvalue() return jsonify({ - 'img_original': img_to_base64_html(img_original), - 'img_process': img_to_base64_html(img_process), + "img_original": img_to_base64_html(img_original), + "img_process": img_to_base64_html(img_process), }) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/get_upload_image_info/main.py b/flask__webservers/get_upload_image_info/main.py index 84ede1861..b1a2edacb 100644 --- a/flask__webservers/get_upload_image_info/main.py +++ b/flask__webservers/get_upload_image_info/main.py @@ -1,18 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import base64 +import io +import json +import logging + +# pip install exifread +import exifread + +import requests + +from flask import Flask, jsonify, render_template_string, redirect, request + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/print_exif/main.py def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags - # pip install exifread - import exifread tags = exifread.process_file(file_object_or_file_name) tags_by_value = dict() @@ -29,9 +45,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -43,7 +59,6 @@ def get_exif_tags(file_object_or_file_name, as_category=True): except: # Example tag JPEGThumbnail if type(value) == bytes: - import base64 value = base64.b64encode(value).decode() # print(' "{}": {}'.format(tag, value)) @@ -53,8 +68,8 @@ def get_exif_tags(file_object_or_file_name, as_category=True): else: # Fill categories_by_tag - if ' ' in tag: - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -70,11 +85,11 @@ def get_exif_tags(file_object_or_file_name, as_category=True): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/img_to_base64_html/main.py -def img_to_base64_html(file_name__or__bytes__or__file_object): +def img_to_base64_html(file_name__or__bytes__or__file_object) -> str: arg = file_name__or__bytes__or__file_object if type(arg) == str: - with open(arg, mode='rb') as f: + with open(arg, mode="rb") as f: img_bytes = f.read() elif type(arg) == bytes: @@ -83,28 +98,13 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): else: img_bytes = arg.read() - import io bytes_io = io.BytesIO(img_bytes) - - from PIL import Image img = Image.open(bytes_io) - import base64 - img_base64 = base64.b64encode(img_bytes).decode('utf-8') + img_base64 = base64.b64encode(img_bytes).decode("utf-8") # print(img_base64) - return 'data:image/{};base64,'.format(img.format.lower()) + img_base64 - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/human_byte_size.py -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') + return f"data:image/{img.format.lower()};base64,{img_base64}" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/84651cfefaee768851170ec4ba7d025bbaae622d/get_image_info/main.py#L84 @@ -114,50 +114,49 @@ def get_image_info(file_name__or__bytes__or__bytes_io, pretty_json_str=False): # File name if type_data == str: - with open(data, mode='rb') as f: + with open(data, mode="rb") as f: data = f.read() if type(data) == bytes: - import io data = io.BytesIO(data) length = len(data.getvalue()) exif = get_exif_tags(data) - from PIL import Image img = Image.open(data) - # Save order - from collections import OrderedDict - info = OrderedDict() - info['length'] = OrderedDict() - info['length']['value'] = length - info['length']['text'] = sizeof_fmt(length) - - info['format'] = img.format - info['mode'] = img.mode - info['channels'] = len(img.getbands()) - info['bit_color'] = { - '1': 1, 'L': 8, 'P': 8, 'RGB': 24, 'RGBA': 32, - 'CMYK': 32, 'YCbCr': 24, 'I': 32, 'F': 32 + info = dict() + info["length"] = dict() + info["length"]["value"] = length + info["length"]["text"] = sizeof_fmt(length) + + info["format"] = img.format + info["mode"] = img.mode + info["channels"] = len(img.getbands()) + info["bit_color"] = { + "1": 1, + "L": 8, + "P": 8, + "RGB": 24, + "RGBA": 32, + "CMYK": 32, + "YCbCr": 24, + "I": 32, + "F": 32, }[img.mode] - info['size'] = OrderedDict() - info['size']['width'] = img.width - info['size']['height'] = img.height + info["size"] = dict() + info["size"]["width"] = img.width + info["size"]["height"] = img.height - info['exif'] = exif + info["exif"] = exif if pretty_json_str: - import json info = json.dumps(info, indent=4, ensure_ascii=False) return info -import requests - -from flask import Flask, jsonify, render_template_string, redirect, request app = Flask(__name__) # http://flask.pocoo.org/docs/0.12/config/#config @@ -165,15 +164,15 @@ def get_image_info(file_name__or__bytes__or__bytes_io, pretty_json_str=False): # ensure that independent of the hash seed of the dictionary the return value will be consistent to not trash external # HTTP caches. You can override the default behavior by changing this variable. This is not recommended but might give # you a performance improvement on the cost of cacheability. -app.config['JSON_SORT_KEYS'] = False +app.config["JSON_SORT_KEYS"] = False -import logging logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -365,57 +364,50 @@ def index(): -''') +""" + ) -@app.route("/get_info_from_file", methods=['POST']) +@app.route("/get_info_from_file", methods=["POST"]) def get_info_from_file(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] file_data = file.stream.read() info = get_image_info(file_data) - info['img_base64'] = img_to_base64_html(file_data) + info["img_base64"] = img_to_base64_html(file_data) return jsonify(info) -@app.route("/get_info_from_url", methods=['POST']) +@app.route("/get_info_from_url", methods=["POST"]) def get_info_from_url(): print(request.form) - if 'url' not in request.form: - return redirect('/') + if "url" not in request.form: + return redirect("/") - url = request.form['url'] + url = request.form["url"] file_data = requests.get(url).content info = get_image_info(file_data) - info['img_base64'] = img_to_base64_html(file_data) + info["img_base64"] = img_to_base64_html(file_data) return jsonify(info) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/hash__md5_sha1_etc/main.py b/flask__webservers/hash__md5_sha1_etc/main.py index fe9517400..f13aaa1ba 100644 --- a/flask__webservers/hash__md5_sha1_etc/main.py +++ b/flask__webservers/hash__md5_sha1_etc/main.py @@ -1,27 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib +import logging + from flask import Flask, request, jsonify, render_template_string -import logging -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.DEBUG) app = Flask(__name__) -SUPPORTED_HASH_ALGOS = set(list(hashlib.algorithms_guaranteed) + ['md4']) -SUPPORTED_HASH_ALGOS = [algo for algo in SUPPORTED_HASH_ALGOS if not algo.lower().startswith('shake')] +SUPPORTED_HASH_ALGOS = set(list(hashlib.algorithms_guaranteed) + ["md4"]) +SUPPORTED_HASH_ALGOS = [ + algo for algo in SUPPORTED_HASH_ALGOS if not algo.lower().startswith("shake") +] SUPPORTED_HASH_ALGOS = sorted(SUPPORTED_HASH_ALGOS) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -130,46 +134,51 @@ def index(): -''', algos=SUPPORTED_HASH_ALGOS) +""", + algos=SUPPORTED_HASH_ALGOS, + ) -@app.route("/do_hash", methods=['GET', 'POST']) +@app.route("/do_hash", methods=["GET", "POST"]) def do_hash(): - print('request.args:', request.args) - print('request.form:', request.form) - print('request.files:', request.files) + print("request.args:", request.args) + print("request.form:", request.form) + print("request.files:", request.files) print() - text = request.args.get('text') + text = request.args.get("text") if not text: - text = request.form.get('text') + text = request.form.get("text") - file = request.files.get('file') + file = request.files.get("file") - algo = request.args.get('hash') + algo = request.args.get("hash") if not algo: - algo = request.form.get('hash') + algo = request.form.get("hash") if not ((text or file) and algo): - return render_template_string("""\ + return render_template_string( + """\ Example:
{{ example_uri }}&hash=md5
{{ example_uri }}&hash=sha1
{{ example_uri }}&hash=sha512 - """, example_uri='/do_hash?text=Hello World!') + """, + example_uri="/do_hash?text=Hello World!", + ) result = { - 'result': None, - 'hash': algo, - 'error': None, + "result": None, + "hash": algo, + "error": None, } if algo not in SUPPORTED_HASH_ALGOS: - result['error'] = f'Unsupported algorithm "{algo}"' + result["error"] = f"Unsupported algorithm {algo!r}" else: if file: digest = hashlib.new(algo) - for chunk in iter(lambda: file.stream.read(128 * digest.block_size), b''): + for chunk in iter(lambda: file.stream.read(128 * digest.block_size), b""): digest.update(chunk) else: @@ -177,7 +186,7 @@ def do_hash(): hex_digest = digest.hexdigest() - result['result'] = hex_digest + result["result"] = hex_digest # Вернется json c результатом хеширования return jsonify(result) @@ -187,14 +196,10 @@ def do_hash(): app.debug = True # Localhost - app.run( - port=5001, - threaded=True, - ) + app.run(port=5001) # # Public IP # app.run( # host='0.0.0.0', - # port=5000, - # threaded=True, + # port=5000 # ) diff --git a/flask__webservers/hello_world.py b/flask__webservers/hello_world.py index 922e772a1..90f9c84e3 100644 --- a/flask__webservers/hello_world.py +++ b/flask__webservers/hello_world.py @@ -1,36 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask + + app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return "Hello World!" -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/hello_world_with_compress.py b/flask__webservers/hello_world_with_compress.py new file mode 100644 index 000000000..4c7185f22 --- /dev/null +++ b/flask__webservers/hello_world_with_compress.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging + +from flask import Flask + +# pip install flask-compress +from flask_compress import Compress + + +app = Flask(__name__) +Compress(app) + +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/") +def index(): + return "Hello World!" * 100 + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=5001) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/json_as_response.py b/flask__webservers/json_as_response.py index a521639e3..a744bf9e3 100644 --- a/flask__webservers/json_as_response.py +++ b/flask__webservers/json_as_response.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging - from flask import Flask, jsonify -app = Flask(__name__) + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) DATA = { - 'text': "Hello World!", - 'ok': True, - 'items': [ - 'Hello', - 'World!', - ] + "text": "Hello World!", + "ok": True, + "items": [ + "Hello", + "World!", + ], } @@ -32,5 +32,5 @@ def index_v2(): return DATA -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/logging__examples/log-config.yaml b/flask__webservers/logging__examples/log-config.yaml new file mode 100644 index 000000000..b427173a0 --- /dev/null +++ b/flask__webservers/logging__examples/log-config.yaml @@ -0,0 +1,27 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + web-server-file: + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main_new.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + +loggers: + web-server: &web-server + handlers: ["console", "web-server-file"] + level: "DEBUG" + + werkzeug: *web-server diff --git a/flask__webservers/logging__examples/main_new.py b/flask__webservers/logging__examples/main_new.py new file mode 100644 index 000000000..8000e0e63 --- /dev/null +++ b/flask__webservers/logging__examples/main_new.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + +from flask import Flask + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + +logging.config.dictConfig(LOGGING) + +log = logging.getLogger("web-server") + + +app = Flask(__name__) +app.logger = log + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__examples/main_old.py b/flask__webservers/logging__examples/main_old.py new file mode 100644 index 000000000..e5bdab429 --- /dev/null +++ b/flask__webservers/logging__examples/main_old.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from flask import Flask + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +app = Flask(__name__) + +formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" +) + +file_handler = RotatingFileHandler( + DIR_LOGS / "main_old.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8" +) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler(stream=sys.stdout) +stream_handler.setFormatter(formatter) + +log: logging.Logger = app.logger +log.handlers.clear() +log.setLevel(logging.DEBUG) +log.addHandler(file_handler) +log.addHandler(stream_handler) + +log_werkzeug = logging.getLogger("werkzeug") +log_werkzeug.setLevel(logging.DEBUG) +log_werkzeug.addHandler(file_handler) +log_werkzeug.addHandler(stream_handler) + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml b/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml new file mode 100644 index 000000000..90c089ccd --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml @@ -0,0 +1,30 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + web-server-file: + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main_new.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + +filters: + filter_remove_date_from_werkzeug_logs: + (): utils.FilterRemoveDateFromWerkzeugLogs + +loggers: + werkzeug: + handlers: ["console", "web-server-file"] + level: "DEBUG" + filters: ["filter_remove_date_from_werkzeug_logs"] diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py new file mode 100644 index 000000000..6c2e83aec --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + +from flask import Flask + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + +logging.config.dictConfig(LOGGING) + +log = logging.getLogger("werkzeug") + + +app = Flask(__name__) +app.logger = log + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py new file mode 100644 index 000000000..7b7c003bb --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from flask import Flask + +from utils import FilterRemoveDateFromWerkzeugLogs + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +app = Flask(__name__) + +formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" +) + +file_handler = RotatingFileHandler( + DIR_LOGS / "main_old.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8" +) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler(stream=sys.stdout) +stream_handler.setFormatter(formatter) + +log: logging.Logger = app.logger +log.handlers.clear() +log.setLevel(logging.DEBUG) +log.addHandler(file_handler) +log.addHandler(stream_handler) + +log_werkzeug = logging.getLogger("werkzeug") +log_werkzeug.setLevel(logging.DEBUG) +log_werkzeug.addHandler(file_handler) +log_werkzeug.addHandler(stream_handler) +log_werkzeug.addFilter(FilterRemoveDateFromWerkzeugLogs()) + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py new file mode 100644 index 000000000..531a2deba --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import re + + +# NOTE: Fix https://github.com/pallets/werkzeug/blob/72b2e48e7d44927b1b7d6b2f940d0691230de893/src/werkzeug/serving.py#L425C38-L425C44 +class FilterRemoveDateFromWerkzeugLogs(logging.Filter): + # '192.168.0.102 - - [30/Jun/2024 01:14:03] "%s" %s %s' -> '192.168.0.102 - "%s" %s %s' + pattern: re.Pattern = re.compile(r' - - \[.+?] "') + + def filter(self, record: logging.LogRecord) -> bool: + record.msg = self.pattern.sub(' - "', record.msg) + return True diff --git a/flask__webservers/post_data.py b/flask__webservers/post_data.py index d2ece5c21..6d22577a5 100644 --- a/flask__webservers/post_data.py +++ b/flask__webservers/post_data.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -38,13 +39,13 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) return data -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/post_data__as_form.py b/flask__webservers/post_data__as_form.py index 2791a8c43..ab732dd71 100644 --- a/flask__webservers/post_data__as_form.py +++ b/flask__webservers/post_data__as_form.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, request, jsonify -app = Flask(__name__) - import logging +from flask import Flask, request + + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """

@@ -46,16 +47,16 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) form = request.form - print('form:', form) + print("form:", form) - return form['name'] + " " + form['surname'] + return form["name"] + " " + form["surname"] -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/post_data__as_json.py b/flask__webservers/post_data__as_json.py index 9666b60aa..3c44aa550 100644 --- a/flask__webservers/post_data__as_json.py +++ b/flask__webservers/post_data__as_json.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -42,16 +43,16 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) json = request.json - print('json:', json) + print("json:", json) return data -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/print_hex_post_data/main.py b/flask__webservers/print_hex_post_data/main.py index 1e9e68e99..e8b699b97 100644 --- a/flask__webservers/print_hex_post_data/main.py +++ b/flask__webservers/print_hex_post_data/main.py @@ -1,30 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import binascii +import logging + from flask import Flask, request + + app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) -@app.route("/", methods=['POST']) -def index(): +@app.route("/", methods=["POST"]) +def index() -> str: data = request.data - - import binascii print(binascii.hexlify(data), data) return "Ok" -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True - app.run( - port=33333, - threaded=True, - ) + app.run(port=33333) diff --git a/flask__webservers/redirect_to_/main.py b/flask__webservers/redirect_to_/main.py index b315d5013..1e5e8dec1 100644 --- a/flask__webservers/redirect_to_/main.py +++ b/flask__webservers/redirect_to_/main.py @@ -1,40 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, redirect, request, render_template_string -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\
{{ example }}

- ''', example='/redirect?url=http://bash.im/') + """, + example="/redirect?url=http://bash.im/", + ) -@app.route('/redirect') +@app.route("/redirect") def redirect_to(): # From url arguments - url = request.args.get('url', None) + url = request.args.get("url", None) if not url: # From form arguments - url = request.form.get('url', '/') + url = request.form.get("url", "/") return redirect(url) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/redirect_to_google/main.py b/flask__webservers/redirect_to_google/main.py index 7f7751ac2..93b33e734 100644 --- a/flask__webservers/redirect_to_google/main.py +++ b/flask__webservers/redirect_to_google/main.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return redirect('https://google.ru') + return redirect("https://google.ru") -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123 pause.bat b/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123 pause.bat new file mode 100644 index 000000000..582e8deae --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123 pause.bat @@ -0,0 +1,2 @@ +cmd /C echo 123 +pause \ No newline at end of file diff --git a/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123.bat b/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123.bat new file mode 100644 index 000000000..661c36246 --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/bat_scripts/echo 123.bat @@ -0,0 +1 @@ +cmd /C echo 123 \ No newline at end of file diff --git a/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig pause.bat b/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig pause.bat new file mode 100644 index 000000000..05fb282c9 --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig pause.bat @@ -0,0 +1,2 @@ +cmd /C ipconfig +pause \ No newline at end of file diff --git a/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig.bat b/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig.bat new file mode 100644 index 000000000..703b408c8 --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/bat_scripts/ipconfig.bat @@ -0,0 +1 @@ +cmd /C ipconfig \ No newline at end of file diff --git a/flask__webservers/run_bat_from_ajax/main.py b/flask__webservers/run_bat_from_ajax/main.py new file mode 100644 index 000000000..9055ec144 --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import os +import subprocess + +from pathlib import Path + +from flask import Flask, jsonify, render_template + + +DIR = Path(__file__).resolve().parent +DIR_BAT_SCRIPTS = DIR / "bat_scripts" + + +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/") +def index(): + return render_template( + "index.html", + scripts=[f.name for f in DIR_BAT_SCRIPTS.glob("*.bat")], + ) + + +@app.route("/os_startfile/", methods=["POST"]) +def on_os_startfile(script_name: str): + os.startfile(DIR_BAT_SCRIPTS / script_name) + return jsonify({"ok": True}) + + +@app.route("/subprocess/", methods=["POST"]) +def on_subprocess(script_name: str): + result = subprocess.check_output( + DIR_BAT_SCRIPTS / script_name, universal_newlines=True, timeout=1 + ) + print(result) + os.startfile(DIR_BAT_SCRIPTS / script_name) + return jsonify({"ok": True, "result": result}) + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 - random free port + # app.run(port=0) + app.run(port=5000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/run_bat_from_ajax/static/js/index.js b/flask__webservers/run_bat_from_ajax/static/js/index.js new file mode 100644 index 000000000..232b65ce1 --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/static/js/index.js @@ -0,0 +1,30 @@ +$(document).ready(function() { + $('button.os_startfile').click(function() { + let script_name = $(this).text(); + console.log(script_name); + + $.ajax({ + url: '/os_startfile/' + script_name, + type: 'POST', + dataType: 'json', + success: function (data) { + console.log(data); + } + }); + }); + + $('button.subprocess').click(function() { + let script_name = $(this).text(); + console.log(script_name); + + $.ajax({ + url: '/subprocess/' + script_name, + type: 'POST', + dataType: 'json', + success: function (data) { + console.log(data); + $('#subprocess_result').text(data.result); + } + }); + }); +}); diff --git a/flask__webservers/run_bat_from_ajax/static/js/jquery-3.1.1.min.js b/flask__webservers/run_bat_from_ajax/static/js/jquery-3.1.1.min.js new file mode 100644 index 000000000..4c5be4c0f --- /dev/null +++ b/flask__webservers/run_bat_from_ajax/static/js/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + +
+
+

os_startfile:

+ {% for script_name in scripts %} + + {% endfor %} +
+
+
+

subprocess (return output):

+ {% for script_name in scripts %} + + {% endfor %} +
+ +
+ + + \ No newline at end of file diff --git a/flask__webservers/run_with_random_port/main.py b/flask__webservers/run_with_random_port/main.py index 5e3244a97..f9a8d2d53 100644 --- a/flask__webservers/run_with_random_port/main.py +++ b/flask__webservers/run_with_random_port/main.py @@ -5,22 +5,23 @@ # SOURCE: https://stackoverflow.com/a/5089963/5909792 -from flask import Flask, request import socket +from flask import Flask, request + app = Flask(__name__) -@app.route('/') -def hello(): - return 'Hello, world! running on %s' % request.host +@app.route("/") +def hello() -> str: + return "Hello, world! running on %s" % request.host -if __name__ == '__main__': +if __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(('localhost', 0)) + sock.bind(("localhost", 0)) port = sock.getsockname()[1] - print('sock.getsockname:', sock.getsockname()) + print("sock.getsockname:", sock.getsockname()) sock.close() app.run(port=port) diff --git a/flask__webservers/server__handle_stdin__input.py b/flask__webservers/server__handle_stdin__input.py index 1bc087748..288ff43e8 100644 --- a/flask__webservers/server__handle_stdin__input.py +++ b/flask__webservers/server__handle_stdin__input.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from threading import Thread +import logging import time +from threading import Thread + from flask import Flask + + app = Flask(__name__) -import logging logging.basicConfig(level=logging.DEBUG) text = "Hello World!" -def go(): +def go() -> None: time.sleep(2) - print('\n') + print("\n") global text @@ -32,7 +35,7 @@ def index(): return text -if __name__ == '__main__': +if __name__ == "__main__": thread = Thread(target=go) thread.start() @@ -41,15 +44,7 @@ def index(): # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000, - - # :param threaded: should the process handle each request in a separate - # thread? - # :param processes: if greater than 1 then handle each request in a new process - # up to this maximum number of concurrent processes. - threaded=True, - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/server_with_additional_command_thread/main.py b/flask__webservers/server_with_additional_command_thread/main.py index 12d4ccac4..fcb917e94 100644 --- a/flask__webservers/server_with_additional_command_thread/main.py +++ b/flask__webservers/server_with_additional_command_thread/main.py @@ -1,13 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import time + +from urllib.parse import urljoin + +import requests + +from bs4 import BeautifulSoup from flask import Flask, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @@ -17,55 +25,50 @@ @app.route("/") def index(): - return redirect('/img_search?text=Котята') + return redirect("/img_search?text=Котята") @app.route("/img_search") def img_search(): - text = request.args.get('text') - print('text:', text) + text = request.args.get("text") + print("text:", text) global EXECUTE_COMMAND, COMMAND_TEXT COMMAND_TEXT = text EXECUTE_COMMAND = False - return 'text: ' + text - + return "text: " + text -def loop_command_function(): - import time +def loop_command_function() -> None: while True: global EXECUTE_COMMAND, COMMAND_TEXT if not EXECUTE_COMMAND and COMMAND_TEXT: print(COMMAND_TEXT) - url = 'http://yandex.ru/images/search?text=' + COMMAND_TEXT + url = "http://yandex.ru/images/search?text=" + COMMAND_TEXT - import requests rs = requests.get(url) print(rs) - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - from urllib.parse import urljoin + root = BeautifulSoup(rs.content, "lxml") img_list = [] - for img in root.select('img.serp-item__thumb'): - url_img = urljoin(url, img['src']) + for img in root.select("img.serp-item__thumb"): + url_img = urljoin(url, img["src"]) img_list.append(url_img) - print('img_list[{}]: {}'.format(len(img_list), img_list)) + print(f"img_list[{len(img_list)}]: {img_list}") EXECUTE_COMMAND = True time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from threading import Thread + thread = Thread(target=loop_command_function) thread.start() diff --git a/flask__webservers/server_with_window_notification/main.py b/flask__webservers/server_with_window_notification/main.py index 238dc74a3..c7a16d49f 100644 --- a/flask__webservers/server_with_window_notification/main.py +++ b/flask__webservers/server_with_window_notification/main.py @@ -1,44 +1,51 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import threading +import sys + +from pathlib import Path + from flask import Flask, request, redirect -app = Flask(__name__) -import logging -logging.basicConfig(level=logging.DEBUG) +ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.append( + str(ROOT / "winapi__windows__ctypes/windows__toast_balloontip_notifications") +) +from run_notify import run_in_thread -import threading + +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) -def show(text): +def show(text) -> None: title = str(threading.current_thread()) - - # Copy from: SimplePyScripts\windows__toast_balloontip_notifications\main.py - from notifications import WindowsBalloonTip - WindowsBalloonTip.balloon_tip(title, text, duration=20) + run_in_thread(title, text, duration=20) @app.route("/") def index(): - return redirect('/show_notification?text=О, уведомление пришло!') + return redirect("/show_notification?text=О, уведомление пришло!") @app.route("/show_notification") -def show_notification(): - text = request.args.get('text') - print('text:', text) +def show_notification() -> str: + text = request.args.get("text") + print("text:", text) # Run function in new thread thread = threading.Thread(target=show, args=(text,)) thread.start() - return 'Ok' + return "Ok" -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run( port=5000, diff --git a/flask__webservers/server_with_window_notification/notifications.py b/flask__webservers/server_with_window_notification/notifications.py deleted file mode 100644 index 983993f5c..000000000 --- a/flask__webservers/server_with_window_notification/notifications.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://gist.github.com/wontoncc/1808234 -# -# Analog: https://github.com/K-DawG007/Stack-Watch/blob/master/windows_popup.py -# Analog: https://github.com/jithurjacob/Windows-10-Toast-Notifications - -from win32gui import * -import win32con -import sys -import os -import time -import uuid - - -class WindowsBalloonTip: - @staticmethod - def balloon_tip(title, msg, duration=5, icon_path_name=None): - message_map = { - win32con.WM_DESTROY: WindowsBalloonTip.on_destroy, - } - - # Register the Window class. - wc = WNDCLASS() - hinst = wc.hInstance = GetModuleHandle(None) - - # Random class name - wc.lpszClassName = str(uuid.uuid1()) - wc.lpfnWndProc = message_map # could also specify a wndproc. - - class_atom = RegisterClass(wc) - - # Create the Window. - style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU - hwnd = CreateWindow(class_atom, "Taskbar", style, 0, 0, - win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, - 0, 0, hinst, None) - UpdateWindow(hwnd) - - if not icon_path_name: - icon_path_name = os.path.abspath(os.path.join(sys.path[0], "balloontip.ico")) - - icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE - try: - hicon = LoadImage(hinst, icon_path_name, win32con.IMAGE_ICON, 0, 0, icon_flags) - except: - hicon = LoadIcon(0, win32con.IDI_APPLICATION) - - flags = NIF_ICON | NIF_MESSAGE | NIF_TIP - nid = (hwnd, 0, flags, win32con.WM_USER + 20, hicon, "tooltip") - Shell_NotifyIcon(NIM_ADD, nid) - Shell_NotifyIcon(NIM_MODIFY, - (hwnd, 0, NIF_INFO, win32con.WM_USER + 20, hicon, "Balloon tooltip", msg, 200, title)) - - time.sleep(duration) - - DestroyWindow(hwnd) - UnregisterClass(wc.lpszClassName, None) - - @staticmethod - def on_destroy(hwnd, msg, wparam, lparam): - nid = (hwnd, 0) - Shell_NotifyIcon(NIM_DELETE, nid) - PostQuitMessage(0) # Terminate the app. - - -if __name__ == '__main__': - WindowsBalloonTip.balloon_tip('First', 'My Text!', duration=2) - WindowsBalloonTip.balloon_tip('Second', 'My NEW Text!', duration=3) - WindowsBalloonTip.balloon_tip('Three', 'With invalid icons!', icon_path_name='fdfs.ico') diff --git a/flask__webservers/set_response_headers.py b/flask__webservers/set_response_headers.py index 5038d328e..edc84839f 100644 --- a/flask__webservers/set_response_headers.py +++ b/flask__webservers/set_response_headers.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, Response -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): rs = Response("Hello World!") - rs.headers['User-Agent'] = "FooBar" + rs.headers["User-Agent"] = "FooBar" return rs -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/show_and_download_from_readmanga/main.py b/flask__webservers/show_and_download_from_readmanga/main.py index 8d8d0eb6d..7b8a2a63c 100644 --- a/flask__webservers/show_and_download_from_readmanga/main.py +++ b/flask__webservers/show_and_download_from_readmanga/main.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import os +import sys + from flask import Flask, render_template_string, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Добавление пути основной папки репозитория, чтобы импортировать модуль download_volume_readmanga -import os dir = os.path.dirname(__file__) dir = os.path.dirname(dir) dir = os.path.dirname(dir) - -import sys sys.path.append(dir) - from download_volume_readmanga import get_url_images, save_urls_to_zip @@ -27,7 +27,7 @@

Например: {0}

""" -DEFAULT_URL_MANGA = 'https://readmanga.live/one_punch_man__A1bc88e/vol1/1' +DEFAULT_URL_MANGA = "https://readmanga.live/one_punch_man__A1bc88e/vol1/1" # TODO: FIXED HTTP STATUS "402 Payment Required" on @@ -35,15 +35,16 @@ @app.route("/") def index(): if not request.args: - return NOT_ARGS_HTML.format(f'/?url={DEFAULT_URL_MANGA}') + return NOT_ARGS_HTML.format(f"/?url={DEFAULT_URL_MANGA}") - url = request.args.get('url') - print('Url manga:', url) + url = request.args.get("url") + print("Url manga:", url) images_urls = get_url_images(url) - print('Urls images:', images_urls) + print("Urls images:", images_urls) - return render_template_string('''\ + return render_template_string( + """\ Показать и скачать из readmanga @@ -61,32 +62,37 @@ def index(): - ''', url=url, images_urls=images_urls) + """, + url=url, + images_urls=images_urls, + ) @app.route("/export") def export(): if not request.args: - return NOT_ARGS_HTML.format(f'/export?{DEFAULT_URL_MANGA}') + return NOT_ARGS_HTML.format(f"/export?{DEFAULT_URL_MANGA}") - url = request.args.get('url') - print('Url manga:', url) + url = request.args.get("url") + print("Url manga:", url) - file_name = os.path.basename(url) + '.zip' - static_file_name = os.path.join('static', file_name) + file_name = os.path.basename(url) + ".zip" + static_file_name = os.path.join("static", file_name) # Если архива с главой нет, качаем ее if not os.path.exists(static_file_name): - print('Архива "{}" нет, качаем и создаем.'.format(static_file_name)) + print(f'Архива "{static_file_name}" нет, качаем и создаем.') images_urls = get_url_images(url) - print('Urls images:', images_urls) + print("Urls images:", images_urls) save_urls_to_zip(static_file_name, images_urls) - print('Сохранено в архиве:', file_name) + print("Сохранено в архиве:", file_name) # Перенаправляем к url с архивом - relative_url = render_template_string("{{ url_for('static', filename='%s') }}" % (file_name, )) + relative_url = render_template_string( + "{{ url_for('static', filename='%s') }}" % (file_name,) + ) return redirect(relative_url) diff --git a/flask__webservers/show_client_ip_API.py b/flask__webservers/show_client_ip_API.py new file mode 100644 index 000000000..422c6a93a --- /dev/null +++ b/flask__webservers/show_client_ip_API.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import xml.etree.ElementTree as ET + +from flask import Flask, request, Response + + +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/") +def index(): + return request.remote_addr + + +@app.route("/json") +def get_json(): + return { + "ip": request.remote_addr, + } + + +@app.route("/xml") +def get_xml(): + root = ET.Element("ip") + root.text = request.remote_addr + xml_bytes = ET.tostring(root, encoding="utf-8", xml_declaration=True) + return Response(xml_bytes, mimetype="text/xml") + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/show_job_report/main.py b/flask__webservers/show_job_report/main.py deleted file mode 100644 index 60f439e42..000000000 --- a/flask__webservers/show_job_report/main.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -"""Вывод переработки текущего (или конкретного) пользователя""" - - -from flask import Flask -app = Flask(__name__) - -import logging -logging.basicConfig(level=logging.DEBUG) - - -# Добавление пути основной папки репозитория, чтобы импортировать из job_report.utils -import os -dir = os.path.dirname(__file__) -dir = os.path.dirname(dir) -dir = os.path.dirname(dir) - -import sys -sys.path.append(dir) - - -from job_report.utils import get_report_persons_info, get_person_info - - -PEM_FILE_NAME = 'ipetrash.pem' - - -@app.route("/") -def index(): - report_dict = get_report_persons_info(PEM_FILE_NAME) - - try: - person = report_dict['Текущий пользователь'][0] - except: - person = None - - if person is None: - person = get_person_info(PEM_FILE_NAME, second_name='Петраш', report_dict=report_dict) - - if person: - return '{} {}'.format(person.full_name, person.deviation_of_time) - - return 'Not found specific user' - - -if __name__ == '__main__': - # Localhost - app.run(port=5001) - - # # Public IP - # app.run(host='0.0.0.0') diff --git a/flask__webservers/show_last_bash_quotes/main.py b/flask__webservers/show_last_bash_quotes/main.py index 3663c4fee..3c91e6018 100644 --- a/flask__webservers/show_last_bash_quotes/main.py +++ b/flask__webservers/show_last_bash_quotes/main.py @@ -1,37 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging import re +import traceback + +import requests + +from bs4 import BeautifulSoup +from flask import Flask, render_template_string def today_quotes(soup): # Узнаем количество сегодняшних цитат - tag = soup.find(attrs=dict(id='stats')) + tag = soup.find(attrs=dict(id="stats")) - match = re.search('сегодня (\d+),', tag.text) + match = re.search(r"сегодня (\d+),", tag.text) return int(match.group(1)) -from flask import Flask, render_template_string app = Flask(__name__) - -import logging logging.basicConfig(level=logging.DEBUG) -import requests -from bs4 import BeautifulSoup - -import traceback - - @app.route("/") def index(): try: - rs = requests.get('http://bash.im') + rs = requests.get("http://bash.im") soup = BeautifulSoup(rs.text, "lxml") number = today_quotes(soup) @@ -48,7 +46,8 @@ def index(): print(i, text) quotes.append(text) - return render_template_string('''\ + return render_template_string( + """\ Новые за день цитаты bash.im @@ -68,11 +67,14 @@ def index(): {% endif %} - ''', number=number, item_list=quotes) + """, + number=number, + item_list=quotes, + ) except BaseException as e: - print('Error: {}\n\n{}'.format(e, traceback.format_exc())) - return 'Error: ' + str(e) + print(f"Error: {e}\n\n{traceback.format_exc()}") + return "Error: " + str(e) if __name__ == "__main__": diff --git a/flask__webservers/show_lunch_menu/main.py b/flask__webservers/show_lunch_menu/main.py index 8f5ce497a..cb2de5d3c 100644 --- a/flask__webservers/show_lunch_menu/main.py +++ b/flask__webservers/show_lunch_menu/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Чтение из docx файла таблицы меню и отображении таблицы на веб странице. @@ -10,17 +10,18 @@ """ +import logging +import re + +from docx import Document from flask import Flask, render_template_string -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') - -from docx import Document +multi_space_pattern = re.compile(r"[ ]{2,}") def get_rows_lunch_menu(): @@ -31,16 +32,19 @@ def get_rows_lunch_menu(): for table in document.tables: # Перебор начинается со второй строки, потому что, первая строка таблицы -- это строка "Обеденное меню" for row in table.rows[1:]: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) + for i in row.cells + ] if name == weight == price or (not weight or not price): name = name.title() logging.debug(name) - rows.append((name, )) + rows.append((name,)) continue rows.append((name, weight, price)) - logging.debug('{} {} {}'.format(name, weight, price)) + logging.debug(f"{name} {weight} {price}") # Таблицы в меню дублируются break @@ -52,7 +56,8 @@ def get_rows_lunch_menu(): def index(): rows = get_rows_lunch_menu() - return render_template_string('''\ + return render_template_string( + """\ Обеденное меню @@ -82,10 +87,12 @@ def index(): - ''', rows=rows) + """, + rows=rows, + ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/show_lunch_menu_from_email/config.py b/flask__webservers/show_lunch_menu_from_email/config.py index 747621f50..fb26fa83a 100644 --- a/flask__webservers/show_lunch_menu_from_email/config.py +++ b/flask__webservers/show_lunch_menu_from_email/config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Например: @mail.com diff --git a/flask__webservers/show_lunch_menu_from_email/main.py b/flask__webservers/show_lunch_menu_from_email/main.py index 4e8e8057b..6ebcd5323 100644 --- a/flask__webservers/show_lunch_menu_from_email/main.py +++ b/flask__webservers/show_lunch_menu_from_email/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,21 +14,30 @@ """ +import email +import imaplib import os -import traceback +import re import sys +import traceback +from datetime import datetime, date + +# pip install python-docx +from docx import Document +from flask import Flask, render_template_string, request import config + + if config.debug: import logging logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) def get_current_lunch_file_name(): - from datetime import date - filename = date.today().strftime('%d.%m.%y') + '.docx' - return os.path.join('lunch menu', filename) + filename = date.today().strftime("%d.%m.%y") + ".docx" + return os.path.join("lunch menu", filename) def save_attachment(msg): @@ -43,22 +52,22 @@ def save_attachment(msg): os.makedirs(os.path.dirname(file_name), exist_ok=True) for part in msg.walk(): - if part.get_content_maintype() == 'multipart': + if part.get_content_maintype() == "multipart": continue - if part.get('Content-Disposition') is None: + if part.get("Content-Disposition") is None: continue if not os.path.exists(file_name): - with open(file_name, 'wb') as fp: + with open(file_name, "wb") as fp: fp.write(part.get_payload(decode=True)) else: - logging.debug('Lunch menu file exist: %s', file_name) + logging.debug("Lunch menu file exist: %s", file_name) return file_name -def add_lunch_email_info(msg, file_name): +def add_lunch_email_info(msg, file_name) -> None: """ Функция для добавления в поле comments docx информации о письме. @@ -68,24 +77,20 @@ def add_lunch_email_info(msg, file_name): """ try: - import email - # Получаем заголовок письма - data, charset = email.header.decode_header(msg['Subject'])[0] - subject = data.decode(charset).strip().replace('\t', ' ') + data, charset = email.header.decode_header(msg["Subject"])[0] + subject = data.decode(charset).strip().replace("\t", " ") logging.debug('Subject: "%s".', subject) # Получаем дату получения (отправления??) письма - from datetime import datetime - date_tuple = email.utils.parsedate_tz(msg['Date']) + date_tuple = email.utils.parsedate_tz(msg["Date"]) email_date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple)) email_date = email_date.strftime(config.header_date_format) logging.debug("Date: %s.", email_date) # Открываем документ и переписываем поле comments, добавляя в него свою информацию - from docx import Document document = Document(file_name) - document.core_properties.comments = subject + '\t' + email_date + document.core_properties.comments = subject + "\t" + email_date document.save(file_name) except BaseException as e: @@ -103,26 +108,24 @@ def get_last_lunch_menu(): # Проверка на то, что меню уже скачано file_name = get_current_lunch_file_name() if os.path.exists(file_name): - logging.debug('Lunch menu file already exist: %s', file_name) + logging.debug("Lunch menu file already exist: %s", file_name) return file_name - logging.debug('Check last email.') + logging.debug("Check last email.") - import imaplib connect = imaplib.IMAP4(config.smtp_server) connect.login(config.username, config.password) connect.select() - logging.debug('Search emails from %s.', config.lunch_email) + logging.debug("Search emails from %s.", config.lunch_email) - typ, msgnums = connect.search(None, '(HEADER From {})'.format(config.lunch_email)) + typ, msgnums = connect.search(None, f"(HEADER From {config.lunch_email})") last_id = msgnums[0].split()[-1] - typ, data = connect.fetch(last_id, '(RFC822)') + typ, data = connect.fetch(last_id, "(RFC822)") - import email msg = email.message_from_bytes(data[0][1]) - logging.debug('Save lunch file name: %s.', file_name) + logging.debug("Save lunch file name: %s.", file_name) file_name = save_attachment(msg) add_lunch_email_info(msg, file_name) @@ -132,12 +135,10 @@ def get_last_lunch_menu(): return file_name -from flask import Flask, render_template_string, request app = Flask(__name__) # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') +multi_space_pattern = re.compile(r"[ ]{2,}") def get_info_lunch_menu(): @@ -147,14 +148,14 @@ def get_info_lunch_menu(): if file_name is None: return rows - logging.debug('Read lunch file name: %s.', file_name) - from docx import Document + logging.debug("Read lunch file name: %s.", file_name) + document = Document(file_name) - subject, date = '', '' + subject, date = "", "" comments = document.core_properties.comments if comments: - parts = comments.split('\t', 2) + parts = comments.split("\t", 2) if len(parts) == 2: subject, date = parts @@ -164,16 +165,18 @@ def get_info_lunch_menu(): for table in document.tables: # Перебор начинается со второй строки, потому что, первая строка таблицы -- это строка "Обеденное меню" for row in table.rows[1:]: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) for i in row.cells + ] if name == weight == price or (not weight or not price): name = name.title() logging.debug(name) - rows.append((name, )) + rows.append((name,)) continue rows.append((name, weight, price)) - logging.debug('{} {} {}'.format(name, weight, price)) + logging.debug(f"{name} {weight} {price}") # Таблицы в меню дублируются break @@ -183,16 +186,17 @@ def get_info_lunch_menu(): @app.route("/") def index(): - logging.debug('/index from %s.', request.remote_addr) + logging.debug("/index from %s.", request.remote_addr) try: rows, subject, date = get_info_lunch_menu() except BaseException as e: logging.error(e) logging.error(traceback.format_exc()) - return 'Error: "{}".'.format(e), 500 + return f'Error: "{e}".', 500 - return render_template_string('''\ + return render_template_string( + """\ {{ subject }} @@ -222,10 +226,14 @@ def index(): - ''', rows=rows, subject=subject, date=date) + """, + rows=rows, + subject=subject, + date=date, + ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/show_my_ip/main.py b/flask__webservers/show_my_ip/main.py index 991ea4a38..8ff891a59 100644 --- a/flask__webservers/show_my_ip/main.py +++ b/flask__webservers/show_my_ip/main.py @@ -1,24 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): - return """Your IPv4 Address Is: {}""".format(request.remote_addr) +def index() -> str: + return f"""Your IPv4 Address Is: {request.remote_addr}""" -if __name__ == '__main__': +if __name__ == "__main__": # # Localhost # app.run(port=5001) # Public IP - app.run(host='0.0.0.0') + app.run(host="0.0.0.0") diff --git a/flask__webservers/show_random_abusive/main.py b/flask__webservers/show_random_abusive/main.py index 9c2410772..5947db507 100644 --- a/flask__webservers/show_random_abusive/main.py +++ b/flask__webservers/show_random_abusive/main.py @@ -1,40 +1,41 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import os +import sys + from flask import Flask, render_template_string, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Добавление пути основной папки репозитория, чтобы импортировать модуль random_abusive -import os dir = os.path.dirname(__file__) dir = os.path.dirname(dir) dir = os.path.dirname(dir) - -import sys sys.path.append(dir) - import random_abusive @app.route("/") def index(): if not request.args: - return redirect('/?number=10&chain=2') + return redirect("/?number=10&chain=2") - print('query_string:', request.query_string) - number = int(request.args['number']) - chain = int(request.args['chain']) + print("query_string:", request.query_string) + number = int(request.args["number"]) + chain = int(request.args["chain"]) words = random_abusive.get_words(number, chain) - print('words:', words) + print("words:", words) - return render_template_string('''\ + return render_template_string( + """\ Рандомные матерные слова @@ -44,7 +45,9 @@ def index(): - ''', words=words) + """, + words=words, + ) if __name__ == "__main__": diff --git a/flask__webservers/show_screenshot/main.py b/flask__webservers/show_screenshot/main.py index 58b8f0918..29f52c27d 100644 --- a/flask__webservers/show_screenshot/main.py +++ b/flask__webservers/show_screenshot/main.py @@ -1,30 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, render_template_string -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) + +from flask import Flask, render_template_string import pyscreenshot as ImageGrab +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + @app.route("/") def index(): - im = ImageGrab.grab() - im.save('static/screenshot.png') + img = ImageGrab.grab() + img.save("static/screenshot.png") - return render_template_string('''\ + return render_template_string( + """\ Show screenshot

- ''') + """ + ) if __name__ == "__main__": diff --git a/flask__webservers/show_your_user_agent.py b/flask__webservers/show_your_user_agent.py index 96aa46a87..addb8a263 100644 --- a/flask__webservers/show_your_user_agent.py +++ b/flask__webservers/show_your_user_agent.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - user_agent = request.headers['User-Agent'] + user_agent = request.headers["User-Agent"] print(user_agent) return user_agent -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/simple_echo.py b/flask__webservers/simple_echo.py index e0a234438..5bd7bf429 100644 --- a/flask__webservers/simple_echo.py +++ b/flask__webservers/simple_echo.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from flask import Flask, request, Response + + app = Flask(__name__) -@app.route('/', methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def index(): - print('-' * 20) - print('Method:', request.method) - print('Args:', dict(request.args)) - print('Data:', request.data) - print('Form:', request.form) - print('Headers:', dict(request.headers)) - print('-' * 20) + print("-" * 20) + print("Method:", request.method) + print("Args:", dict(request.args)) + print("Data:", request.data) + print("Form:", request.form) + print("Headers:", dict(request.headers)) + print("-" * 20) return Response( response=request.data, - content_type=request.headers.get('Content-Type') + content_type=request.headers.get("Content-Type"), ) diff --git a/flask__webservers/simple_upload_server/main.py b/flask__webservers/simple_upload_server/main.py index 82d65ec3c..2f5faf9aa 100644 --- a/flask__webservers/simple_upload_server/main.py +++ b/flask__webservers/simple_upload_server/main.py @@ -1,24 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from flask import Flask, request + + app = Flask(__name__) -@app.route("/", methods=['POST']) -def index(): - file = request.files['file'] +@app.route("/", methods=["POST"]) +def index() -> str: + file = request.files["file"] # file.save(file.filename) # OR: - file.save('upload_file.txt') + file.save("upload_file.txt") - return 'ok' + return "ok" -if __name__ == '__main__': +if __name__ == "__main__": app.run( port=6000, ) diff --git a/flask__webservers/simple_upload_server/test.py b/flask__webservers/simple_upload_server/test.py index 0bacceb12..f4584925a 100644 --- a/flask__webservers/simple_upload_server/test.py +++ b/flask__webservers/simple_upload_server/test.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Отправка файла на сервер import requests -rs = requests.post('http://localhost:6000', files={'file': open('disk_info.txt', 'rb')}) + + +rs = requests.post("http://localhost:6000", files={"file": open("disk_info.txt", "rb")}) print(rs) diff --git a/flask__webservers/upload_and_download_files/main.py b/flask__webservers/upload_and_download_files/main.py index e40007623..f92fabaf2 100644 --- a/flask__webservers/upload_and_download_files/main.py +++ b/flask__webservers/upload_and_download_files/main.py @@ -1,48 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os + from flask import Flask, request, redirect, url_for, flash, send_from_directory from werkzeug.utils import secure_filename -UPLOAD_FOLDER = 'uploads' + +UPLOAD_FOLDER = "uploads" UPLOAD_FOLDER = os.path.abspath(UPLOAD_FOLDER) app = Flask(__name__) -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True) -@app.route('/', methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def upload_file(): - if request.method == 'POST': + if request.method == "POST": # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') + if "file" not in request.files: + flash("No file part") return redirect(request.url) - file = request.files['file'] + file = request.files["file"] # if user does not select file, browser also # submit a empty part without filename - if file.filename == '': - flash('No selected file') + if file.filename == "": + flash("No selected file") return redirect(request.url) if file: # Функция secure_filename не дружит с не ascii-символами, поэтому # файлы русскими словами не называть filename = secure_filename(file.filename) - file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) # Функция url_for('uploaded_file', filename=filename) возвращает строку вида: /uploads/ - return redirect(url_for('uploaded_file', filename=filename)) + return redirect(url_for("uploaded_file", filename=filename)) - return ''' + return """ Upload new File

Upload new File

@@ -50,18 +52,17 @@ def upload_file():

- ''' + """ # Пример обработчика, возвращающий файлы из папки app.config['UPLOAD_FOLDER'] для путей uploads и files. # т.е. не нужно давать специальное название, чтобы получить файл в flask # @app.route('/uploads/') -@app.route('/files/') +@app.route("/files/") def uploaded_file(filename): - return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + return send_from_directory(app.config["UPLOAD_FOLDER"], filename) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost - # Включение поддержки множества подключений - app.run(threaded=True) + app.run() diff --git a/flask__webservers/upload_and_download_files/test_download_from_script.py b/flask__webservers/upload_and_download_files/test_download_from_script.py index 995b39f95..c52b6b141 100644 --- a/flask__webservers/upload_and_download_files/test_download_from_script.py +++ b/flask__webservers/upload_and_download_files/test_download_from_script.py @@ -1,39 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) +import os - num /= 1024.0 +# pip install humanize +from humanize import naturalsize as sizeof_fmt - return "%3.1f %s" % (num, 'TB') - -def progress(count, block_size, total_size): +def progress(count, block_size, total_size) -> None: percent = count * block_size * 100.0 / total_size - print("Download: %s/%s(%3.1f%%)" % (sizeof_fmt(count * block_size), sizeof_fmt(total_size), percent) + ' ' * 20, - end='\r') - + print( + f"Download: {sizeof_fmt(count * block_size)}/{sizeof_fmt(total_size)}({percent:.1f}%)" + + " " * 20, + end="\r", + ) -def create_test_file(): - file_name = 'uploads/bigfile' - import os +def create_test_file() -> None: + file_name = "uploads/bigfile" if os.path.exists(file_name): return # Создание больших файлов для теста - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: for i in range(1024 * 1024 * 600): - f.write(b'0') + f.write(b"0") + -if __name__ == '__main__': +if __name__ == "__main__": create_test_file() from urllib.request import urlretrieve - urlretrieve('http://127.0.0.1:5000/files/bigfile', "file", reporthook=progress) + urlretrieve("http://127.0.0.1:5000/files/bigfile", "file", reporthook=progress) diff --git a/flask__webservers/url_shortener/config.py b/flask__webservers/url_shortener/config.py new file mode 100644 index 000000000..f23e91138 --- /dev/null +++ b/flask__webservers/url_shortener/config.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +# Текущая папка, где находится скрипт +DIR = Path(__file__).resolve().parent + +# Создание папки для базы данных +DB_DIR_NAME = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +# Путь к файлу базы данных +DB_FILE_NAME = str(DB_DIR_NAME / "database.sqlite") + +# Максимальная длина идентификаторов ссылок +LENGTH_URL_ID: int = 5 diff --git a/flask__webservers/url_shortener/db.py b/flask__webservers/url_shortener/db.py new file mode 100644 index 000000000..2b8c3dc08 --- /dev/null +++ b/flask__webservers/url_shortener/db.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +from typing import Type, Optional, Iterable, TypeVar +from uuid import uuid4 + +# pip install peewee +from peewee import Model, TextField, ForeignKeyField, CharField +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DB_FILE_NAME, LENGTH_URL_ID +from shorten import shorten + + +def generate_link_id(length: int = LENGTH_URL_ID) -> str: + # TODO: Возможна ситуация, когда length будет больше размера UUID + return uuid4().hex[:length] + + +class NotDefinedParameterException(Exception): + def __init__(self, parameter_name: str) -> None: + self.parameter_name = parameter_name + text = f'Parameter "{self.parameter_name}" must be defined!' + + super().__init__(text) + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +ChildModel = TypeVar("ChildModel", bound="BaseModel") + + +class BaseModel(Model): + """ + Базовая модель классов-таблиц + """ + + class Meta: + database = db + + def get_new(self) -> ChildModel: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> ChildModel: + return cls.select().first() + + @classmethod + def get_last(cls) -> ChildModel: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Link(BaseModel): + link_id = TextField(primary_key=True) + link_url = TextField(unique=True) + + @classmethod + def get_by_link_url(cls, link_url: str) -> Optional["Link"]: + if not link_url or not link_url.strip(): + raise NotDefinedParameterException(parameter_name="link_url") + + return cls.get_or_none(cls.link_url == link_url) + + @classmethod + def get_by_link_id(cls, link_id: str) -> Optional["Link"]: + if not link_id or not link_id.strip(): + raise NotDefinedParameterException(parameter_name="link_id") + + return cls.get_or_none(cls.link_id == link_id) + + @classmethod + def add(cls, link_url: str) -> "Link": + obj = cls.get_by_link_url(link_url) + if not obj: + # Убеждаемся, что link_id будет уникальным + while True: # TODO: Возможна ситуация, когда свободные закончатся + link_id = generate_link_id() + if not cls.get_by_link_id(link_id): + break + + obj = cls.create( + link_id=link_id, + link_url=link_url, + ) + + return obj + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + # Link: 1 + print() + + link = Link.add(link_url="https://example.com") + assert Link.select().count() + assert link == link.get_by_link_url(link_url=link.link_url) + assert link == link.get_by_link_id(link_id=link.link_id) diff --git a/flask__webservers/url_shortener/main.py b/flask__webservers/url_shortener/main.py new file mode 100644 index 000000000..a048af41f --- /dev/null +++ b/flask__webservers/url_shortener/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from urllib.parse import urlparse, urljoin + +from flask import Flask, request, render_template, redirect, abort, jsonify + +import db + + +app = Flask(__name__) + + +def server_url() -> str: + parse = urlparse(request.url) + return f"{parse.scheme}://{parse.netloc}/" + + +@app.route("/", defaults={"link_id": ""}) +@app.route("/") +def index(link_id: str): + if link_id: + link = db.Link.get_by_link_id(link_id) + if not link: + abort(404) + + url = link.link_url + return redirect(url) + + return render_template( + "index.html", + title=Path(__file__).parent.resolve().stem, + ) + + +@app.route("/add", methods=["POST"]) +def add(): + result = { + "url": "", + } + if "url" in request.form: + link_url = request.form["url"] + link = db.Link.add(link_url) + result["url"] = urljoin(server_url(), link.link_id) + + return jsonify(result) + + +if __name__ == "__main__": + # # Localhost + # app.run(port=5001) + + # Public IP + app.run(host="0.0.0.0") diff --git a/webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.bundle.js b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js similarity index 100% rename from webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.bundle.js rename to flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js diff --git a/webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.bundle.js.map b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js.map similarity index 100% rename from webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.bundle.js.map rename to flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js.map diff --git a/webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.css b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.css similarity index 100% rename from webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.css rename to flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.css diff --git a/webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.css.map b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.css.map similarity index 100% rename from webserver__handler_key_mouse_click_and_move/static/bootstrap-4.4.1/bootstrap.css.map rename to flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.css.map diff --git a/flask__webservers/url_shortener/static/bootstrap-dark.min_3.0.0.css b/flask__webservers/url_shortener/static/bootstrap-dark.min_3.0.0.css new file mode 100644 index 000000000..59af74274 --- /dev/null +++ b/flask__webservers/url_shortener/static/bootstrap-dark.min_3.0.0.css @@ -0,0 +1 @@ +:root{--blue:#3395ff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffce3a;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#3395ff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#fd7e14;--danger:#dc3545;--light:#dee2e6;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#d3d3d3;text-align:left;background-color:#191d21}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#adadad;text-decoration:none;background-color:transparent}a:hover{color:#878787;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(255,255,255,.1)}.small,small{font-size:.875em;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#d3d3d3}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #343a40}.table thead th{vertical-align:bottom;border-bottom:2px solid #343a40}.table tbody+tbody{border-top:2px solid #343a40}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #343a40}.table-bordered td,.table-bordered th{border:1px solid #343a40}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#d3d3d3;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#c6e1ff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#95c8ff}.table-hover .table-primary:hover{background-color:#add4ff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#add4ff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#fedbbd}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#febc85}.table-hover .table-warning:hover{background-color:#fecda4}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fecda4}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#f6f7f8}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#eef0f2}.table-hover .table-light:hover{background-color:#e8eaed}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#e8eaed}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#dee2e6;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#343a40}.table-dark{color:#dee2e6;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.table-primary,.table-primary>td,.table-primary>th{color:#343a40}.table-hover .table-primary:hover{color:#343a40}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{color:#343a40}.table-secondary,.table-secondary>td,.table-secondary>th{color:#343a40}.table-hover .table-secondary:hover{color:#343a40}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{color:#343a40}.table-success,.table-success>td,.table-success>th{color:#343a40}.table-hover .table-success:hover{color:#343a40}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{color:#343a40}.table-info,.table-info>td,.table-info>th{color:#343a40}.table-hover .table-info:hover{color:#343a40}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{color:#343a40}.table-warning,.table-warning>td,.table-warning>th{color:#343a40}.table-hover .table-warning:hover{color:#343a40}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{color:#343a40}.table-danger,.table-danger>td,.table-danger>th{color:#343a40}.table-hover .table-danger:hover{color:#343a40}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{color:#343a40}.table-light,.table-light>td,.table-light>th{color:#343a40}.table-hover .table-light:hover{color:#343a40}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{color:#343a40}.table-dark,.table-dark>td,.table-dark>th{color:#343a40}.table-hover .table-dark:hover{color:#343a40}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{color:#343a40}.table-active,.table-active>td,.table-active>th{color:#e9ecef}.table-hover .table-active:hover{color:#e9ecef}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{color:#e9ecef}.table-dark{color:#dee2e6}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#dee2e6;background-color:#000;background-clip:padding-box;border:1px solid #6c757d;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#dee2e6;background-color:#191d21;border-color:#b3d7ff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#343a40;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #dee2e6}select.form-control:focus::-ms-value{color:#dee2e6;background-color:#000}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#e9ecef;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#e9ecef;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#d3d3d3;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#d3d3d3;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#e9ecef;background-color:#3395ff;border-color:#3395ff}.btn-primary:hover{color:#e9ecef;background-color:#0d82ff;border-color:#007bff}.btn-primary.focus,.btn-primary:focus{color:#e9ecef;background-color:#0d82ff;border-color:#007bff;box-shadow:0 0 0 .2rem rgba(78,162,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#e9ecef;background-color:#3395ff;border-color:#3395ff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#e9ecef;background-color:#007bff;border-color:#0075f2}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(78,162,253,.5)}.btn-secondary{color:#e9ecef;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#e9ecef;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#e9ecef;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(127,135,142,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#e9ecef;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#e9ecef;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(127,135,142,.5)}.btn-success{color:#e9ecef;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#e9ecef;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#e9ecef;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(69,177,95,.5)}.btn-success.disabled,.btn-success:disabled{color:#e9ecef;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#e9ecef;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(69,177,95,.5)}.btn-info{color:#e9ecef;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#e9ecef;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#e9ecef;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(55,173,192,.5)}.btn-info.disabled,.btn-info:disabled{color:#e9ecef;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#e9ecef;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(55,173,192,.5)}.btn-warning{color:#343a40;background-color:#fd7e14;border-color:#fd7e14}.btn-warning:hover{color:#e9ecef;background-color:#e96b02;border-color:#dc6502}.btn-warning.focus,.btn-warning:focus{color:#e9ecef;background-color:#e96b02;border-color:#dc6502;box-shadow:0 0 0 .2rem rgba(223,116,27,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#343a40;background-color:#fd7e14;border-color:#fd7e14}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#e9ecef;background-color:#dc6502;border-color:#cf5f02}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(223,116,27,.5)}.btn-danger{color:#e9ecef;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#e9ecef;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#e9ecef;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(222,80,95,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#e9ecef;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#e9ecef;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,80,95,.5)}.btn-light{color:#343a40;background-color:#dee2e6;border-color:#dee2e6}.btn-light:hover{color:#343a40;background-color:#c8cfd6;border-color:#c1c9d0}.btn-light.focus,.btn-light:focus{color:#343a40;background-color:#c8cfd6;border-color:#c1c9d0;box-shadow:0 0 0 .2rem rgba(197,201,205,.5)}.btn-light.disabled,.btn-light:disabled{color:#343a40;background-color:#dee2e6;border-color:#dee2e6}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#343a40;background-color:#c1c9d0;border-color:#bac2cb}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(197,201,205,.5)}.btn-dark{color:#e9ecef;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#e9ecef;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#e9ecef;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(79,85,90,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#e9ecef;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#e9ecef;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(79,85,90,.5)}.btn-outline-primary{color:#3395ff;border-color:#3395ff}.btn-outline-primary:hover{color:#e9ecef;background-color:#3395ff;border-color:#3395ff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(51,149,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#3395ff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#e9ecef;background-color:#3395ff;border-color:#3395ff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(51,149,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#e9ecef;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#e9ecef;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#e9ecef;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#e9ecef;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#e9ecef;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#e9ecef;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#fd7e14;border-color:#fd7e14}.btn-outline-warning:hover{color:#343a40;background-color:#fd7e14;border-color:#fd7e14}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(253,126,20,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#fd7e14;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#343a40;background-color:#fd7e14;border-color:#fd7e14}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(253,126,20,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#e9ecef;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#e9ecef;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#dee2e6;border-color:#dee2e6}.btn-outline-light:hover{color:#343a40;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#dee2e6;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#343a40;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#e9ecef;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#e9ecef;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#adadad;text-decoration:none}.btn-link:hover{color:#878787;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.width{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.width{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#d3d3d3;text-align:left;list-style:none;background-color:#000;background-clip:padding-box;border:1px solid rgba(255,255,255,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #343a40}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#f8f9fa;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#fff;text-decoration:none;background-color:#212529}.dropdown-item.active,.dropdown-item:active{color:#000;text-decoration:none;background-color:#3395ff}.dropdown-item.disabled,.dropdown-item:disabled{color:#ced4da;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#ced4da;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#f8f9fa}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#dee2e6;text-align:center;white-space:nowrap;background-color:#343a40;border:1px solid #6c757d;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.input-group-append>.custom-select{border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.input-group-prepend>.custom-select{border-top-right-radius:0;border-bottom-right-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;print-color-adjust:exact}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:1px solid #adb5bd}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#dee2e6;vertical-align:middle;background:#000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #6c757d;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#dee2e6;background-color:#000}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#ced4da;background-color:#343a40}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #dee2e6}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid rgba(255,255,255,.125)}.nav-tabs .nav-link{margin-bottom:-1px;background-color:transparent;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:#495057 #495057 rgba(255,255,255,.125)}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#f8f9fa;background-color:#191d21;border-color:#495057 #495057 #191d21}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-themed .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover,.navbar-themed .navbar-brand:focus,.navbar-themed .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link,.navbar-themed .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover,.navbar-themed .navbar-nav .nav-link:focus,.navbar-themed .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled,.navbar-themed .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link,.navbar-themed .navbar-nav .active>.nav-link,.navbar-themed .navbar-nav .nav-link.active,.navbar-themed .navbar-nav .nav-link.show,.navbar-themed .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler,.navbar-themed .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon,.navbar-themed .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text,.navbar-themed .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a,.navbar-themed .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover,.navbar-themed .navbar-text a:focus,.navbar-themed .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#212529;background-clip:border-box;border:1px solid rgba(255,255,255,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#343a40;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#ced4da;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#ced4da}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#adadad;background-color:#000;border:1px solid #495057}.page-link:hover{z-index:2;color:#878787;text-decoration:none;background-color:#343a40;border-color:#495057}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#000;background-color:#3395ff;border-color:#3395ff}.page-item.disabled .page-link{color:#ced4da;pointer-events:none;cursor:auto;background-color:#000;border-color:#495057}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#e9ecef;background-color:#3395ff}a.badge-primary:focus,a.badge-primary:hover{color:#e9ecef;background-color:#007bff}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(51,149,255,.5)}.badge-secondary{color:#e9ecef;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#e9ecef;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#e9ecef;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#e9ecef;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#e9ecef;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#e9ecef;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#343a40;background-color:#fd7e14}a.badge-warning:focus,a.badge-warning:hover{color:#343a40;background-color:#dc6502}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(253,126,20,.5)}.badge-danger{color:#e9ecef;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#e9ecef;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#343a40;background-color:#dee2e6}a.badge-light:focus,a.badge-light:hover{color:#343a40;background-color:#c1c9d0}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.badge-dark{color:#e9ecef;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#e9ecef;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#343a40;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#1b4e85;background-color:#d6eaff;border-color:#c6e1ff}.alert-primary hr{border-top-color:#add4ff}.alert-primary .alert-link{color:#12355b}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#84420a;background-color:#ffe5d0;border-color:#fedbbd}.alert-warning hr{border-top-color:#fecda4}.alert-warning .alert-link{color:#552a06}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light,.alert-themed-inverted{color:#737678;background-color:#f8f9fa;border-color:#f6f7f8}.alert-light hr,.alert-themed-inverted hr{border-top-color:#e8eaed}.alert-light .alert-link,.alert-themed-inverted .alert-link{color:#5a5c5e}.alert-dark,.alert-themed{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr,.alert-themed hr{border-top-color:#b9bbbe}.alert-dark .alert-link,.alert-themed .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#dee2e6;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#dee2e6;text-decoration:none;background-color:#212529}.list-group-item-action:active{color:#d3d3d3;background-color:#343a40}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:rgba(25,29,33,.05);border:1px solid rgba(255,255,255,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#ced4da;pointer-events:none;background-color:rgba(25,29,33,.05)}.list-group-item.active{z-index:2;color:#000;background-color:#3395ff;border-color:#3395ff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1b4e85;background-color:#c6e1ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#1b4e85;background-color:#add4ff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1b4e85;border-color:#1b4e85}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#84420a;background-color:#fedbbd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#84420a;background-color:#fecda4}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#84420a;border-color:#84420a}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#737678;background-color:#f6f7f8}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#737678;background-color:#e8eaed}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#737678;border-color:#737678}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 0 #000;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(0,0,0,.85);background-clip:padding-box;border:1px solid rgba(255,255,255,.1);box-shadow:0 .25rem .75rem rgba(255,255,255,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#ced4da;background-color:rgba(0,0,0,.85);background-clip:padding-box;border-bottom:1px solid rgba(255,255,255,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#191d21;background-clip:padding-box;border:1px solid rgba(255,255,255,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #343a40;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #343a40;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentcolor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentcolor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#3395ff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#007bff!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#fd7e14!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#dc6502!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light,.bg-themed-inverted{background-color:#dee2e6!important}a.bg-light:focus,a.bg-light:hover,a.bg-themed-inverted:focus,a.bg-themed-inverted:hover,button.bg-light:focus,button.bg-light:hover,button.bg-themed-inverted:focus,button.bg-themed-inverted:hover{background-color:#c1c9d0!important}.bg-dark,.bg-themed,.navbar-themed{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,a.bg-themed:focus,a.bg-themed:hover,a.navbar-themed:focus,a.navbar-themed:hover,button.bg-dark:focus,button.bg-dark:hover,button.bg-themed:focus,button.bg-themed:hover,button.navbar-themed:focus,button.navbar-themed:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #343a40!important}.border-top{border-top:1px solid #343a40!important}.border-right{border-right:1px solid #343a40!important}.border-bottom{border-bottom:1px solid #343a40!important}.border-left{border-left:1px solid #343a40!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#3395ff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#fd7e14!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#dee2e6!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#3395ff!important}a.text-primary:focus,a.text-primary:hover{color:#006fe6!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#fd7e14!important}a.text-warning:focus,a.text-warning:hover{color:#c35a02!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light,.text-themed{color:#dee2e6!important}a.text-light:focus,a.text-light:hover,a.text-themed:focus,a.text-themed:hover{color:#b2bcc5!important}.text-dark,.text-themed-inverted{color:#343a40!important}a.text-dark:focus,a.text-dark:hover,a.text-themed-inverted:focus,a.text-themed-inverted:hover{color:#121416!important}.text-body{color:#d3d3d3!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#343a40}.table .thead-dark th{color:inherit;border-color:#343a40}} \ No newline at end of file diff --git a/flask__webservers/url_shortener/static/js/jquery-3.1.1.min.js b/flask__webservers/url_shortener/static/js/jquery-3.1.1.min.js new file mode 100644 index 000000000..4c5be4c0f --- /dev/null +++ b/flask__webservers/url_shortener/static/js/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + + + + + + +
+
+
+

{{ title }}

+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ + + + + diff --git a/flask__webservers/users/main_with_db.py b/flask__webservers/users/main_with_db.py new file mode 100644 index 000000000..7f8fb28af --- /dev/null +++ b/flask__webservers/users/main_with_db.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.geeksforgeeks.org/how-to-add-authentication-to-your-app-with-flask-login/ + + +import os + +from typing import Optional + +# pip install flask==3.0.0 +import flask + +# pip install flask-login==0.6.2 +import flask_login + +# pip install flask-sqlalchemy==3.1.1 +# TODO: Bug fix for flask-login==0.6.2 - flask-sqlalchemy installed/updated Werkzeug +# pip install Werkzeug==2.3.7 +from flask_sqlalchemy import SQLAlchemy + + +# TODO: Убрать дублирование кода в шаблонах +# TODO: Не хранить пароль в чистом виде +# TODO: Сделать вариант примера с peewee + + +app = flask.Flask(__name__) +app.secret_key = "super secret string" # Change this! +app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI", "sqlite:///db.sqlite") + +db = SQLAlchemy() + +login_manager = flask_login.LoginManager() +login_manager.init_app(app) + + +class User(db.Model, flask_login.UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(250), unique=True, nullable=False) + password = db.Column(db.String(250), nullable=False) + + @classmethod + def get_by(cls, username: str) -> Optional["User"]: + return cls.query.filter_by( + username=username + ).first() + + +db.init_app(app) + +with app.app_context(): + db.create_all() + + +@login_manager.user_loader +def loader_user(user_id: int): + return db.session.get(User, user_id) + + +@app.route("/") +def index(): + return flask.render_template_string(""" + + + + + + + Home + + + + + {% if current_user.is_authenticated %} +

You are logged as {{ current_user.username }}

+ {% else %} +

You are not logged in

+ {% endif %} + + + """) + + +@app.route('/register', methods=["GET", "POST"]) +def register(): + if flask.request.method == "POST": + username = flask.request.form.get("username") + if User.get_by(username): + return "There is already a user with this nickname" + + password = flask.request.form.get("password") + + user = User( + username=username, + password=password, + ) + db.session.add(user) + db.session.commit() + + return flask.redirect(flask.url_for("login")) + + return flask.render_template_string(""" + + + + + + + Sign Up + + + + +

Create an account

+
+ + + + + +
+ + + """) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if flask.request.method == "POST": + username = flask.request.form.get("username") + password = flask.request.form.get("password") + + user = User.get_by(username) + if user and user.password == password: + flask_login.login_user(user) + return flask.redirect(flask.url_for("index")) + + return "Bad login" + + return flask.render_template_string(""" + + + + + + + Login + + + + +

Login to your account

+
+ + + + + +
+ + + """) + + +@app.route("/logout") +def logout(): + flask_login.logout_user() + return flask.redirect(flask.url_for("index")) + + +@app.route("/protected") +@flask_login.login_required +def protected() -> str: + return f"Logged in as: {flask_login.current_user.username}" + + +if __name__ == "__main__": + app.debug = True + app.run(port=10101) diff --git a/flask__webservers/users/main_without_db.py b/flask__webservers/users/main_without_db.py new file mode 100644 index 000000000..927caa1fd --- /dev/null +++ b/flask__webservers/users/main_without_db.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +# pip install flask==3.0.0 +import flask + +# pip install flask-login==0.6.2 +import flask_login + + +LOGIN = os.environ.get("ADMIN_LOGIN", "foo@bar.example") +PASSWORD = os.environ.get("ADMIN_PASSWORD", "secret") + +# Our mock database. +USERS = { + LOGIN: { + "password": PASSWORD, + }, +} + + +app = flask.Flask(__name__) +app.secret_key = "super secret string" # Change this! + +login_manager = flask_login.LoginManager() +login_manager.init_app(app) + + +class User(flask_login.UserMixin): + @classmethod + def create(cls, login: str) -> "User": + user = User() + user.id = login + return user + + +# Если авторизован +@login_manager.user_loader +def user_loader(email): + if email not in USERS: + return + + return User.create(email) + + +# Если не авторизован +@login_manager.request_loader +def request_loader(request): + email = request.form.get("email") + if email not in USERS: + return + + return User.create(email) + + +@app.route("/") +def index(): + return flask.render_template_string( + """ + + + + + + + Home + + + + + {% if current_user.is_authenticated %} +

You are logged as {{ current_user.id }}

+ Protected + {% else %} +

You are not logged in

+ {% endif %} + + + """ + ) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if flask.request.method == "GET": + return """ +
+ + + +
+ """ + + email = flask.request.form["email"] + if email in USERS and flask.request.form["password"] == USERS[email]["password"]: + user = User.create(email) + flask_login.login_user(user) + return flask.redirect(flask.url_for("index")) + + return "Bad login" + + +@app.route("/protected") +@flask_login.login_required +def protected() -> str: + return f"Logged in as: {flask_login.current_user.id}" + + +@app.route("/logout") +@flask_login.login_required +def logout(): + flask_login.logout_user() + return flask.redirect(flask.url_for("index")) + + +@login_manager.unauthorized_handler +def unauthorized_handler(): + return "Unauthorized", 401 + + +if __name__ == "__main__": + app.debug = True + app.run(port=10101) diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.js b/flask__websocket/ajax_vs_websocket/_static/socket.io.js deleted file mode 100644 index d3e395011..000000000 --- a/flask__websocket/ajax_vs_websocket/_static/socket.io.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Socket.IO v2.2.0 - * (c) 2014-2018 Guillermo Rauch - * Released under the MIT License. - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,u=r.id,h=r.path,f=p[u]&&h in p[u].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(p[u]||(c("new io instance for %s",s),p[u]=a(s,e)),n=p[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(12),c=n(3)("socket.io-client");t.exports=e=r;var p=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(12),e.Socket=n(36)},function(t,e,n){"use strict";function r(t,e){var n=t;e=e||"undefined"!=typeof location&&location,null==t&&(t=e.protocol+"//"+e.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?e.protocol+t:e.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof e?e.protocol+"//"+t:"https://"+t),i("parse %s",t),n=o(t)),n.port||(/^(http|ws)$/.test(n.protocol)?n.port="80":/^(http|ws)s$/.test(n.protocol)&&(n.port="443")),n.path=n.path||"/";var r=n.host.indexOf(":")!==-1,s=r?"["+n.host+"]":n.host;return n.id=n.protocol+"://"+s+":"+n.port,n.href=n.protocol+"://"+s+(e&&e.port===n.port?"":":"+n.port),n}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function p(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:p(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(u===setTimeout)return setTimeout(t,0);if((u===n||!u)&&setTimeout)return u=setTimeout,setTimeout(t,0);try{return u(t,0)}catch(e){try{return u.call(null,t,0)}catch(e){return u.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*u;case"days":case"day":case"d":return n*p;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=p?Math.round(t/p)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,p,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=p(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function p(t){try{return JSON.parse(t)}catch(e){return!1}}function u(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(3)("socket.io-parser"),l=n(8),d=n(9),y=n(10),m=n(11);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new u(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},u.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},u.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=u(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=u(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(35),p=n(21),u=n(2),h=n(29);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=p.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=p.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),u.send([{type:"ping",data:"probe"}]),u.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",u),!u)return;r.priorWebsocketSuccess="websocket"===u.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),p(),f.setTransport(u),u.send([{type:"upgrade"}]),f.emit("upgrade",u),u=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=u.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,p(),u.close(),u=null)}function o(e){var r=new Error("probe error: "+e);r.transport=u.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){u&&t.name!==u.name&&(a('"%s" works - aborting "%s"',t.name,u.name),n())}function p(){u.removeListener("open",e),u.removeListener("error",o),u.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var u=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,u.once("open",e),u.once("error",o),u.once("close",i),this.once("close",s),this.once("upgrading",c),u.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!p)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=p.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,p=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",p=1;255!==s[p];p++){if(c.length>310)return r(w,0,1);c+=s[p]}o=f(o,2+c.length),c=parseInt(c);var u=f(o,0,c);if(a)try{u=String.fromCharCode.apply(null,new Uint8Array(u))}catch(h){var l=new Uint8Array(u);u="";for(var p=0;pr&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,p="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function p(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function u(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=p(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */ -var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:u}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,p=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new ArrayBuffer(a),h=new Uint8Array(u);for(e=0;e>4,h[p++]=(15&o)<<4|i>>2,h[p++]=(3&i)<<6|63&s;return u}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(u=0;u';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(p,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(20),a=n(21),c=n(29),p=n(30),u=n(31),h=n(3)("engine.io-client:websocket");if("undefined"==typeof self)try{i=n(34)}catch(f){}else o=self.WebSocket||self.MozWebSocket;var l=o||i;t.exports=r,p(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); -//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js new file mode 100644 index 000000000..f39af5c95 --- /dev/null +++ b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js @@ -0,0 +1,7 @@ +/*! + * Socket.IO v4.7.2 + * (c) 2014-2023 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).io=e()}(this,(function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,o=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var g=Object.create(null);Object.keys(v).forEach((function(t){g[v[t]]=t}));var m,b={type:"error",data:"parser error"},k="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),w="function"==typeof ArrayBuffer,_=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t&&t.buffer instanceof ArrayBuffer},A=function(t,e,n){var r=t.type,i=t.data;return k&&i instanceof Blob?e?n(i):O(i,n):w&&(i instanceof ArrayBuffer||_(i))?e?n(i):O(new Blob([i]),n):n(v[r]+(i||""))},O=function(t,e){var n=new FileReader;return n.onload=function(){var t=n.result.split(",")[1];e("b"+(t||""))},n.readAsDataURL(t)};function E(t){return t instanceof Uint8Array?t:t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength)}for(var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",R="undefined"==typeof Uint8Array?[]:new Uint8Array(256),C=0;C<64;C++)R[T.charCodeAt(C)]=C;var B,S="function"==typeof ArrayBuffer,N=function(t,e){if("string"!=typeof t)return{type:"message",data:x(t,e)};var n=t.charAt(0);return"b"===n?{type:"message",data:L(t.substring(1),e)}:g[n]?t.length>1?{type:g[n],data:t.substring(1)}:{type:g[n]}:b},L=function(t,e){if(S){var n=function(t){var e,n,r,i,o,s=.75*t.length,a=t.length,u=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var c=new ArrayBuffer(s),h=new Uint8Array(c);for(e=0;e>4,h[u++]=(15&r)<<4|i>>2,h[u++]=(3&i)<<6|63&o;return c}(t);return x(n,e)}return{base64:!0,data:t}},x=function(t,e){return"blob"===e?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},P=String.fromCharCode(30);function q(){return new TransformStream({transform:function(t,e){!function(t,e){k&&t.data instanceof Blob?t.data.arrayBuffer().then(E).then(e):w&&(t.data instanceof ArrayBuffer||_(t.data))?e(E(t.data)):A(t,!1,(function(t){m||(m=new TextEncoder),e(m.encode(t))}))}(t,(function(n){var r,i=n.length;if(i<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,i);else if(i<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,i)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(i))}t.data&&"string"!=typeof t.data&&(r[0]|=128),e.enqueue(r),e.enqueue(n)}))}})}function j(t){return t.reduce((function(t,e){return t+e.length}),0)}function D(t,e){if(t[0].length===e)return t.shift();for(var n=new Uint8Array(e),r=0,i=0;i1?e-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this._hostname()+this._port()+this.opts.path+this._query(e)}},{key:"_hostname",value:function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"}},{key:"_port",value:function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""}},{key:"_query",value:function(t){var e=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e}(t);return e.length?"?"+e:""}}]),i}(U),z="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),J=64,$={},Q=0,X=0;function G(t){var e="";do{e=z[t%J]+e,t=Math.floor(t/J)}while(t>0);return e}function Z(){var t=G(+new Date);return t!==K?(Q=0,K=t):t+"."+G(Q++)}for(;X0&&void 0!==arguments[0]?arguments[0]:{};return i(t,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new st(this.uri(),t)}},{key:"doWrite",value:function(t,e){var n=this,r=this.request({method:"POST",data:t});r.on("success",e),r.on("error",(function(t,e){n.onError("xhr post error",t,e)}))}},{key:"doPoll",value:function(){var t=this,e=this.request();e.on("data",this.onData.bind(this)),e.on("error",(function(e,n){t.onError("xhr poll error",e,n)})),this.pollXhr=e}}]),s}(W),st=function(t){o(i,t);var n=l(i);function i(t,r){var o;return e(this,i),H(f(o=n.call(this)),r),o.opts=r,o.method=r.method||"GET",o.uri=t,o.data=void 0!==r.data?r.data:null,o.create(),o}return r(i,[{key:"create",value:function(){var t,e=this,n=F(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this.opts.xd;var r=this.xhr=new nt(n);try{r.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders)for(var o in r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this.opts.extraHeaders[o])}catch(t){}if("POST"===this.method)try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(t){}try{r.setRequestHeader("Accept","*/*")}catch(t){}null===(t=this.opts.cookieJar)||void 0===t||t.addCookies(r),"withCredentials"in r&&(r.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(r.timeout=this.opts.requestTimeout),r.onreadystatechange=function(){var t;3===r.readyState&&(null===(t=e.opts.cookieJar)||void 0===t||t.parseCookies(r)),4===r.readyState&&(200===r.status||1223===r.status?e.onLoad():e.setTimeoutFn((function(){e.onError("number"==typeof r.status?r.status:0)}),0))},r.send(this.data)}catch(t){return void this.setTimeoutFn((function(){e.onError(t)}),0)}"undefined"!=typeof document&&(this.index=i.requestsCount++,i.requests[this.index]=this)}},{key:"onError",value:function(t){this.emitReserved("error",t,this.xhr),this.cleanup(!0)}},{key:"cleanup",value:function(t){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=rt,t)try{this.xhr.abort()}catch(t){}"undefined"!=typeof document&&delete i.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var t=this.xhr.responseText;null!==t&&(this.emitReserved("data",t),this.emitReserved("success"),this.cleanup())}},{key:"abort",value:function(){this.cleanup()}}]),i}(U);if(st.requestsCount=0,st.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",at);else if("function"==typeof addEventListener){addEventListener("onpagehide"in I?"pagehide":"unload",at,!1)}function at(){for(var t in st.requests)st.requests.hasOwnProperty(t)&&st.requests[t].abort()}var ut="function"==typeof Promise&&"function"==typeof Promise.resolve?function(t){return Promise.resolve().then(t)}:function(t,e){return e(t,0)},ct=I.WebSocket||I.MozWebSocket,ht="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),ft=function(t){o(i,t);var n=l(i);function i(t){var r;return e(this,i),(r=n.call(this,t)).supportsBinary=!t.forceBase64,r}return r(i,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var t=this.uri(),e=this.opts.protocols,n=ht?{}:F(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=ht?new ct(t,e,n):e?new ct(t,e):new ct(t)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}}},{key:"addEventListeners",value:function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws._socket.unref(),t.onOpen()},this.ws.onclose=function(e){return t.onClose({description:"websocket connection closed",context:e})},this.ws.onmessage=function(e){return t.onData(e.data)},this.ws.onerror=function(e){return t.onError("websocket error",e)}}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=function(){var n=t[r],i=r===t.length-1;A(n,e.supportsBinary,(function(t){try{e.ws.send(t)}catch(t){}i&&ut((function(){e.writable=!0,e.emitReserved("drain")}),e.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){a.enqueue(b);break}i=l*Math.pow(2,32)+f.getUint32(4),r=3}else{if(j(n)t){a.enqueue(b);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=e.readable.pipeThrough(n).getReader(),i=q();i.readable.pipeTo(e.writable),t.writer=i.writable.getWriter();!function e(){r.read().then((function(n){var r=n.done,i=n.value;r||(t.onPacket(i),e())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.writer.write(o).then((function(){return t.onOpen()}))}))})))}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=function(){var n=t[r],i=r===t.length-1;e.writer.write(n).then((function(){i&&ut((function(){e.writable=!0,e.emitReserved("drain")}),e.setTimeoutFn)}))},r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return e(this,a),(r=s.call(this)).binaryType="arraybuffer",r.writeBuffer=[],n&&"object"===t(n)&&(o=n,n=null),n?(n=vt(n),o.hostname=n.host,o.secure="https"===n.protocol||"wss"===n.protocol,o.port=n.port,n.query&&(o.query=n.query)):o.host&&(o.hostname=vt(o.host).host),H(f(r),o),r.secure=null!=o.secure?o.secure:"undefined"!=typeof location&&"https:"===location.protocol,o.hostname&&!o.port&&(o.port=r.secure?"443":"80"),r.hostname=o.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=o.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=o.transports||["polling","websocket","webtransport"],r.writeBuffer=[],r.prevBufferLen=0,r.opts=i({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},o),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var e={},n=t.split("&"),r=0,i=n.length;r1))return this.writeBuffer;for(var t,e=1,n=0;n=57344?n+=3:(r++,n+=4);return n}(t):Math.ceil(1.33*(t.byteLength||t.size))),n>0&&e>this.maxPayload)return this.writeBuffer.slice(0,n);e+=2}return this.writeBuffer}},{key:"write",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"send",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"sendPacket",value:function(t,e,n,r){if("function"==typeof e&&(r=e,e=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var i={type:t,data:e,options:n};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),r&&this.once("flush",r),this.flush()}}},{key:"close",value:function(){var t=this,e=function(){t.onClose("forced close"),t.transport.close()},n=function n(){t.off("upgrade",n),t.off("upgradeError",n),e()},r=function(){t.once("upgrade",n),t.once("upgradeError",n)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():e()})):this.upgrading?r():e()),this}},{key:"onError",value:function(t){a.priorWebsocketSuccess=!1,this.emitReserved("error",t),this.onClose("transport error",t)}},{key:"onClose",value:function(t,e){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",t,e),this.writeBuffer=[],this.prevBufferLen=0)}},{key:"filterUpgrades",value:function(t){for(var e=[],n=0,r=t.length;n=0&&e.num1?e-1:0),r=1;r1?n-1:0),i=1;in._opts.retries&&(n._queue.shift(),e&&e(t));else if(n._queue.shift(),e){for(var i=arguments.length,o=new Array(i>1?i-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this._queue.length){var e=this._queue[0];e.pending&&!t||(e.pending=!0,e.tryCount++,this.flags=e.flags,this.emit.apply(this,e.args))}}},{key:"packet",value:function(t){t.nsp=this.nsp,this.io._packet(t)}},{key:"onopen",value:function(){var t=this;"function"==typeof this.auth?this.auth((function(e){t._sendConnectPacket(e)})):this._sendConnectPacket(this.auth)}},{key:"_sendConnectPacket",value:function(t){this.packet({type:Bt.CONNECT,data:this._pid?i({pid:this._pid,offset:this._lastOffset},t):t})}},{key:"onerror",value:function(t){this.connected||this.emitReserved("connect_error",t)}},{key:"onclose",value:function(t,e){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,e)}},{key:"onpacket",value:function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var e=new Error(t.data.message);e.data=t.data.data,this.emitReserved("connect_error",e)}}},{key:"onevent",value:function(t){var e=t.data||[];null!=t.id&&e.push(this.ack(t.id)),this.connected?this.emitEvent(e):this.receiveBuffer.push(Object.freeze(e))}},{key:"emitEvent",value:function(t){if(this._anyListeners&&this._anyListeners.length){var e,n=y(this._anyListeners.slice());try{for(n.s();!(e=n.n()).done;){e.value.apply(this,t)}}catch(t){n.e(t)}finally{n.f()}}p(s(a.prototype),"emit",this).apply(this,t),this._pid&&t.length&&"string"==typeof t[t.length-1]&&(this._lastOffset=t[t.length-1])}},{key:"ack",value:function(t){var e=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,i=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}It.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},It.prototype.reset=function(){this.attempts=0},It.prototype.setMin=function(t){this.ms=t},It.prototype.setMax=function(t){this.max=t},It.prototype.setJitter=function(t){this.jitter=t};var Ft=function(n){o(s,n);var i=l(s);function s(n,r){var o,a;e(this,s),(o=i.call(this)).nsps={},o.subs=[],n&&"object"===t(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",o.opts=r,H(f(o),r),o.reconnection(!1!==r.reconnection),o.reconnectionAttempts(r.reconnectionAttempts||1/0),o.reconnectionDelay(r.reconnectionDelay||1e3),o.reconnectionDelayMax(r.reconnectionDelayMax||5e3),o.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),o.backoff=new It({min:o.reconnectionDelay(),max:o.reconnectionDelayMax(),jitter:o.randomizationFactor()}),o.timeout(null==r.timeout?2e4:r.timeout),o._readyState="closed",o.uri=n;var u=r.parser||qt;return o.encoder=new u.Encoder,o.decoder=new u.Decoder,o._autoConnect=!1!==r.autoConnect,o._autoConnect&&o.open(),o}return r(s,[{key:"reconnection",value:function(t){return arguments.length?(this._reconnection=!!t,this):this._reconnection}},{key:"reconnectionAttempts",value:function(t){return void 0===t?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}},{key:"reconnectionDelay",value:function(t){var e;return void 0===t?this._reconnectionDelay:(this._reconnectionDelay=t,null===(e=this.backoff)||void 0===e||e.setMin(t),this)}},{key:"randomizationFactor",value:function(t){var e;return void 0===t?this._randomizationFactor:(this._randomizationFactor=t,null===(e=this.backoff)||void 0===e||e.setJitter(t),this)}},{key:"reconnectionDelayMax",value:function(t){var e;return void 0===t?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,null===(e=this.backoff)||void 0===e||e.setMax(t),this)}},{key:"timeout",value:function(t){return arguments.length?(this._timeout=t,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(t){var e=this;if(~this._readyState.indexOf("open"))return this;this.engine=new gt(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var i=jt(n,"open",(function(){r.onopen(),t&&t()})),o=function(n){e.cleanup(),e._readyState="closed",e.emitReserved("error",n),t?t(n):e.maybeReconnectOnOpen()},s=jt(n,"error",o);if(!1!==this._timeout){var a=this._timeout,u=this.setTimeoutFn((function(){i(),o(new Error("timeout")),n.close()}),a);this.opts.autoUnref&&u.unref(),this.subs.push((function(){e.clearTimeoutFn(u)}))}return this.subs.push(i),this.subs.push(s),this}},{key:"connect",value:function(t){return this.open(t)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var t=this.engine;this.subs.push(jt(t,"ping",this.onping.bind(this)),jt(t,"data",this.ondata.bind(this)),jt(t,"error",this.onerror.bind(this)),jt(t,"close",this.onclose.bind(this)),jt(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}}},{key:"ondecoded",value:function(t){var e=this;ut((function(){e.emitReserved("packet",t)}),this.setTimeoutFn)}},{key:"onerror",value:function(t){this.emitReserved("error",t)}},{key:"socket",value:function(t,e){var n=this.nsps[t];return n?this._autoConnect&&!n.active&&n.connect():(n=new Ut(this,t,e),this.nsps[t]=n),n}},{key:"_destroy",value:function(t){for(var e=0,n=Object.keys(this.nsps);e=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){e.skipReconnect||(t.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((function(n){n?(e._reconnecting=!1,e.reconnect(),t.emitReserved("reconnect_error",n)):e.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}}},{key:"onreconnect",value:function(){var t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}]),s}(U),Mt={};function Vt(e,n){"object"===t(e)&&(n=e,e=void 0);var r,i=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=t;n=n||"undefined"!=typeof location&&location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==n?n.protocol+"//"+t:"https://"+t),r=vt(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var i=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+e,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}(e,(n=n||{}).path||"/socket.io"),o=i.source,s=i.id,a=i.path,u=Mt[s]&&a in Mt[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?r=new Ft(o,n):(Mt[s]||(Mt[s]=new Ft(o,n)),r=Mt[s]),i.query&&!n.query&&(n.query=i.queryKey),r.socket(i.path,n)}return i(Vt,{Manager:Ft,Socket:Ut,io:Vt,connect:Vt}),Vt})); +//# sourceMappingURL=socket.io.min.js.map diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map new file mode 100644 index 000000000..6a27b095e --- /dev/null +++ b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"socket.io.min.js","sources":["../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/commons.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/encodePacket.browser.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/index.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/decodePacket.browser.js","../node_modules/@socket.io/component-emitter/index.mjs","../node_modules/engine.io-client/build/esm/globalThis.browser.js","../node_modules/engine.io-client/build/esm/util.js","../node_modules/engine.io-client/build/esm/transport.js","../node_modules/engine.io-client/build/esm/contrib/yeast.js","../node_modules/engine.io-client/build/esm/contrib/parseqs.js","../node_modules/engine.io-client/build/esm/contrib/has-cors.js","../node_modules/engine.io-client/build/esm/transports/xmlhttprequest.browser.js","../node_modules/engine.io-client/build/esm/transports/polling.js","../node_modules/engine.io-client/build/esm/transports/websocket-constructor.browser.js","../node_modules/engine.io-client/build/esm/transports/websocket.js","../node_modules/engine.io-client/build/esm/transports/webtransport.js","../node_modules/engine.io-client/build/esm/transports/index.js","../node_modules/engine.io-client/build/esm/contrib/parseuri.js","../node_modules/engine.io-client/build/esm/socket.js","../node_modules/engine.io-client/build/esm/index.js","../node_modules/socket.io-parser/build/esm/is-binary.js","../node_modules/socket.io-parser/build/esm/binary.js","../node_modules/socket.io-parser/build/esm/index.js","../build/esm/on.js","../build/esm/socket.js","../build/esm/contrib/backo2.js","../build/esm/manager.js","../build/esm/index.js","../build/esm/url.js"],"sourcesContent":["const PACKET_TYPES = Object.create(null); // no Map = no polyfill\nPACKET_TYPES[\"open\"] = \"0\";\nPACKET_TYPES[\"close\"] = \"1\";\nPACKET_TYPES[\"ping\"] = \"2\";\nPACKET_TYPES[\"pong\"] = \"3\";\nPACKET_TYPES[\"message\"] = \"4\";\nPACKET_TYPES[\"upgrade\"] = \"5\";\nPACKET_TYPES[\"noop\"] = \"6\";\nconst PACKET_TYPES_REVERSE = Object.create(null);\nObject.keys(PACKET_TYPES).forEach(key => {\n PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key;\n});\nconst ERROR_PACKET = { type: \"error\", data: \"parser error\" };\nexport { PACKET_TYPES, PACKET_TYPES_REVERSE, ERROR_PACKET };\n","import { PACKET_TYPES } from \"./commons.js\";\nconst withNativeBlob = typeof Blob === \"function\" ||\n (typeof Blob !== \"undefined\" &&\n Object.prototype.toString.call(Blob) === \"[object BlobConstructor]\");\nconst withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\n// ArrayBuffer.isView method is not defined in IE10\nconst isView = obj => {\n return typeof ArrayBuffer.isView === \"function\"\n ? ArrayBuffer.isView(obj)\n : obj && obj.buffer instanceof ArrayBuffer;\n};\nconst encodePacket = ({ type, data }, supportsBinary, callback) => {\n if (withNativeBlob && data instanceof Blob) {\n if (supportsBinary) {\n return callback(data);\n }\n else {\n return encodeBlobAsBase64(data, callback);\n }\n }\n else if (withNativeArrayBuffer &&\n (data instanceof ArrayBuffer || isView(data))) {\n if (supportsBinary) {\n return callback(data);\n }\n else {\n return encodeBlobAsBase64(new Blob([data]), callback);\n }\n }\n // plain string\n return callback(PACKET_TYPES[type] + (data || \"\"));\n};\nconst encodeBlobAsBase64 = (data, callback) => {\n const fileReader = new FileReader();\n fileReader.onload = function () {\n const content = fileReader.result.split(\",\")[1];\n callback(\"b\" + (content || \"\"));\n };\n return fileReader.readAsDataURL(data);\n};\nfunction toArray(data) {\n if (data instanceof Uint8Array) {\n return data;\n }\n else if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n else {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n}\nlet TEXT_ENCODER;\nexport function encodePacketToBinary(packet, callback) {\n if (withNativeBlob && packet.data instanceof Blob) {\n return packet.data\n .arrayBuffer()\n .then(toArray)\n .then(callback);\n }\n else if (withNativeArrayBuffer &&\n (packet.data instanceof ArrayBuffer || isView(packet.data))) {\n return callback(toArray(packet.data));\n }\n encodePacket(packet, false, encoded => {\n if (!TEXT_ENCODER) {\n TEXT_ENCODER = new TextEncoder();\n }\n callback(TEXT_ENCODER.encode(encoded));\n });\n}\nexport { encodePacket };\n","// imported from https://github.com/socketio/base64-arraybuffer\nconst chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nconst lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (let i = 0; i < chars.length; i++) {\n lookup[chars.charCodeAt(i)] = i;\n}\nexport const encode = (arraybuffer) => {\n let bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = '';\n for (i = 0; i < len; i += 3) {\n base64 += chars[bytes[i] >> 2];\n base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];\n base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];\n base64 += chars[bytes[i + 2] & 63];\n }\n if (len % 3 === 2) {\n base64 = base64.substring(0, base64.length - 1) + '=';\n }\n else if (len % 3 === 1) {\n base64 = base64.substring(0, base64.length - 2) + '==';\n }\n return base64;\n};\nexport const decode = (base64) => {\n let bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;\n if (base64[base64.length - 1] === '=') {\n bufferLength--;\n if (base64[base64.length - 2] === '=') {\n bufferLength--;\n }\n }\n const arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer);\n for (i = 0; i < len; i += 4) {\n encoded1 = lookup[base64.charCodeAt(i)];\n encoded2 = lookup[base64.charCodeAt(i + 1)];\n encoded3 = lookup[base64.charCodeAt(i + 2)];\n encoded4 = lookup[base64.charCodeAt(i + 3)];\n bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);\n bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);\n bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);\n }\n return arraybuffer;\n};\n","import { encodePacket, encodePacketToBinary } from \"./encodePacket.js\";\nimport { decodePacket } from \"./decodePacket.js\";\nimport { ERROR_PACKET } from \"./commons.js\";\nconst SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text\nconst encodePayload = (packets, callback) => {\n // some packets may be added to the array while encoding, so the initial length must be saved\n const length = packets.length;\n const encodedPackets = new Array(length);\n let count = 0;\n packets.forEach((packet, i) => {\n // force base64 encoding for binary packets\n encodePacket(packet, false, encodedPacket => {\n encodedPackets[i] = encodedPacket;\n if (++count === length) {\n callback(encodedPackets.join(SEPARATOR));\n }\n });\n });\n};\nconst decodePayload = (encodedPayload, binaryType) => {\n const encodedPackets = encodedPayload.split(SEPARATOR);\n const packets = [];\n for (let i = 0; i < encodedPackets.length; i++) {\n const decodedPacket = decodePacket(encodedPackets[i], binaryType);\n packets.push(decodedPacket);\n if (decodedPacket.type === \"error\") {\n break;\n }\n }\n return packets;\n};\nexport function createPacketEncoderStream() {\n return new TransformStream({\n transform(packet, controller) {\n encodePacketToBinary(packet, encodedPacket => {\n const payloadLength = encodedPacket.length;\n let header;\n // inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length\n if (payloadLength < 126) {\n header = new Uint8Array(1);\n new DataView(header.buffer).setUint8(0, payloadLength);\n }\n else if (payloadLength < 65536) {\n header = new Uint8Array(3);\n const view = new DataView(header.buffer);\n view.setUint8(0, 126);\n view.setUint16(1, payloadLength);\n }\n else {\n header = new Uint8Array(9);\n const view = new DataView(header.buffer);\n view.setUint8(0, 127);\n view.setBigUint64(1, BigInt(payloadLength));\n }\n // first bit indicates whether the payload is plain text (0) or binary (1)\n if (packet.data && typeof packet.data !== \"string\") {\n header[0] |= 0x80;\n }\n controller.enqueue(header);\n controller.enqueue(encodedPacket);\n });\n }\n });\n}\nlet TEXT_DECODER;\nfunction totalLength(chunks) {\n return chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n}\nfunction concatChunks(chunks, size) {\n if (chunks[0].length === size) {\n return chunks.shift();\n }\n const buffer = new Uint8Array(size);\n let j = 0;\n for (let i = 0; i < size; i++) {\n buffer[i] = chunks[0][j++];\n if (j === chunks[0].length) {\n chunks.shift();\n j = 0;\n }\n }\n if (chunks.length && j < chunks[0].length) {\n chunks[0] = chunks[0].slice(j);\n }\n return buffer;\n}\nexport function createPacketDecoderStream(maxPayload, binaryType) {\n if (!TEXT_DECODER) {\n TEXT_DECODER = new TextDecoder();\n }\n const chunks = [];\n let state = 0 /* READ_HEADER */;\n let expectedLength = -1;\n let isBinary = false;\n return new TransformStream({\n transform(chunk, controller) {\n chunks.push(chunk);\n while (true) {\n if (state === 0 /* READ_HEADER */) {\n if (totalLength(chunks) < 1) {\n break;\n }\n const header = concatChunks(chunks, 1);\n isBinary = (header[0] & 0x80) === 0x80;\n expectedLength = header[0] & 0x7f;\n if (expectedLength < 126) {\n state = 3 /* READ_PAYLOAD */;\n }\n else if (expectedLength === 126) {\n state = 1 /* READ_EXTENDED_LENGTH_16 */;\n }\n else {\n state = 2 /* READ_EXTENDED_LENGTH_64 */;\n }\n }\n else if (state === 1 /* READ_EXTENDED_LENGTH_16 */) {\n if (totalLength(chunks) < 2) {\n break;\n }\n const headerArray = concatChunks(chunks, 2);\n expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0);\n state = 3 /* READ_PAYLOAD */;\n }\n else if (state === 2 /* READ_EXTENDED_LENGTH_64 */) {\n if (totalLength(chunks) < 8) {\n break;\n }\n const headerArray = concatChunks(chunks, 8);\n const view = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length);\n const n = view.getUint32(0);\n if (n > Math.pow(2, 53 - 32) - 1) {\n // the maximum safe integer in JavaScript is 2^53 - 1\n controller.enqueue(ERROR_PACKET);\n break;\n }\n expectedLength = n * Math.pow(2, 32) + view.getUint32(4);\n state = 3 /* READ_PAYLOAD */;\n }\n else {\n if (totalLength(chunks) < expectedLength) {\n break;\n }\n const data = concatChunks(chunks, expectedLength);\n controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType));\n state = 0 /* READ_HEADER */;\n }\n if (expectedLength === 0 || expectedLength > maxPayload) {\n controller.enqueue(ERROR_PACKET);\n break;\n }\n }\n }\n });\n}\nexport const protocol = 4;\nexport { encodePacket, encodePayload, decodePacket, decodePayload };\n","import { ERROR_PACKET, PACKET_TYPES_REVERSE } from \"./commons.js\";\nimport { decode } from \"./contrib/base64-arraybuffer.js\";\nconst withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\nexport const decodePacket = (encodedPacket, binaryType) => {\n if (typeof encodedPacket !== \"string\") {\n return {\n type: \"message\",\n data: mapBinary(encodedPacket, binaryType)\n };\n }\n const type = encodedPacket.charAt(0);\n if (type === \"b\") {\n return {\n type: \"message\",\n data: decodeBase64Packet(encodedPacket.substring(1), binaryType)\n };\n }\n const packetType = PACKET_TYPES_REVERSE[type];\n if (!packetType) {\n return ERROR_PACKET;\n }\n return encodedPacket.length > 1\n ? {\n type: PACKET_TYPES_REVERSE[type],\n data: encodedPacket.substring(1)\n }\n : {\n type: PACKET_TYPES_REVERSE[type]\n };\n};\nconst decodeBase64Packet = (data, binaryType) => {\n if (withNativeArrayBuffer) {\n const decoded = decode(data);\n return mapBinary(decoded, binaryType);\n }\n else {\n return { base64: true, data }; // fallback for old browsers\n }\n};\nconst mapBinary = (data, binaryType) => {\n switch (binaryType) {\n case \"blob\":\n if (data instanceof Blob) {\n // from WebSocket + binaryType \"blob\"\n return data;\n }\n else {\n // from HTTP long-polling or WebTransport\n return new Blob([data]);\n }\n case \"arraybuffer\":\n default:\n if (data instanceof ArrayBuffer) {\n // from HTTP long-polling (base64) or WebSocket + binaryType \"arraybuffer\"\n return data;\n }\n else {\n // from WebTransport (Uint8Array)\n return data.buffer;\n }\n }\n};\n","/**\n * Initialize a new `Emitter`.\n *\n * @api public\n */\n\nexport function Emitter(obj) {\n if (obj) return mixin(obj);\n}\n\n/**\n * Mixin the emitter properties.\n *\n * @param {Object} obj\n * @return {Object}\n * @api private\n */\n\nfunction mixin(obj) {\n for (var key in Emitter.prototype) {\n obj[key] = Emitter.prototype[key];\n }\n return obj;\n}\n\n/**\n * Listen on the given `event` with `fn`.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.on =\nEmitter.prototype.addEventListener = function(event, fn){\n this._callbacks = this._callbacks || {};\n (this._callbacks['$' + event] = this._callbacks['$' + event] || [])\n .push(fn);\n return this;\n};\n\n/**\n * Adds an `event` listener that will be invoked a single\n * time then automatically removed.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.once = function(event, fn){\n function on() {\n this.off(event, on);\n fn.apply(this, arguments);\n }\n\n on.fn = fn;\n this.on(event, on);\n return this;\n};\n\n/**\n * Remove the given callback for `event` or all\n * registered callbacks.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.off =\nEmitter.prototype.removeListener =\nEmitter.prototype.removeAllListeners =\nEmitter.prototype.removeEventListener = function(event, fn){\n this._callbacks = this._callbacks || {};\n\n // all\n if (0 == arguments.length) {\n this._callbacks = {};\n return this;\n }\n\n // specific event\n var callbacks = this._callbacks['$' + event];\n if (!callbacks) return this;\n\n // remove all handlers\n if (1 == arguments.length) {\n delete this._callbacks['$' + event];\n return this;\n }\n\n // remove specific handler\n var cb;\n for (var i = 0; i < callbacks.length; i++) {\n cb = callbacks[i];\n if (cb === fn || cb.fn === fn) {\n callbacks.splice(i, 1);\n break;\n }\n }\n\n // Remove event specific arrays for event types that no\n // one is subscribed for to avoid memory leak.\n if (callbacks.length === 0) {\n delete this._callbacks['$' + event];\n }\n\n return this;\n};\n\n/**\n * Emit `event` with the given args.\n *\n * @param {String} event\n * @param {Mixed} ...\n * @return {Emitter}\n */\n\nEmitter.prototype.emit = function(event){\n this._callbacks = this._callbacks || {};\n\n var args = new Array(arguments.length - 1)\n , callbacks = this._callbacks['$' + event];\n\n for (var i = 1; i < arguments.length; i++) {\n args[i - 1] = arguments[i];\n }\n\n if (callbacks) {\n callbacks = callbacks.slice(0);\n for (var i = 0, len = callbacks.length; i < len; ++i) {\n callbacks[i].apply(this, args);\n }\n }\n\n return this;\n};\n\n// alias used for reserved events (protected method)\nEmitter.prototype.emitReserved = Emitter.prototype.emit;\n\n/**\n * Return array of callbacks for `event`.\n *\n * @param {String} event\n * @return {Array}\n * @api public\n */\n\nEmitter.prototype.listeners = function(event){\n this._callbacks = this._callbacks || {};\n return this._callbacks['$' + event] || [];\n};\n\n/**\n * Check if this emitter has `event` handlers.\n *\n * @param {String} event\n * @return {Boolean}\n * @api public\n */\n\nEmitter.prototype.hasListeners = function(event){\n return !! this.listeners(event).length;\n};\n","export const globalThisShim = (() => {\n if (typeof self !== \"undefined\") {\n return self;\n }\n else if (typeof window !== \"undefined\") {\n return window;\n }\n else {\n return Function(\"return this\")();\n }\n})();\n","import { globalThisShim as globalThis } from \"./globalThis.js\";\nexport function pick(obj, ...attr) {\n return attr.reduce((acc, k) => {\n if (obj.hasOwnProperty(k)) {\n acc[k] = obj[k];\n }\n return acc;\n }, {});\n}\n// Keep a reference to the real timeout functions so they can be used when overridden\nconst NATIVE_SET_TIMEOUT = globalThis.setTimeout;\nconst NATIVE_CLEAR_TIMEOUT = globalThis.clearTimeout;\nexport function installTimerFunctions(obj, opts) {\n if (opts.useNativeTimers) {\n obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis);\n obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis);\n }\n else {\n obj.setTimeoutFn = globalThis.setTimeout.bind(globalThis);\n obj.clearTimeoutFn = globalThis.clearTimeout.bind(globalThis);\n }\n}\n// base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)\nconst BASE64_OVERHEAD = 1.33;\n// we could also have used `new Blob([obj]).size`, but it isn't supported in IE9\nexport function byteLength(obj) {\n if (typeof obj === \"string\") {\n return utf8Length(obj);\n }\n // arraybuffer or blob\n return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD);\n}\nfunction utf8Length(str) {\n let c = 0, length = 0;\n for (let i = 0, l = str.length; i < l; i++) {\n c = str.charCodeAt(i);\n if (c < 0x80) {\n length += 1;\n }\n else if (c < 0x800) {\n length += 2;\n }\n else if (c < 0xd800 || c >= 0xe000) {\n length += 3;\n }\n else {\n i++;\n length += 4;\n }\n }\n return length;\n}\n","import { decodePacket } from \"engine.io-parser\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { installTimerFunctions } from \"./util.js\";\nimport { encode } from \"./contrib/parseqs.js\";\nclass TransportError extends Error {\n constructor(reason, description, context) {\n super(reason);\n this.description = description;\n this.context = context;\n this.type = \"TransportError\";\n }\n}\nexport class Transport extends Emitter {\n /**\n * Transport abstract constructor.\n *\n * @param {Object} opts - options\n * @protected\n */\n constructor(opts) {\n super();\n this.writable = false;\n installTimerFunctions(this, opts);\n this.opts = opts;\n this.query = opts.query;\n this.socket = opts.socket;\n }\n /**\n * Emits an error.\n *\n * @param {String} reason\n * @param description\n * @param context - the error context\n * @return {Transport} for chaining\n * @protected\n */\n onError(reason, description, context) {\n super.emitReserved(\"error\", new TransportError(reason, description, context));\n return this;\n }\n /**\n * Opens the transport.\n */\n open() {\n this.readyState = \"opening\";\n this.doOpen();\n return this;\n }\n /**\n * Closes the transport.\n */\n close() {\n if (this.readyState === \"opening\" || this.readyState === \"open\") {\n this.doClose();\n this.onClose();\n }\n return this;\n }\n /**\n * Sends multiple packets.\n *\n * @param {Array} packets\n */\n send(packets) {\n if (this.readyState === \"open\") {\n this.write(packets);\n }\n else {\n // this might happen if the transport was silently closed in the beforeunload event handler\n }\n }\n /**\n * Called upon open\n *\n * @protected\n */\n onOpen() {\n this.readyState = \"open\";\n this.writable = true;\n super.emitReserved(\"open\");\n }\n /**\n * Called with data.\n *\n * @param {String} data\n * @protected\n */\n onData(data) {\n const packet = decodePacket(data, this.socket.binaryType);\n this.onPacket(packet);\n }\n /**\n * Called with a decoded packet.\n *\n * @protected\n */\n onPacket(packet) {\n super.emitReserved(\"packet\", packet);\n }\n /**\n * Called upon close.\n *\n * @protected\n */\n onClose(details) {\n this.readyState = \"closed\";\n super.emitReserved(\"close\", details);\n }\n /**\n * Pauses the transport, in order not to lose packets during an upgrade.\n *\n * @param onPause\n */\n pause(onPause) { }\n createUri(schema, query = {}) {\n return (schema +\n \"://\" +\n this._hostname() +\n this._port() +\n this.opts.path +\n this._query(query));\n }\n _hostname() {\n const hostname = this.opts.hostname;\n return hostname.indexOf(\":\") === -1 ? hostname : \"[\" + hostname + \"]\";\n }\n _port() {\n if (this.opts.port &&\n ((this.opts.secure && Number(this.opts.port !== 443)) ||\n (!this.opts.secure && Number(this.opts.port) !== 80))) {\n return \":\" + this.opts.port;\n }\n else {\n return \"\";\n }\n }\n _query(query) {\n const encodedQuery = encode(query);\n return encodedQuery.length ? \"?\" + encodedQuery : \"\";\n }\n}\n","// imported from https://github.com/unshiftio/yeast\n'use strict';\nconst alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split(''), length = 64, map = {};\nlet seed = 0, i = 0, prev;\n/**\n * Return a string representing the specified number.\n *\n * @param {Number} num The number to convert.\n * @returns {String} The string representation of the number.\n * @api public\n */\nexport function encode(num) {\n let encoded = '';\n do {\n encoded = alphabet[num % length] + encoded;\n num = Math.floor(num / length);\n } while (num > 0);\n return encoded;\n}\n/**\n * Return the integer value specified by the given string.\n *\n * @param {String} str The string to convert.\n * @returns {Number} The integer value represented by the string.\n * @api public\n */\nexport function decode(str) {\n let decoded = 0;\n for (i = 0; i < str.length; i++) {\n decoded = decoded * length + map[str.charAt(i)];\n }\n return decoded;\n}\n/**\n * Yeast: A tiny growing id generator.\n *\n * @returns {String} A unique id.\n * @api public\n */\nexport function yeast() {\n const now = encode(+new Date());\n if (now !== prev)\n return seed = 0, prev = now;\n return now + '.' + encode(seed++);\n}\n//\n// Map each character to its index.\n//\nfor (; i < length; i++)\n map[alphabet[i]] = i;\n","// imported from https://github.com/galkn/querystring\n/**\n * Compiles a querystring\n * Returns string representation of the object\n *\n * @param {Object}\n * @api private\n */\nexport function encode(obj) {\n let str = '';\n for (let i in obj) {\n if (obj.hasOwnProperty(i)) {\n if (str.length)\n str += '&';\n str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);\n }\n }\n return str;\n}\n/**\n * Parses a simple querystring into an object\n *\n * @param {String} qs\n * @api private\n */\nexport function decode(qs) {\n let qry = {};\n let pairs = qs.split('&');\n for (let i = 0, l = pairs.length; i < l; i++) {\n let pair = pairs[i].split('=');\n qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);\n }\n return qry;\n}\n","// imported from https://github.com/component/has-cors\nlet value = false;\ntry {\n value = typeof XMLHttpRequest !== 'undefined' &&\n 'withCredentials' in new XMLHttpRequest();\n}\ncatch (err) {\n // if XMLHttp support is disabled in IE then it will throw\n // when trying to create\n}\nexport const hasCORS = value;\n","// browser shim for xmlhttprequest module\nimport { hasCORS } from \"../contrib/has-cors.js\";\nimport { globalThisShim as globalThis } from \"../globalThis.js\";\nexport function XHR(opts) {\n const xdomain = opts.xdomain;\n // XMLHttpRequest can be disabled on IE\n try {\n if (\"undefined\" !== typeof XMLHttpRequest && (!xdomain || hasCORS)) {\n return new XMLHttpRequest();\n }\n }\n catch (e) { }\n if (!xdomain) {\n try {\n return new globalThis[[\"Active\"].concat(\"Object\").join(\"X\")](\"Microsoft.XMLHTTP\");\n }\n catch (e) { }\n }\n}\nexport function createCookieJar() { }\n","import { Transport } from \"../transport.js\";\nimport { yeast } from \"../contrib/yeast.js\";\nimport { encodePayload, decodePayload } from \"engine.io-parser\";\nimport { createCookieJar, XHR as XMLHttpRequest, } from \"./xmlhttprequest.js\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { installTimerFunctions, pick } from \"../util.js\";\nimport { globalThisShim as globalThis } from \"../globalThis.js\";\nfunction empty() { }\nconst hasXHR2 = (function () {\n const xhr = new XMLHttpRequest({\n xdomain: false,\n });\n return null != xhr.responseType;\n})();\nexport class Polling extends Transport {\n /**\n * XHR Polling constructor.\n *\n * @param {Object} opts\n * @package\n */\n constructor(opts) {\n super(opts);\n this.polling = false;\n if (typeof location !== \"undefined\") {\n const isSSL = \"https:\" === location.protocol;\n let port = location.port;\n // some user agents have empty `location.port`\n if (!port) {\n port = isSSL ? \"443\" : \"80\";\n }\n this.xd =\n (typeof location !== \"undefined\" &&\n opts.hostname !== location.hostname) ||\n port !== opts.port;\n }\n /**\n * XHR supports binary\n */\n const forceBase64 = opts && opts.forceBase64;\n this.supportsBinary = hasXHR2 && !forceBase64;\n if (this.opts.withCredentials) {\n this.cookieJar = createCookieJar();\n }\n }\n get name() {\n return \"polling\";\n }\n /**\n * Opens the socket (triggers polling). We write a PING message to determine\n * when the transport is open.\n *\n * @protected\n */\n doOpen() {\n this.poll();\n }\n /**\n * Pauses polling.\n *\n * @param {Function} onPause - callback upon buffers are flushed and transport is paused\n * @package\n */\n pause(onPause) {\n this.readyState = \"pausing\";\n const pause = () => {\n this.readyState = \"paused\";\n onPause();\n };\n if (this.polling || !this.writable) {\n let total = 0;\n if (this.polling) {\n total++;\n this.once(\"pollComplete\", function () {\n --total || pause();\n });\n }\n if (!this.writable) {\n total++;\n this.once(\"drain\", function () {\n --total || pause();\n });\n }\n }\n else {\n pause();\n }\n }\n /**\n * Starts polling cycle.\n *\n * @private\n */\n poll() {\n this.polling = true;\n this.doPoll();\n this.emitReserved(\"poll\");\n }\n /**\n * Overloads onData to detect payloads.\n *\n * @protected\n */\n onData(data) {\n const callback = (packet) => {\n // if its the first message we consider the transport open\n if (\"opening\" === this.readyState && packet.type === \"open\") {\n this.onOpen();\n }\n // if its a close packet, we close the ongoing requests\n if (\"close\" === packet.type) {\n this.onClose({ description: \"transport closed by the server\" });\n return false;\n }\n // otherwise bypass onData and handle the message\n this.onPacket(packet);\n };\n // decode payload\n decodePayload(data, this.socket.binaryType).forEach(callback);\n // if an event did not trigger closing\n if (\"closed\" !== this.readyState) {\n // if we got data we're not polling\n this.polling = false;\n this.emitReserved(\"pollComplete\");\n if (\"open\" === this.readyState) {\n this.poll();\n }\n else {\n }\n }\n }\n /**\n * For polling, send a close packet.\n *\n * @protected\n */\n doClose() {\n const close = () => {\n this.write([{ type: \"close\" }]);\n };\n if (\"open\" === this.readyState) {\n close();\n }\n else {\n // in case we're trying to close while\n // handshaking is in progress (GH-164)\n this.once(\"open\", close);\n }\n }\n /**\n * Writes a packets payload.\n *\n * @param {Array} packets - data packets\n * @protected\n */\n write(packets) {\n this.writable = false;\n encodePayload(packets, (data) => {\n this.doWrite(data, () => {\n this.writable = true;\n this.emitReserved(\"drain\");\n });\n });\n }\n /**\n * Generates uri for connection.\n *\n * @private\n */\n uri() {\n const schema = this.opts.secure ? \"https\" : \"http\";\n const query = this.query || {};\n // cache busting is forced\n if (false !== this.opts.timestampRequests) {\n query[this.opts.timestampParam] = yeast();\n }\n if (!this.supportsBinary && !query.sid) {\n query.b64 = 1;\n }\n return this.createUri(schema, query);\n }\n /**\n * Creates a request.\n *\n * @param {String} method\n * @private\n */\n request(opts = {}) {\n Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);\n return new Request(this.uri(), opts);\n }\n /**\n * Sends data.\n *\n * @param {String} data to send.\n * @param {Function} called upon flush.\n * @private\n */\n doWrite(data, fn) {\n const req = this.request({\n method: \"POST\",\n data: data,\n });\n req.on(\"success\", fn);\n req.on(\"error\", (xhrStatus, context) => {\n this.onError(\"xhr post error\", xhrStatus, context);\n });\n }\n /**\n * Starts a poll cycle.\n *\n * @private\n */\n doPoll() {\n const req = this.request();\n req.on(\"data\", this.onData.bind(this));\n req.on(\"error\", (xhrStatus, context) => {\n this.onError(\"xhr poll error\", xhrStatus, context);\n });\n this.pollXhr = req;\n }\n}\nexport class Request extends Emitter {\n /**\n * Request constructor\n *\n * @param {Object} options\n * @package\n */\n constructor(uri, opts) {\n super();\n installTimerFunctions(this, opts);\n this.opts = opts;\n this.method = opts.method || \"GET\";\n this.uri = uri;\n this.data = undefined !== opts.data ? opts.data : null;\n this.create();\n }\n /**\n * Creates the XHR object and sends the request.\n *\n * @private\n */\n create() {\n var _a;\n const opts = pick(this.opts, \"agent\", \"pfx\", \"key\", \"passphrase\", \"cert\", \"ca\", \"ciphers\", \"rejectUnauthorized\", \"autoUnref\");\n opts.xdomain = !!this.opts.xd;\n const xhr = (this.xhr = new XMLHttpRequest(opts));\n try {\n xhr.open(this.method, this.uri, true);\n try {\n if (this.opts.extraHeaders) {\n xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);\n for (let i in this.opts.extraHeaders) {\n if (this.opts.extraHeaders.hasOwnProperty(i)) {\n xhr.setRequestHeader(i, this.opts.extraHeaders[i]);\n }\n }\n }\n }\n catch (e) { }\n if (\"POST\" === this.method) {\n try {\n xhr.setRequestHeader(\"Content-type\", \"text/plain;charset=UTF-8\");\n }\n catch (e) { }\n }\n try {\n xhr.setRequestHeader(\"Accept\", \"*/*\");\n }\n catch (e) { }\n (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.addCookies(xhr);\n // ie6 check\n if (\"withCredentials\" in xhr) {\n xhr.withCredentials = this.opts.withCredentials;\n }\n if (this.opts.requestTimeout) {\n xhr.timeout = this.opts.requestTimeout;\n }\n xhr.onreadystatechange = () => {\n var _a;\n if (xhr.readyState === 3) {\n (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.parseCookies(xhr);\n }\n if (4 !== xhr.readyState)\n return;\n if (200 === xhr.status || 1223 === xhr.status) {\n this.onLoad();\n }\n else {\n // make sure the `error` event handler that's user-set\n // does not throw in the same tick and gets caught here\n this.setTimeoutFn(() => {\n this.onError(typeof xhr.status === \"number\" ? xhr.status : 0);\n }, 0);\n }\n };\n xhr.send(this.data);\n }\n catch (e) {\n // Need to defer since .create() is called directly from the constructor\n // and thus the 'error' event can only be only bound *after* this exception\n // occurs. Therefore, also, we cannot throw here at all.\n this.setTimeoutFn(() => {\n this.onError(e);\n }, 0);\n return;\n }\n if (typeof document !== \"undefined\") {\n this.index = Request.requestsCount++;\n Request.requests[this.index] = this;\n }\n }\n /**\n * Called upon error.\n *\n * @private\n */\n onError(err) {\n this.emitReserved(\"error\", err, this.xhr);\n this.cleanup(true);\n }\n /**\n * Cleans up house.\n *\n * @private\n */\n cleanup(fromError) {\n if (\"undefined\" === typeof this.xhr || null === this.xhr) {\n return;\n }\n this.xhr.onreadystatechange = empty;\n if (fromError) {\n try {\n this.xhr.abort();\n }\n catch (e) { }\n }\n if (typeof document !== \"undefined\") {\n delete Request.requests[this.index];\n }\n this.xhr = null;\n }\n /**\n * Called upon load.\n *\n * @private\n */\n onLoad() {\n const data = this.xhr.responseText;\n if (data !== null) {\n this.emitReserved(\"data\", data);\n this.emitReserved(\"success\");\n this.cleanup();\n }\n }\n /**\n * Aborts the request.\n *\n * @package\n */\n abort() {\n this.cleanup();\n }\n}\nRequest.requestsCount = 0;\nRequest.requests = {};\n/**\n * Aborts pending requests when unloading the window. This is needed to prevent\n * memory leaks (e.g. when using IE) and to ensure that no spurious error is\n * emitted.\n */\nif (typeof document !== \"undefined\") {\n // @ts-ignore\n if (typeof attachEvent === \"function\") {\n // @ts-ignore\n attachEvent(\"onunload\", unloadHandler);\n }\n else if (typeof addEventListener === \"function\") {\n const terminationEvent = \"onpagehide\" in globalThis ? \"pagehide\" : \"unload\";\n addEventListener(terminationEvent, unloadHandler, false);\n }\n}\nfunction unloadHandler() {\n for (let i in Request.requests) {\n if (Request.requests.hasOwnProperty(i)) {\n Request.requests[i].abort();\n }\n }\n}\n","import { globalThisShim as globalThis } from \"../globalThis.js\";\nexport const nextTick = (() => {\n const isPromiseAvailable = typeof Promise === \"function\" && typeof Promise.resolve === \"function\";\n if (isPromiseAvailable) {\n return (cb) => Promise.resolve().then(cb);\n }\n else {\n return (cb, setTimeoutFn) => setTimeoutFn(cb, 0);\n }\n})();\nexport const WebSocket = globalThis.WebSocket || globalThis.MozWebSocket;\nexport const usingBrowserWebSocket = true;\nexport const defaultBinaryType = \"arraybuffer\";\n","import { Transport } from \"../transport.js\";\nimport { yeast } from \"../contrib/yeast.js\";\nimport { pick } from \"../util.js\";\nimport { nextTick, usingBrowserWebSocket, WebSocket, } from \"./websocket-constructor.js\";\nimport { encodePacket } from \"engine.io-parser\";\n// detect ReactNative environment\nconst isReactNative = typeof navigator !== \"undefined\" &&\n typeof navigator.product === \"string\" &&\n navigator.product.toLowerCase() === \"reactnative\";\nexport class WS extends Transport {\n /**\n * WebSocket transport constructor.\n *\n * @param {Object} opts - connection options\n * @protected\n */\n constructor(opts) {\n super(opts);\n this.supportsBinary = !opts.forceBase64;\n }\n get name() {\n return \"websocket\";\n }\n doOpen() {\n if (!this.check()) {\n // let probe timeout\n return;\n }\n const uri = this.uri();\n const protocols = this.opts.protocols;\n // React Native only supports the 'headers' option, and will print a warning if anything else is passed\n const opts = isReactNative\n ? {}\n : pick(this.opts, \"agent\", \"perMessageDeflate\", \"pfx\", \"key\", \"passphrase\", \"cert\", \"ca\", \"ciphers\", \"rejectUnauthorized\", \"localAddress\", \"protocolVersion\", \"origin\", \"maxPayload\", \"family\", \"checkServerIdentity\");\n if (this.opts.extraHeaders) {\n opts.headers = this.opts.extraHeaders;\n }\n try {\n this.ws =\n usingBrowserWebSocket && !isReactNative\n ? protocols\n ? new WebSocket(uri, protocols)\n : new WebSocket(uri)\n : new WebSocket(uri, protocols, opts);\n }\n catch (err) {\n return this.emitReserved(\"error\", err);\n }\n this.ws.binaryType = this.socket.binaryType;\n this.addEventListeners();\n }\n /**\n * Adds event listeners to the socket\n *\n * @private\n */\n addEventListeners() {\n this.ws.onopen = () => {\n if (this.opts.autoUnref) {\n this.ws._socket.unref();\n }\n this.onOpen();\n };\n this.ws.onclose = (closeEvent) => this.onClose({\n description: \"websocket connection closed\",\n context: closeEvent,\n });\n this.ws.onmessage = (ev) => this.onData(ev.data);\n this.ws.onerror = (e) => this.onError(\"websocket error\", e);\n }\n write(packets) {\n this.writable = false;\n // encodePacket efficient as it uses WS framing\n // no need for encodePayload\n for (let i = 0; i < packets.length; i++) {\n const packet = packets[i];\n const lastPacket = i === packets.length - 1;\n encodePacket(packet, this.supportsBinary, (data) => {\n // always create a new object (GH-437)\n const opts = {};\n if (!usingBrowserWebSocket) {\n if (packet.options) {\n opts.compress = packet.options.compress;\n }\n if (this.opts.perMessageDeflate) {\n const len = \n // @ts-ignore\n \"string\" === typeof data ? Buffer.byteLength(data) : data.length;\n if (len < this.opts.perMessageDeflate.threshold) {\n opts.compress = false;\n }\n }\n }\n // Sometimes the websocket has already been closed but the browser didn't\n // have a chance of informing us about it yet, in that case send will\n // throw an error\n try {\n if (usingBrowserWebSocket) {\n // TypeError is thrown when passing the second argument on Safari\n this.ws.send(data);\n }\n else {\n this.ws.send(data, opts);\n }\n }\n catch (e) {\n }\n if (lastPacket) {\n // fake drain\n // defer to next tick to allow Socket to clear writeBuffer\n nextTick(() => {\n this.writable = true;\n this.emitReserved(\"drain\");\n }, this.setTimeoutFn);\n }\n });\n }\n }\n doClose() {\n if (typeof this.ws !== \"undefined\") {\n this.ws.close();\n this.ws = null;\n }\n }\n /**\n * Generates uri for connection.\n *\n * @private\n */\n uri() {\n const schema = this.opts.secure ? \"wss\" : \"ws\";\n const query = this.query || {};\n // append timestamp to URI\n if (this.opts.timestampRequests) {\n query[this.opts.timestampParam] = yeast();\n }\n // communicate binary support capabilities\n if (!this.supportsBinary) {\n query.b64 = 1;\n }\n return this.createUri(schema, query);\n }\n /**\n * Feature detection for WebSocket.\n *\n * @return {Boolean} whether this transport is available.\n * @private\n */\n check() {\n return !!WebSocket;\n }\n}\n","import { Transport } from \"../transport.js\";\nimport { nextTick } from \"./websocket-constructor.js\";\nimport { createPacketDecoderStream, createPacketEncoderStream, } from \"engine.io-parser\";\nexport class WT extends Transport {\n get name() {\n return \"webtransport\";\n }\n doOpen() {\n // @ts-ignore\n if (typeof WebTransport !== \"function\") {\n return;\n }\n // @ts-ignore\n this.transport = new WebTransport(this.createUri(\"https\"), this.opts.transportOptions[this.name]);\n this.transport.closed\n .then(() => {\n this.onClose();\n })\n .catch((err) => {\n this.onError(\"webtransport error\", err);\n });\n // note: we could have used async/await, but that would require some additional polyfills\n this.transport.ready.then(() => {\n this.transport.createBidirectionalStream().then((stream) => {\n const decoderStream = createPacketDecoderStream(Number.MAX_SAFE_INTEGER, this.socket.binaryType);\n const reader = stream.readable.pipeThrough(decoderStream).getReader();\n const encoderStream = createPacketEncoderStream();\n encoderStream.readable.pipeTo(stream.writable);\n this.writer = encoderStream.writable.getWriter();\n const read = () => {\n reader\n .read()\n .then(({ done, value }) => {\n if (done) {\n return;\n }\n this.onPacket(value);\n read();\n })\n .catch((err) => {\n });\n };\n read();\n const packet = { type: \"open\" };\n if (this.query.sid) {\n packet.data = `{\"sid\":\"${this.query.sid}\"}`;\n }\n this.writer.write(packet).then(() => this.onOpen());\n });\n });\n }\n write(packets) {\n this.writable = false;\n for (let i = 0; i < packets.length; i++) {\n const packet = packets[i];\n const lastPacket = i === packets.length - 1;\n this.writer.write(packet).then(() => {\n if (lastPacket) {\n nextTick(() => {\n this.writable = true;\n this.emitReserved(\"drain\");\n }, this.setTimeoutFn);\n }\n });\n }\n }\n doClose() {\n var _a;\n (_a = this.transport) === null || _a === void 0 ? void 0 : _a.close();\n }\n}\n","import { Polling } from \"./polling.js\";\nimport { WS } from \"./websocket.js\";\nimport { WT } from \"./webtransport.js\";\nexport const transports = {\n websocket: WS,\n webtransport: WT,\n polling: Polling,\n};\n","// imported from https://github.com/galkn/parseuri\n/**\n * Parses a URI\n *\n * Note: we could also have used the built-in URL object, but it isn't supported on all platforms.\n *\n * See:\n * - https://developer.mozilla.org/en-US/docs/Web/API/URL\n * - https://caniuse.com/url\n * - https://www.rfc-editor.org/rfc/rfc3986#appendix-B\n *\n * History of the parse() method:\n * - first commit: https://github.com/socketio/socket.io-client/commit/4ee1d5d94b3906a9c052b459f1a818b15f38f91c\n * - export into its own module: https://github.com/socketio/engine.io-client/commit/de2c561e4564efeb78f1bdb1ba39ef81b2822cb3\n * - reimport: https://github.com/socketio/engine.io-client/commit/df32277c3f6d622eec5ed09f493cae3f3391d242\n *\n * @author Steven Levithan (MIT license)\n * @api private\n */\nconst re = /^(?:(?![^:@\\/?#]+:[^:@\\/]*@)(http|https|ws|wss):\\/\\/)?((?:(([^:@\\/?#]*)(?::([^:@\\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\\/?#]*)(?::(\\d*))?)(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))(?:\\?([^#]*))?(?:#(.*))?)/;\nconst parts = [\n 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'\n];\nexport function parse(str) {\n const src = str, b = str.indexOf('['), e = str.indexOf(']');\n if (b != -1 && e != -1) {\n str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);\n }\n let m = re.exec(str || ''), uri = {}, i = 14;\n while (i--) {\n uri[parts[i]] = m[i] || '';\n }\n if (b != -1 && e != -1) {\n uri.source = src;\n uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');\n uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');\n uri.ipv6uri = true;\n }\n uri.pathNames = pathNames(uri, uri['path']);\n uri.queryKey = queryKey(uri, uri['query']);\n return uri;\n}\nfunction pathNames(obj, path) {\n const regx = /\\/{2,9}/g, names = path.replace(regx, \"/\").split(\"/\");\n if (path.slice(0, 1) == '/' || path.length === 0) {\n names.splice(0, 1);\n }\n if (path.slice(-1) == '/') {\n names.splice(names.length - 1, 1);\n }\n return names;\n}\nfunction queryKey(uri, query) {\n const data = {};\n query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {\n if ($1) {\n data[$1] = $2;\n }\n });\n return data;\n}\n","import { transports } from \"./transports/index.js\";\nimport { installTimerFunctions, byteLength } from \"./util.js\";\nimport { decode } from \"./contrib/parseqs.js\";\nimport { parse } from \"./contrib/parseuri.js\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { protocol } from \"engine.io-parser\";\nimport { defaultBinaryType } from \"./transports/websocket-constructor.js\";\nexport class Socket extends Emitter {\n /**\n * Socket constructor.\n *\n * @param {String|Object} uri - uri or options\n * @param {Object} opts - options\n */\n constructor(uri, opts = {}) {\n super();\n this.binaryType = defaultBinaryType;\n this.writeBuffer = [];\n if (uri && \"object\" === typeof uri) {\n opts = uri;\n uri = null;\n }\n if (uri) {\n uri = parse(uri);\n opts.hostname = uri.host;\n opts.secure = uri.protocol === \"https\" || uri.protocol === \"wss\";\n opts.port = uri.port;\n if (uri.query)\n opts.query = uri.query;\n }\n else if (opts.host) {\n opts.hostname = parse(opts.host).host;\n }\n installTimerFunctions(this, opts);\n this.secure =\n null != opts.secure\n ? opts.secure\n : typeof location !== \"undefined\" && \"https:\" === location.protocol;\n if (opts.hostname && !opts.port) {\n // if no port is specified manually, use the protocol default\n opts.port = this.secure ? \"443\" : \"80\";\n }\n this.hostname =\n opts.hostname ||\n (typeof location !== \"undefined\" ? location.hostname : \"localhost\");\n this.port =\n opts.port ||\n (typeof location !== \"undefined\" && location.port\n ? location.port\n : this.secure\n ? \"443\"\n : \"80\");\n this.transports = opts.transports || [\n \"polling\",\n \"websocket\",\n \"webtransport\",\n ];\n this.writeBuffer = [];\n this.prevBufferLen = 0;\n this.opts = Object.assign({\n path: \"/engine.io\",\n agent: false,\n withCredentials: false,\n upgrade: true,\n timestampParam: \"t\",\n rememberUpgrade: false,\n addTrailingSlash: true,\n rejectUnauthorized: true,\n perMessageDeflate: {\n threshold: 1024,\n },\n transportOptions: {},\n closeOnBeforeunload: false,\n }, opts);\n this.opts.path =\n this.opts.path.replace(/\\/$/, \"\") +\n (this.opts.addTrailingSlash ? \"/\" : \"\");\n if (typeof this.opts.query === \"string\") {\n this.opts.query = decode(this.opts.query);\n }\n // set on handshake\n this.id = null;\n this.upgrades = null;\n this.pingInterval = null;\n this.pingTimeout = null;\n // set on heartbeat\n this.pingTimeoutTimer = null;\n if (typeof addEventListener === \"function\") {\n if (this.opts.closeOnBeforeunload) {\n // Firefox closes the connection when the \"beforeunload\" event is emitted but not Chrome. This event listener\n // ensures every browser behaves the same (no \"disconnect\" event at the Socket.IO level when the page is\n // closed/reloaded)\n this.beforeunloadEventListener = () => {\n if (this.transport) {\n // silently close the transport\n this.transport.removeAllListeners();\n this.transport.close();\n }\n };\n addEventListener(\"beforeunload\", this.beforeunloadEventListener, false);\n }\n if (this.hostname !== \"localhost\") {\n this.offlineEventListener = () => {\n this.onClose(\"transport close\", {\n description: \"network connection lost\",\n });\n };\n addEventListener(\"offline\", this.offlineEventListener, false);\n }\n }\n this.open();\n }\n /**\n * Creates transport of the given type.\n *\n * @param {String} name - transport name\n * @return {Transport}\n * @private\n */\n createTransport(name) {\n const query = Object.assign({}, this.opts.query);\n // append engine.io protocol identifier\n query.EIO = protocol;\n // transport name\n query.transport = name;\n // session id if we already have one\n if (this.id)\n query.sid = this.id;\n const opts = Object.assign({}, this.opts, {\n query,\n socket: this,\n hostname: this.hostname,\n secure: this.secure,\n port: this.port,\n }, this.opts.transportOptions[name]);\n return new transports[name](opts);\n }\n /**\n * Initializes transport to use and starts probe.\n *\n * @private\n */\n open() {\n let transport;\n if (this.opts.rememberUpgrade &&\n Socket.priorWebsocketSuccess &&\n this.transports.indexOf(\"websocket\") !== -1) {\n transport = \"websocket\";\n }\n else if (0 === this.transports.length) {\n // Emit error on next tick so it can be listened to\n this.setTimeoutFn(() => {\n this.emitReserved(\"error\", \"No transports available\");\n }, 0);\n return;\n }\n else {\n transport = this.transports[0];\n }\n this.readyState = \"opening\";\n // Retry with the next transport if the transport is disabled (jsonp: false)\n try {\n transport = this.createTransport(transport);\n }\n catch (e) {\n this.transports.shift();\n this.open();\n return;\n }\n transport.open();\n this.setTransport(transport);\n }\n /**\n * Sets the current transport. Disables the existing one (if any).\n *\n * @private\n */\n setTransport(transport) {\n if (this.transport) {\n this.transport.removeAllListeners();\n }\n // set up transport\n this.transport = transport;\n // set up transport listeners\n transport\n .on(\"drain\", this.onDrain.bind(this))\n .on(\"packet\", this.onPacket.bind(this))\n .on(\"error\", this.onError.bind(this))\n .on(\"close\", (reason) => this.onClose(\"transport close\", reason));\n }\n /**\n * Probes a transport.\n *\n * @param {String} name - transport name\n * @private\n */\n probe(name) {\n let transport = this.createTransport(name);\n let failed = false;\n Socket.priorWebsocketSuccess = false;\n const onTransportOpen = () => {\n if (failed)\n return;\n transport.send([{ type: \"ping\", data: \"probe\" }]);\n transport.once(\"packet\", (msg) => {\n if (failed)\n return;\n if (\"pong\" === msg.type && \"probe\" === msg.data) {\n this.upgrading = true;\n this.emitReserved(\"upgrading\", transport);\n if (!transport)\n return;\n Socket.priorWebsocketSuccess = \"websocket\" === transport.name;\n this.transport.pause(() => {\n if (failed)\n return;\n if (\"closed\" === this.readyState)\n return;\n cleanup();\n this.setTransport(transport);\n transport.send([{ type: \"upgrade\" }]);\n this.emitReserved(\"upgrade\", transport);\n transport = null;\n this.upgrading = false;\n this.flush();\n });\n }\n else {\n const err = new Error(\"probe error\");\n // @ts-ignore\n err.transport = transport.name;\n this.emitReserved(\"upgradeError\", err);\n }\n });\n };\n function freezeTransport() {\n if (failed)\n return;\n // Any callback called by transport should be ignored since now\n failed = true;\n cleanup();\n transport.close();\n transport = null;\n }\n // Handle any error that happens while probing\n const onerror = (err) => {\n const error = new Error(\"probe error: \" + err);\n // @ts-ignore\n error.transport = transport.name;\n freezeTransport();\n this.emitReserved(\"upgradeError\", error);\n };\n function onTransportClose() {\n onerror(\"transport closed\");\n }\n // When the socket is closed while we're probing\n function onclose() {\n onerror(\"socket closed\");\n }\n // When the socket is upgraded while we're probing\n function onupgrade(to) {\n if (transport && to.name !== transport.name) {\n freezeTransport();\n }\n }\n // Remove all listeners on the transport and on self\n const cleanup = () => {\n transport.removeListener(\"open\", onTransportOpen);\n transport.removeListener(\"error\", onerror);\n transport.removeListener(\"close\", onTransportClose);\n this.off(\"close\", onclose);\n this.off(\"upgrading\", onupgrade);\n };\n transport.once(\"open\", onTransportOpen);\n transport.once(\"error\", onerror);\n transport.once(\"close\", onTransportClose);\n this.once(\"close\", onclose);\n this.once(\"upgrading\", onupgrade);\n if (this.upgrades.indexOf(\"webtransport\") !== -1 &&\n name !== \"webtransport\") {\n // favor WebTransport\n this.setTimeoutFn(() => {\n if (!failed) {\n transport.open();\n }\n }, 200);\n }\n else {\n transport.open();\n }\n }\n /**\n * Called when connection is deemed open.\n *\n * @private\n */\n onOpen() {\n this.readyState = \"open\";\n Socket.priorWebsocketSuccess = \"websocket\" === this.transport.name;\n this.emitReserved(\"open\");\n this.flush();\n // we check for `readyState` in case an `open`\n // listener already closed the socket\n if (\"open\" === this.readyState && this.opts.upgrade) {\n let i = 0;\n const l = this.upgrades.length;\n for (; i < l; i++) {\n this.probe(this.upgrades[i]);\n }\n }\n }\n /**\n * Handles a packet.\n *\n * @private\n */\n onPacket(packet) {\n if (\"opening\" === this.readyState ||\n \"open\" === this.readyState ||\n \"closing\" === this.readyState) {\n this.emitReserved(\"packet\", packet);\n // Socket is live - any packet counts\n this.emitReserved(\"heartbeat\");\n this.resetPingTimeout();\n switch (packet.type) {\n case \"open\":\n this.onHandshake(JSON.parse(packet.data));\n break;\n case \"ping\":\n this.sendPacket(\"pong\");\n this.emitReserved(\"ping\");\n this.emitReserved(\"pong\");\n break;\n case \"error\":\n const err = new Error(\"server error\");\n // @ts-ignore\n err.code = packet.data;\n this.onError(err);\n break;\n case \"message\":\n this.emitReserved(\"data\", packet.data);\n this.emitReserved(\"message\", packet.data);\n break;\n }\n }\n else {\n }\n }\n /**\n * Called upon handshake completion.\n *\n * @param {Object} data - handshake obj\n * @private\n */\n onHandshake(data) {\n this.emitReserved(\"handshake\", data);\n this.id = data.sid;\n this.transport.query.sid = data.sid;\n this.upgrades = this.filterUpgrades(data.upgrades);\n this.pingInterval = data.pingInterval;\n this.pingTimeout = data.pingTimeout;\n this.maxPayload = data.maxPayload;\n this.onOpen();\n // In case open handler closes socket\n if (\"closed\" === this.readyState)\n return;\n this.resetPingTimeout();\n }\n /**\n * Sets and resets ping timeout timer based on server pings.\n *\n * @private\n */\n resetPingTimeout() {\n this.clearTimeoutFn(this.pingTimeoutTimer);\n this.pingTimeoutTimer = this.setTimeoutFn(() => {\n this.onClose(\"ping timeout\");\n }, this.pingInterval + this.pingTimeout);\n if (this.opts.autoUnref) {\n this.pingTimeoutTimer.unref();\n }\n }\n /**\n * Called on `drain` event\n *\n * @private\n */\n onDrain() {\n this.writeBuffer.splice(0, this.prevBufferLen);\n // setting prevBufferLen = 0 is very important\n // for example, when upgrading, upgrade packet is sent over,\n // and a nonzero prevBufferLen could cause problems on `drain`\n this.prevBufferLen = 0;\n if (0 === this.writeBuffer.length) {\n this.emitReserved(\"drain\");\n }\n else {\n this.flush();\n }\n }\n /**\n * Flush write buffers.\n *\n * @private\n */\n flush() {\n if (\"closed\" !== this.readyState &&\n this.transport.writable &&\n !this.upgrading &&\n this.writeBuffer.length) {\n const packets = this.getWritablePackets();\n this.transport.send(packets);\n // keep track of current length of writeBuffer\n // splice writeBuffer and callbackBuffer on `drain`\n this.prevBufferLen = packets.length;\n this.emitReserved(\"flush\");\n }\n }\n /**\n * Ensure the encoded size of the writeBuffer is below the maxPayload value sent by the server (only for HTTP\n * long-polling)\n *\n * @private\n */\n getWritablePackets() {\n const shouldCheckPayloadSize = this.maxPayload &&\n this.transport.name === \"polling\" &&\n this.writeBuffer.length > 1;\n if (!shouldCheckPayloadSize) {\n return this.writeBuffer;\n }\n let payloadSize = 1; // first packet type\n for (let i = 0; i < this.writeBuffer.length; i++) {\n const data = this.writeBuffer[i].data;\n if (data) {\n payloadSize += byteLength(data);\n }\n if (i > 0 && payloadSize > this.maxPayload) {\n return this.writeBuffer.slice(0, i);\n }\n payloadSize += 2; // separator + packet type\n }\n return this.writeBuffer;\n }\n /**\n * Sends a message.\n *\n * @param {String} msg - message.\n * @param {Object} options.\n * @param {Function} callback function.\n * @return {Socket} for chaining.\n */\n write(msg, options, fn) {\n this.sendPacket(\"message\", msg, options, fn);\n return this;\n }\n send(msg, options, fn) {\n this.sendPacket(\"message\", msg, options, fn);\n return this;\n }\n /**\n * Sends a packet.\n *\n * @param {String} type: packet type.\n * @param {String} data.\n * @param {Object} options.\n * @param {Function} fn - callback function.\n * @private\n */\n sendPacket(type, data, options, fn) {\n if (\"function\" === typeof data) {\n fn = data;\n data = undefined;\n }\n if (\"function\" === typeof options) {\n fn = options;\n options = null;\n }\n if (\"closing\" === this.readyState || \"closed\" === this.readyState) {\n return;\n }\n options = options || {};\n options.compress = false !== options.compress;\n const packet = {\n type: type,\n data: data,\n options: options,\n };\n this.emitReserved(\"packetCreate\", packet);\n this.writeBuffer.push(packet);\n if (fn)\n this.once(\"flush\", fn);\n this.flush();\n }\n /**\n * Closes the connection.\n */\n close() {\n const close = () => {\n this.onClose(\"forced close\");\n this.transport.close();\n };\n const cleanupAndClose = () => {\n this.off(\"upgrade\", cleanupAndClose);\n this.off(\"upgradeError\", cleanupAndClose);\n close();\n };\n const waitForUpgrade = () => {\n // wait for upgrade to finish since we can't send packets while pausing a transport\n this.once(\"upgrade\", cleanupAndClose);\n this.once(\"upgradeError\", cleanupAndClose);\n };\n if (\"opening\" === this.readyState || \"open\" === this.readyState) {\n this.readyState = \"closing\";\n if (this.writeBuffer.length) {\n this.once(\"drain\", () => {\n if (this.upgrading) {\n waitForUpgrade();\n }\n else {\n close();\n }\n });\n }\n else if (this.upgrading) {\n waitForUpgrade();\n }\n else {\n close();\n }\n }\n return this;\n }\n /**\n * Called upon transport error\n *\n * @private\n */\n onError(err) {\n Socket.priorWebsocketSuccess = false;\n this.emitReserved(\"error\", err);\n this.onClose(\"transport error\", err);\n }\n /**\n * Called upon transport close.\n *\n * @private\n */\n onClose(reason, description) {\n if (\"opening\" === this.readyState ||\n \"open\" === this.readyState ||\n \"closing\" === this.readyState) {\n // clear timers\n this.clearTimeoutFn(this.pingTimeoutTimer);\n // stop event from firing again for transport\n this.transport.removeAllListeners(\"close\");\n // ensure transport won't stay open\n this.transport.close();\n // ignore further transport communication\n this.transport.removeAllListeners();\n if (typeof removeEventListener === \"function\") {\n removeEventListener(\"beforeunload\", this.beforeunloadEventListener, false);\n removeEventListener(\"offline\", this.offlineEventListener, false);\n }\n // set ready state\n this.readyState = \"closed\";\n // clear session id\n this.id = null;\n // emit close event\n this.emitReserved(\"close\", reason, description);\n // clean buffers after, so users can still\n // grab the buffers on `close` event\n this.writeBuffer = [];\n this.prevBufferLen = 0;\n }\n }\n /**\n * Filters upgrades, returning only those matching client transports.\n *\n * @param {Array} upgrades - server upgrades\n * @private\n */\n filterUpgrades(upgrades) {\n const filteredUpgrades = [];\n let i = 0;\n const j = upgrades.length;\n for (; i < j; i++) {\n if (~this.transports.indexOf(upgrades[i]))\n filteredUpgrades.push(upgrades[i]);\n }\n return filteredUpgrades;\n }\n}\nSocket.protocol = protocol;\n","import { Socket } from \"./socket.js\";\nexport { Socket };\nexport const protocol = Socket.protocol;\nexport { Transport } from \"./transport.js\";\nexport { transports } from \"./transports/index.js\";\nexport { installTimerFunctions } from \"./util.js\";\nexport { parse } from \"./contrib/parseuri.js\";\nexport { nextTick } from \"./transports/websocket-constructor.js\";\n","const withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\nconst isView = (obj) => {\n return typeof ArrayBuffer.isView === \"function\"\n ? ArrayBuffer.isView(obj)\n : obj.buffer instanceof ArrayBuffer;\n};\nconst toString = Object.prototype.toString;\nconst withNativeBlob = typeof Blob === \"function\" ||\n (typeof Blob !== \"undefined\" &&\n toString.call(Blob) === \"[object BlobConstructor]\");\nconst withNativeFile = typeof File === \"function\" ||\n (typeof File !== \"undefined\" &&\n toString.call(File) === \"[object FileConstructor]\");\n/**\n * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.\n *\n * @private\n */\nexport function isBinary(obj) {\n return ((withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||\n (withNativeBlob && obj instanceof Blob) ||\n (withNativeFile && obj instanceof File));\n}\nexport function hasBinary(obj, toJSON) {\n if (!obj || typeof obj !== \"object\") {\n return false;\n }\n if (Array.isArray(obj)) {\n for (let i = 0, l = obj.length; i < l; i++) {\n if (hasBinary(obj[i])) {\n return true;\n }\n }\n return false;\n }\n if (isBinary(obj)) {\n return true;\n }\n if (obj.toJSON &&\n typeof obj.toJSON === \"function\" &&\n arguments.length === 1) {\n return hasBinary(obj.toJSON(), true);\n }\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {\n return true;\n }\n }\n return false;\n}\n","import { isBinary } from \"./is-binary.js\";\n/**\n * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.\n *\n * @param {Object} packet - socket.io event packet\n * @return {Object} with deconstructed packet and list of buffers\n * @public\n */\nexport function deconstructPacket(packet) {\n const buffers = [];\n const packetData = packet.data;\n const pack = packet;\n pack.data = _deconstructPacket(packetData, buffers);\n pack.attachments = buffers.length; // number of binary 'attachments'\n return { packet: pack, buffers: buffers };\n}\nfunction _deconstructPacket(data, buffers) {\n if (!data)\n return data;\n if (isBinary(data)) {\n const placeholder = { _placeholder: true, num: buffers.length };\n buffers.push(data);\n return placeholder;\n }\n else if (Array.isArray(data)) {\n const newData = new Array(data.length);\n for (let i = 0; i < data.length; i++) {\n newData[i] = _deconstructPacket(data[i], buffers);\n }\n return newData;\n }\n else if (typeof data === \"object\" && !(data instanceof Date)) {\n const newData = {};\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n newData[key] = _deconstructPacket(data[key], buffers);\n }\n }\n return newData;\n }\n return data;\n}\n/**\n * Reconstructs a binary packet from its placeholder packet and buffers\n *\n * @param {Object} packet - event packet with placeholders\n * @param {Array} buffers - binary buffers to put in placeholder positions\n * @return {Object} reconstructed packet\n * @public\n */\nexport function reconstructPacket(packet, buffers) {\n packet.data = _reconstructPacket(packet.data, buffers);\n delete packet.attachments; // no longer useful\n return packet;\n}\nfunction _reconstructPacket(data, buffers) {\n if (!data)\n return data;\n if (data && data._placeholder === true) {\n const isIndexValid = typeof data.num === \"number\" &&\n data.num >= 0 &&\n data.num < buffers.length;\n if (isIndexValid) {\n return buffers[data.num]; // appropriate buffer (should be natural order anyway)\n }\n else {\n throw new Error(\"illegal attachments\");\n }\n }\n else if (Array.isArray(data)) {\n for (let i = 0; i < data.length; i++) {\n data[i] = _reconstructPacket(data[i], buffers);\n }\n }\n else if (typeof data === \"object\") {\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n data[key] = _reconstructPacket(data[key], buffers);\n }\n }\n }\n return data;\n}\n","import { Emitter } from \"@socket.io/component-emitter\";\nimport { deconstructPacket, reconstructPacket } from \"./binary.js\";\nimport { isBinary, hasBinary } from \"./is-binary.js\";\n/**\n * These strings must not be used as event names, as they have a special meaning.\n */\nconst RESERVED_EVENTS = [\n \"connect\",\n \"connect_error\",\n \"disconnect\",\n \"disconnecting\",\n \"newListener\",\n \"removeListener\", // used by the Node.js EventEmitter\n];\n/**\n * Protocol version.\n *\n * @public\n */\nexport const protocol = 5;\nexport var PacketType;\n(function (PacketType) {\n PacketType[PacketType[\"CONNECT\"] = 0] = \"CONNECT\";\n PacketType[PacketType[\"DISCONNECT\"] = 1] = \"DISCONNECT\";\n PacketType[PacketType[\"EVENT\"] = 2] = \"EVENT\";\n PacketType[PacketType[\"ACK\"] = 3] = \"ACK\";\n PacketType[PacketType[\"CONNECT_ERROR\"] = 4] = \"CONNECT_ERROR\";\n PacketType[PacketType[\"BINARY_EVENT\"] = 5] = \"BINARY_EVENT\";\n PacketType[PacketType[\"BINARY_ACK\"] = 6] = \"BINARY_ACK\";\n})(PacketType || (PacketType = {}));\n/**\n * A socket.io Encoder instance\n */\nexport class Encoder {\n /**\n * Encoder constructor\n *\n * @param {function} replacer - custom replacer to pass down to JSON.parse\n */\n constructor(replacer) {\n this.replacer = replacer;\n }\n /**\n * Encode a packet as a single string if non-binary, or as a\n * buffer sequence, depending on packet type.\n *\n * @param {Object} obj - packet object\n */\n encode(obj) {\n if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {\n if (hasBinary(obj)) {\n return this.encodeAsBinary({\n type: obj.type === PacketType.EVENT\n ? PacketType.BINARY_EVENT\n : PacketType.BINARY_ACK,\n nsp: obj.nsp,\n data: obj.data,\n id: obj.id,\n });\n }\n }\n return [this.encodeAsString(obj)];\n }\n /**\n * Encode packet as string.\n */\n encodeAsString(obj) {\n // first is type\n let str = \"\" + obj.type;\n // attachments if we have them\n if (obj.type === PacketType.BINARY_EVENT ||\n obj.type === PacketType.BINARY_ACK) {\n str += obj.attachments + \"-\";\n }\n // if we have a namespace other than `/`\n // we append it followed by a comma `,`\n if (obj.nsp && \"/\" !== obj.nsp) {\n str += obj.nsp + \",\";\n }\n // immediately followed by the id\n if (null != obj.id) {\n str += obj.id;\n }\n // json data\n if (null != obj.data) {\n str += JSON.stringify(obj.data, this.replacer);\n }\n return str;\n }\n /**\n * Encode packet as 'buffer sequence' by removing blobs, and\n * deconstructing packet into object with placeholders and\n * a list of buffers.\n */\n encodeAsBinary(obj) {\n const deconstruction = deconstructPacket(obj);\n const pack = this.encodeAsString(deconstruction.packet);\n const buffers = deconstruction.buffers;\n buffers.unshift(pack); // add packet info to beginning of data list\n return buffers; // write all the buffers\n }\n}\n// see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript\nfunction isObject(value) {\n return Object.prototype.toString.call(value) === \"[object Object]\";\n}\n/**\n * A socket.io Decoder instance\n *\n * @return {Object} decoder\n */\nexport class Decoder extends Emitter {\n /**\n * Decoder constructor\n *\n * @param {function} reviver - custom reviver to pass down to JSON.stringify\n */\n constructor(reviver) {\n super();\n this.reviver = reviver;\n }\n /**\n * Decodes an encoded packet string into packet JSON.\n *\n * @param {String} obj - encoded packet\n */\n add(obj) {\n let packet;\n if (typeof obj === \"string\") {\n if (this.reconstructor) {\n throw new Error(\"got plaintext data when reconstructing a packet\");\n }\n packet = this.decodeString(obj);\n const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;\n if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {\n packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;\n // binary packet's json\n this.reconstructor = new BinaryReconstructor(packet);\n // no attachments, labeled binary but no binary data to follow\n if (packet.attachments === 0) {\n super.emitReserved(\"decoded\", packet);\n }\n }\n else {\n // non-binary full packet\n super.emitReserved(\"decoded\", packet);\n }\n }\n else if (isBinary(obj) || obj.base64) {\n // raw binary data\n if (!this.reconstructor) {\n throw new Error(\"got binary data when not reconstructing a packet\");\n }\n else {\n packet = this.reconstructor.takeBinaryData(obj);\n if (packet) {\n // received final buffer\n this.reconstructor = null;\n super.emitReserved(\"decoded\", packet);\n }\n }\n }\n else {\n throw new Error(\"Unknown type: \" + obj);\n }\n }\n /**\n * Decode a packet String (JSON data)\n *\n * @param {String} str\n * @return {Object} packet\n */\n decodeString(str) {\n let i = 0;\n // look up type\n const p = {\n type: Number(str.charAt(0)),\n };\n if (PacketType[p.type] === undefined) {\n throw new Error(\"unknown packet type \" + p.type);\n }\n // look up attachments if type binary\n if (p.type === PacketType.BINARY_EVENT ||\n p.type === PacketType.BINARY_ACK) {\n const start = i + 1;\n while (str.charAt(++i) !== \"-\" && i != str.length) { }\n const buf = str.substring(start, i);\n if (buf != Number(buf) || str.charAt(i) !== \"-\") {\n throw new Error(\"Illegal attachments\");\n }\n p.attachments = Number(buf);\n }\n // look up namespace (if any)\n if (\"/\" === str.charAt(i + 1)) {\n const start = i + 1;\n while (++i) {\n const c = str.charAt(i);\n if (\",\" === c)\n break;\n if (i === str.length)\n break;\n }\n p.nsp = str.substring(start, i);\n }\n else {\n p.nsp = \"/\";\n }\n // look up id\n const next = str.charAt(i + 1);\n if (\"\" !== next && Number(next) == next) {\n const start = i + 1;\n while (++i) {\n const c = str.charAt(i);\n if (null == c || Number(c) != c) {\n --i;\n break;\n }\n if (i === str.length)\n break;\n }\n p.id = Number(str.substring(start, i + 1));\n }\n // look up json data\n if (str.charAt(++i)) {\n const payload = this.tryParse(str.substr(i));\n if (Decoder.isPayloadValid(p.type, payload)) {\n p.data = payload;\n }\n else {\n throw new Error(\"invalid payload\");\n }\n }\n return p;\n }\n tryParse(str) {\n try {\n return JSON.parse(str, this.reviver);\n }\n catch (e) {\n return false;\n }\n }\n static isPayloadValid(type, payload) {\n switch (type) {\n case PacketType.CONNECT:\n return isObject(payload);\n case PacketType.DISCONNECT:\n return payload === undefined;\n case PacketType.CONNECT_ERROR:\n return typeof payload === \"string\" || isObject(payload);\n case PacketType.EVENT:\n case PacketType.BINARY_EVENT:\n return (Array.isArray(payload) &&\n (typeof payload[0] === \"number\" ||\n (typeof payload[0] === \"string\" &&\n RESERVED_EVENTS.indexOf(payload[0]) === -1)));\n case PacketType.ACK:\n case PacketType.BINARY_ACK:\n return Array.isArray(payload);\n }\n }\n /**\n * Deallocates a parser's resources\n */\n destroy() {\n if (this.reconstructor) {\n this.reconstructor.finishedReconstruction();\n this.reconstructor = null;\n }\n }\n}\n/**\n * A manager of a binary event's 'buffer sequence'. Should\n * be constructed whenever a packet of type BINARY_EVENT is\n * decoded.\n *\n * @param {Object} packet\n * @return {BinaryReconstructor} initialized reconstructor\n */\nclass BinaryReconstructor {\n constructor(packet) {\n this.packet = packet;\n this.buffers = [];\n this.reconPack = packet;\n }\n /**\n * Method to be called when binary data received from connection\n * after a BINARY_EVENT packet.\n *\n * @param {Buffer | ArrayBuffer} binData - the raw binary data received\n * @return {null | Object} returns null if more binary data is expected or\n * a reconstructed packet object if all buffers have been received.\n */\n takeBinaryData(binData) {\n this.buffers.push(binData);\n if (this.buffers.length === this.reconPack.attachments) {\n // done with buffer list\n const packet = reconstructPacket(this.reconPack, this.buffers);\n this.finishedReconstruction();\n return packet;\n }\n return null;\n }\n /**\n * Cleans up binary packet reconstruction variables.\n */\n finishedReconstruction() {\n this.reconPack = null;\n this.buffers = [];\n }\n}\n","export function on(obj, ev, fn) {\n obj.on(ev, fn);\n return function subDestroy() {\n obj.off(ev, fn);\n };\n}\n","import { PacketType } from \"socket.io-parser\";\nimport { on } from \"./on.js\";\nimport { Emitter, } from \"@socket.io/component-emitter\";\n/**\n * Internal events.\n * These events can't be emitted by the user.\n */\nconst RESERVED_EVENTS = Object.freeze({\n connect: 1,\n connect_error: 1,\n disconnect: 1,\n disconnecting: 1,\n // EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener\n newListener: 1,\n removeListener: 1,\n});\n/**\n * A Socket is the fundamental class for interacting with the server.\n *\n * A Socket belongs to a certain Namespace (by default /) and uses an underlying {@link Manager} to communicate.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(\"connected\");\n * });\n *\n * // send an event to the server\n * socket.emit(\"foo\", \"bar\");\n *\n * socket.on(\"foobar\", () => {\n * // an event was received from the server\n * });\n *\n * // upon disconnection\n * socket.on(\"disconnect\", (reason) => {\n * console.log(`disconnected due to ${reason}`);\n * });\n */\nexport class Socket extends Emitter {\n /**\n * `Socket` constructor.\n */\n constructor(io, nsp, opts) {\n super();\n /**\n * Whether the socket is currently connected to the server.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(socket.connected); // true\n * });\n *\n * socket.on(\"disconnect\", () => {\n * console.log(socket.connected); // false\n * });\n */\n this.connected = false;\n /**\n * Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will\n * be transmitted by the server.\n */\n this.recovered = false;\n /**\n * Buffer for packets received before the CONNECT packet\n */\n this.receiveBuffer = [];\n /**\n * Buffer for packets that will be sent once the socket is connected\n */\n this.sendBuffer = [];\n /**\n * The queue of packets to be sent with retry in case of failure.\n *\n * Packets are sent one by one, each waiting for the server acknowledgement, in order to guarantee the delivery order.\n * @private\n */\n this._queue = [];\n /**\n * A sequence to generate the ID of the {@link QueuedPacket}.\n * @private\n */\n this._queueSeq = 0;\n this.ids = 0;\n this.acks = {};\n this.flags = {};\n this.io = io;\n this.nsp = nsp;\n if (opts && opts.auth) {\n this.auth = opts.auth;\n }\n this._opts = Object.assign({}, opts);\n if (this.io._autoConnect)\n this.open();\n }\n /**\n * Whether the socket is currently disconnected\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(socket.disconnected); // false\n * });\n *\n * socket.on(\"disconnect\", () => {\n * console.log(socket.disconnected); // true\n * });\n */\n get disconnected() {\n return !this.connected;\n }\n /**\n * Subscribe to open, close and packet events\n *\n * @private\n */\n subEvents() {\n if (this.subs)\n return;\n const io = this.io;\n this.subs = [\n on(io, \"open\", this.onopen.bind(this)),\n on(io, \"packet\", this.onpacket.bind(this)),\n on(io, \"error\", this.onerror.bind(this)),\n on(io, \"close\", this.onclose.bind(this)),\n ];\n }\n /**\n * Whether the Socket will try to reconnect when its Manager connects or reconnects.\n *\n * @example\n * const socket = io();\n *\n * console.log(socket.active); // true\n *\n * socket.on(\"disconnect\", (reason) => {\n * if (reason === \"io server disconnect\") {\n * // the disconnection was initiated by the server, you need to manually reconnect\n * console.log(socket.active); // false\n * }\n * // else the socket will automatically try to reconnect\n * console.log(socket.active); // true\n * });\n */\n get active() {\n return !!this.subs;\n }\n /**\n * \"Opens\" the socket.\n *\n * @example\n * const socket = io({\n * autoConnect: false\n * });\n *\n * socket.connect();\n */\n connect() {\n if (this.connected)\n return this;\n this.subEvents();\n if (!this.io[\"_reconnecting\"])\n this.io.open(); // ensure open\n if (\"open\" === this.io._readyState)\n this.onopen();\n return this;\n }\n /**\n * Alias for {@link connect()}.\n */\n open() {\n return this.connect();\n }\n /**\n * Sends a `message` event.\n *\n * This method mimics the WebSocket.send() method.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send\n *\n * @example\n * socket.send(\"hello\");\n *\n * // this is equivalent to\n * socket.emit(\"message\", \"hello\");\n *\n * @return self\n */\n send(...args) {\n args.unshift(\"message\");\n this.emit.apply(this, args);\n return this;\n }\n /**\n * Override `emit`.\n * If the event is in `events`, it's emitted normally.\n *\n * @example\n * socket.emit(\"hello\", \"world\");\n *\n * // all serializable datastructures are supported (no need to call JSON.stringify)\n * socket.emit(\"hello\", 1, \"2\", { 3: [\"4\"], 5: Uint8Array.from([6]) });\n *\n * // with an acknowledgement from the server\n * socket.emit(\"hello\", \"world\", (val) => {\n * // ...\n * });\n *\n * @return self\n */\n emit(ev, ...args) {\n if (RESERVED_EVENTS.hasOwnProperty(ev)) {\n throw new Error('\"' + ev.toString() + '\" is a reserved event name');\n }\n args.unshift(ev);\n if (this._opts.retries && !this.flags.fromQueue && !this.flags.volatile) {\n this._addToQueue(args);\n return this;\n }\n const packet = {\n type: PacketType.EVENT,\n data: args,\n };\n packet.options = {};\n packet.options.compress = this.flags.compress !== false;\n // event ack callback\n if (\"function\" === typeof args[args.length - 1]) {\n const id = this.ids++;\n const ack = args.pop();\n this._registerAckCallback(id, ack);\n packet.id = id;\n }\n const isTransportWritable = this.io.engine &&\n this.io.engine.transport &&\n this.io.engine.transport.writable;\n const discardPacket = this.flags.volatile && (!isTransportWritable || !this.connected);\n if (discardPacket) {\n }\n else if (this.connected) {\n this.notifyOutgoingListeners(packet);\n this.packet(packet);\n }\n else {\n this.sendBuffer.push(packet);\n }\n this.flags = {};\n return this;\n }\n /**\n * @private\n */\n _registerAckCallback(id, ack) {\n var _a;\n const timeout = (_a = this.flags.timeout) !== null && _a !== void 0 ? _a : this._opts.ackTimeout;\n if (timeout === undefined) {\n this.acks[id] = ack;\n return;\n }\n // @ts-ignore\n const timer = this.io.setTimeoutFn(() => {\n delete this.acks[id];\n for (let i = 0; i < this.sendBuffer.length; i++) {\n if (this.sendBuffer[i].id === id) {\n this.sendBuffer.splice(i, 1);\n }\n }\n ack.call(this, new Error(\"operation has timed out\"));\n }, timeout);\n this.acks[id] = (...args) => {\n // @ts-ignore\n this.io.clearTimeoutFn(timer);\n ack.apply(this, [null, ...args]);\n };\n }\n /**\n * Emits an event and waits for an acknowledgement\n *\n * @example\n * // without timeout\n * const response = await socket.emitWithAck(\"hello\", \"world\");\n *\n * // with a specific timeout\n * try {\n * const response = await socket.timeout(1000).emitWithAck(\"hello\", \"world\");\n * } catch (err) {\n * // the server did not acknowledge the event in the given delay\n * }\n *\n * @return a Promise that will be fulfilled when the server acknowledges the event\n */\n emitWithAck(ev, ...args) {\n // the timeout flag is optional\n const withErr = this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined;\n return new Promise((resolve, reject) => {\n args.push((arg1, arg2) => {\n if (withErr) {\n return arg1 ? reject(arg1) : resolve(arg2);\n }\n else {\n return resolve(arg1);\n }\n });\n this.emit(ev, ...args);\n });\n }\n /**\n * Add the packet to the queue.\n * @param args\n * @private\n */\n _addToQueue(args) {\n let ack;\n if (typeof args[args.length - 1] === \"function\") {\n ack = args.pop();\n }\n const packet = {\n id: this._queueSeq++,\n tryCount: 0,\n pending: false,\n args,\n flags: Object.assign({ fromQueue: true }, this.flags),\n };\n args.push((err, ...responseArgs) => {\n if (packet !== this._queue[0]) {\n // the packet has already been acknowledged\n return;\n }\n const hasError = err !== null;\n if (hasError) {\n if (packet.tryCount > this._opts.retries) {\n this._queue.shift();\n if (ack) {\n ack(err);\n }\n }\n }\n else {\n this._queue.shift();\n if (ack) {\n ack(null, ...responseArgs);\n }\n }\n packet.pending = false;\n return this._drainQueue();\n });\n this._queue.push(packet);\n this._drainQueue();\n }\n /**\n * Send the first packet of the queue, and wait for an acknowledgement from the server.\n * @param force - whether to resend a packet that has not been acknowledged yet\n *\n * @private\n */\n _drainQueue(force = false) {\n if (!this.connected || this._queue.length === 0) {\n return;\n }\n const packet = this._queue[0];\n if (packet.pending && !force) {\n return;\n }\n packet.pending = true;\n packet.tryCount++;\n this.flags = packet.flags;\n this.emit.apply(this, packet.args);\n }\n /**\n * Sends a packet.\n *\n * @param packet\n * @private\n */\n packet(packet) {\n packet.nsp = this.nsp;\n this.io._packet(packet);\n }\n /**\n * Called upon engine `open`.\n *\n * @private\n */\n onopen() {\n if (typeof this.auth == \"function\") {\n this.auth((data) => {\n this._sendConnectPacket(data);\n });\n }\n else {\n this._sendConnectPacket(this.auth);\n }\n }\n /**\n * Sends a CONNECT packet to initiate the Socket.IO session.\n *\n * @param data\n * @private\n */\n _sendConnectPacket(data) {\n this.packet({\n type: PacketType.CONNECT,\n data: this._pid\n ? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)\n : data,\n });\n }\n /**\n * Called upon engine or manager `error`.\n *\n * @param err\n * @private\n */\n onerror(err) {\n if (!this.connected) {\n this.emitReserved(\"connect_error\", err);\n }\n }\n /**\n * Called upon engine `close`.\n *\n * @param reason\n * @param description\n * @private\n */\n onclose(reason, description) {\n this.connected = false;\n delete this.id;\n this.emitReserved(\"disconnect\", reason, description);\n }\n /**\n * Called with socket packet.\n *\n * @param packet\n * @private\n */\n onpacket(packet) {\n const sameNamespace = packet.nsp === this.nsp;\n if (!sameNamespace)\n return;\n switch (packet.type) {\n case PacketType.CONNECT:\n if (packet.data && packet.data.sid) {\n this.onconnect(packet.data.sid, packet.data.pid);\n }\n else {\n this.emitReserved(\"connect_error\", new Error(\"It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)\"));\n }\n break;\n case PacketType.EVENT:\n case PacketType.BINARY_EVENT:\n this.onevent(packet);\n break;\n case PacketType.ACK:\n case PacketType.BINARY_ACK:\n this.onack(packet);\n break;\n case PacketType.DISCONNECT:\n this.ondisconnect();\n break;\n case PacketType.CONNECT_ERROR:\n this.destroy();\n const err = new Error(packet.data.message);\n // @ts-ignore\n err.data = packet.data.data;\n this.emitReserved(\"connect_error\", err);\n break;\n }\n }\n /**\n * Called upon a server event.\n *\n * @param packet\n * @private\n */\n onevent(packet) {\n const args = packet.data || [];\n if (null != packet.id) {\n args.push(this.ack(packet.id));\n }\n if (this.connected) {\n this.emitEvent(args);\n }\n else {\n this.receiveBuffer.push(Object.freeze(args));\n }\n }\n emitEvent(args) {\n if (this._anyListeners && this._anyListeners.length) {\n const listeners = this._anyListeners.slice();\n for (const listener of listeners) {\n listener.apply(this, args);\n }\n }\n super.emit.apply(this, args);\n if (this._pid && args.length && typeof args[args.length - 1] === \"string\") {\n this._lastOffset = args[args.length - 1];\n }\n }\n /**\n * Produces an ack callback to emit with an event.\n *\n * @private\n */\n ack(id) {\n const self = this;\n let sent = false;\n return function (...args) {\n // prevent double callbacks\n if (sent)\n return;\n sent = true;\n self.packet({\n type: PacketType.ACK,\n id: id,\n data: args,\n });\n };\n }\n /**\n * Called upon a server acknowlegement.\n *\n * @param packet\n * @private\n */\n onack(packet) {\n const ack = this.acks[packet.id];\n if (\"function\" === typeof ack) {\n ack.apply(this, packet.data);\n delete this.acks[packet.id];\n }\n else {\n }\n }\n /**\n * Called upon server connect.\n *\n * @private\n */\n onconnect(id, pid) {\n this.id = id;\n this.recovered = pid && this._pid === pid;\n this._pid = pid; // defined only if connection state recovery is enabled\n this.connected = true;\n this.emitBuffered();\n this.emitReserved(\"connect\");\n this._drainQueue(true);\n }\n /**\n * Emit buffered events (received and emitted).\n *\n * @private\n */\n emitBuffered() {\n this.receiveBuffer.forEach((args) => this.emitEvent(args));\n this.receiveBuffer = [];\n this.sendBuffer.forEach((packet) => {\n this.notifyOutgoingListeners(packet);\n this.packet(packet);\n });\n this.sendBuffer = [];\n }\n /**\n * Called upon server disconnect.\n *\n * @private\n */\n ondisconnect() {\n this.destroy();\n this.onclose(\"io server disconnect\");\n }\n /**\n * Called upon forced client/server side disconnections,\n * this method ensures the manager stops tracking us and\n * that reconnections don't get triggered for this.\n *\n * @private\n */\n destroy() {\n if (this.subs) {\n // clean subscriptions to avoid reconnections\n this.subs.forEach((subDestroy) => subDestroy());\n this.subs = undefined;\n }\n this.io[\"_destroy\"](this);\n }\n /**\n * Disconnects the socket manually. In that case, the socket will not try to reconnect.\n *\n * If this is the last active Socket instance of the {@link Manager}, the low-level connection will be closed.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"disconnect\", (reason) => {\n * // console.log(reason); prints \"io client disconnect\"\n * });\n *\n * socket.disconnect();\n *\n * @return self\n */\n disconnect() {\n if (this.connected) {\n this.packet({ type: PacketType.DISCONNECT });\n }\n // remove socket from pool\n this.destroy();\n if (this.connected) {\n // fire events\n this.onclose(\"io client disconnect\");\n }\n return this;\n }\n /**\n * Alias for {@link disconnect()}.\n *\n * @return self\n */\n close() {\n return this.disconnect();\n }\n /**\n * Sets the compress flag.\n *\n * @example\n * socket.compress(false).emit(\"hello\");\n *\n * @param compress - if `true`, compresses the sending data\n * @return self\n */\n compress(compress) {\n this.flags.compress = compress;\n return this;\n }\n /**\n * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not\n * ready to send messages.\n *\n * @example\n * socket.volatile.emit(\"hello\"); // the server may or may not receive it\n *\n * @returns self\n */\n get volatile() {\n this.flags.volatile = true;\n return this;\n }\n /**\n * Sets a modifier for a subsequent event emission that the callback will be called with an error when the\n * given number of milliseconds have elapsed without an acknowledgement from the server:\n *\n * @example\n * socket.timeout(5000).emit(\"my-event\", (err) => {\n * if (err) {\n * // the server did not acknowledge the event in the given delay\n * }\n * });\n *\n * @returns self\n */\n timeout(timeout) {\n this.flags.timeout = timeout;\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback.\n *\n * @example\n * socket.onAny((event, ...args) => {\n * console.log(`got ${event}`);\n * });\n *\n * @param listener\n */\n onAny(listener) {\n this._anyListeners = this._anyListeners || [];\n this._anyListeners.push(listener);\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback. The listener is added to the beginning of the listeners array.\n *\n * @example\n * socket.prependAny((event, ...args) => {\n * console.log(`got event ${event}`);\n * });\n *\n * @param listener\n */\n prependAny(listener) {\n this._anyListeners = this._anyListeners || [];\n this._anyListeners.unshift(listener);\n return this;\n }\n /**\n * Removes the listener that will be fired when any event is emitted.\n *\n * @example\n * const catchAllListener = (event, ...args) => {\n * console.log(`got event ${event}`);\n * }\n *\n * socket.onAny(catchAllListener);\n *\n * // remove a specific listener\n * socket.offAny(catchAllListener);\n *\n * // or remove all listeners\n * socket.offAny();\n *\n * @param listener\n */\n offAny(listener) {\n if (!this._anyListeners) {\n return this;\n }\n if (listener) {\n const listeners = this._anyListeners;\n for (let i = 0; i < listeners.length; i++) {\n if (listener === listeners[i]) {\n listeners.splice(i, 1);\n return this;\n }\n }\n }\n else {\n this._anyListeners = [];\n }\n return this;\n }\n /**\n * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,\n * e.g. to remove listeners.\n */\n listenersAny() {\n return this._anyListeners || [];\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback.\n *\n * Note: acknowledgements sent to the server are not included.\n *\n * @example\n * socket.onAnyOutgoing((event, ...args) => {\n * console.log(`sent event ${event}`);\n * });\n *\n * @param listener\n */\n onAnyOutgoing(listener) {\n this._anyOutgoingListeners = this._anyOutgoingListeners || [];\n this._anyOutgoingListeners.push(listener);\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback. The listener is added to the beginning of the listeners array.\n *\n * Note: acknowledgements sent to the server are not included.\n *\n * @example\n * socket.prependAnyOutgoing((event, ...args) => {\n * console.log(`sent event ${event}`);\n * });\n *\n * @param listener\n */\n prependAnyOutgoing(listener) {\n this._anyOutgoingListeners = this._anyOutgoingListeners || [];\n this._anyOutgoingListeners.unshift(listener);\n return this;\n }\n /**\n * Removes the listener that will be fired when any event is emitted.\n *\n * @example\n * const catchAllListener = (event, ...args) => {\n * console.log(`sent event ${event}`);\n * }\n *\n * socket.onAnyOutgoing(catchAllListener);\n *\n * // remove a specific listener\n * socket.offAnyOutgoing(catchAllListener);\n *\n * // or remove all listeners\n * socket.offAnyOutgoing();\n *\n * @param [listener] - the catch-all listener (optional)\n */\n offAnyOutgoing(listener) {\n if (!this._anyOutgoingListeners) {\n return this;\n }\n if (listener) {\n const listeners = this._anyOutgoingListeners;\n for (let i = 0; i < listeners.length; i++) {\n if (listener === listeners[i]) {\n listeners.splice(i, 1);\n return this;\n }\n }\n }\n else {\n this._anyOutgoingListeners = [];\n }\n return this;\n }\n /**\n * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,\n * e.g. to remove listeners.\n */\n listenersAnyOutgoing() {\n return this._anyOutgoingListeners || [];\n }\n /**\n * Notify the listeners for each packet sent\n *\n * @param packet\n *\n * @private\n */\n notifyOutgoingListeners(packet) {\n if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {\n const listeners = this._anyOutgoingListeners.slice();\n for (const listener of listeners) {\n listener.apply(this, packet.data);\n }\n }\n }\n}\n","/**\n * Initialize backoff timer with `opts`.\n *\n * - `min` initial timeout in milliseconds [100]\n * - `max` max timeout [10000]\n * - `jitter` [0]\n * - `factor` [2]\n *\n * @param {Object} opts\n * @api public\n */\nexport function Backoff(opts) {\n opts = opts || {};\n this.ms = opts.min || 100;\n this.max = opts.max || 10000;\n this.factor = opts.factor || 2;\n this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0;\n this.attempts = 0;\n}\n/**\n * Return the backoff duration.\n *\n * @return {Number}\n * @api public\n */\nBackoff.prototype.duration = function () {\n var ms = this.ms * Math.pow(this.factor, this.attempts++);\n if (this.jitter) {\n var rand = Math.random();\n var deviation = Math.floor(rand * this.jitter * ms);\n ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation;\n }\n return Math.min(ms, this.max) | 0;\n};\n/**\n * Reset the number of attempts.\n *\n * @api public\n */\nBackoff.prototype.reset = function () {\n this.attempts = 0;\n};\n/**\n * Set the minimum duration\n *\n * @api public\n */\nBackoff.prototype.setMin = function (min) {\n this.ms = min;\n};\n/**\n * Set the maximum duration\n *\n * @api public\n */\nBackoff.prototype.setMax = function (max) {\n this.max = max;\n};\n/**\n * Set the jitter\n *\n * @api public\n */\nBackoff.prototype.setJitter = function (jitter) {\n this.jitter = jitter;\n};\n","import { Socket as Engine, installTimerFunctions, nextTick, } from \"engine.io-client\";\nimport { Socket } from \"./socket.js\";\nimport * as parser from \"socket.io-parser\";\nimport { on } from \"./on.js\";\nimport { Backoff } from \"./contrib/backo2.js\";\nimport { Emitter, } from \"@socket.io/component-emitter\";\nexport class Manager extends Emitter {\n constructor(uri, opts) {\n var _a;\n super();\n this.nsps = {};\n this.subs = [];\n if (uri && \"object\" === typeof uri) {\n opts = uri;\n uri = undefined;\n }\n opts = opts || {};\n opts.path = opts.path || \"/socket.io\";\n this.opts = opts;\n installTimerFunctions(this, opts);\n this.reconnection(opts.reconnection !== false);\n this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);\n this.reconnectionDelay(opts.reconnectionDelay || 1000);\n this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);\n this.randomizationFactor((_a = opts.randomizationFactor) !== null && _a !== void 0 ? _a : 0.5);\n this.backoff = new Backoff({\n min: this.reconnectionDelay(),\n max: this.reconnectionDelayMax(),\n jitter: this.randomizationFactor(),\n });\n this.timeout(null == opts.timeout ? 20000 : opts.timeout);\n this._readyState = \"closed\";\n this.uri = uri;\n const _parser = opts.parser || parser;\n this.encoder = new _parser.Encoder();\n this.decoder = new _parser.Decoder();\n this._autoConnect = opts.autoConnect !== false;\n if (this._autoConnect)\n this.open();\n }\n reconnection(v) {\n if (!arguments.length)\n return this._reconnection;\n this._reconnection = !!v;\n return this;\n }\n reconnectionAttempts(v) {\n if (v === undefined)\n return this._reconnectionAttempts;\n this._reconnectionAttempts = v;\n return this;\n }\n reconnectionDelay(v) {\n var _a;\n if (v === undefined)\n return this._reconnectionDelay;\n this._reconnectionDelay = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMin(v);\n return this;\n }\n randomizationFactor(v) {\n var _a;\n if (v === undefined)\n return this._randomizationFactor;\n this._randomizationFactor = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setJitter(v);\n return this;\n }\n reconnectionDelayMax(v) {\n var _a;\n if (v === undefined)\n return this._reconnectionDelayMax;\n this._reconnectionDelayMax = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMax(v);\n return this;\n }\n timeout(v) {\n if (!arguments.length)\n return this._timeout;\n this._timeout = v;\n return this;\n }\n /**\n * Starts trying to reconnect if reconnection is enabled and we have not\n * started reconnecting yet\n *\n * @private\n */\n maybeReconnectOnOpen() {\n // Only try to reconnect if it's the first time we're connecting\n if (!this._reconnecting &&\n this._reconnection &&\n this.backoff.attempts === 0) {\n // keeps reconnection from firing twice for the same reconnection loop\n this.reconnect();\n }\n }\n /**\n * Sets the current transport `socket`.\n *\n * @param {Function} fn - optional, callback\n * @return self\n * @public\n */\n open(fn) {\n if (~this._readyState.indexOf(\"open\"))\n return this;\n this.engine = new Engine(this.uri, this.opts);\n const socket = this.engine;\n const self = this;\n this._readyState = \"opening\";\n this.skipReconnect = false;\n // emit `open`\n const openSubDestroy = on(socket, \"open\", function () {\n self.onopen();\n fn && fn();\n });\n const onError = (err) => {\n this.cleanup();\n this._readyState = \"closed\";\n this.emitReserved(\"error\", err);\n if (fn) {\n fn(err);\n }\n else {\n // Only do this if there is no fn to handle the error\n this.maybeReconnectOnOpen();\n }\n };\n // emit `error`\n const errorSub = on(socket, \"error\", onError);\n if (false !== this._timeout) {\n const timeout = this._timeout;\n // set timer\n const timer = this.setTimeoutFn(() => {\n openSubDestroy();\n onError(new Error(\"timeout\"));\n socket.close();\n }, timeout);\n if (this.opts.autoUnref) {\n timer.unref();\n }\n this.subs.push(() => {\n this.clearTimeoutFn(timer);\n });\n }\n this.subs.push(openSubDestroy);\n this.subs.push(errorSub);\n return this;\n }\n /**\n * Alias for open()\n *\n * @return self\n * @public\n */\n connect(fn) {\n return this.open(fn);\n }\n /**\n * Called upon transport open.\n *\n * @private\n */\n onopen() {\n // clear old subs\n this.cleanup();\n // mark as open\n this._readyState = \"open\";\n this.emitReserved(\"open\");\n // add new subs\n const socket = this.engine;\n this.subs.push(on(socket, \"ping\", this.onping.bind(this)), on(socket, \"data\", this.ondata.bind(this)), on(socket, \"error\", this.onerror.bind(this)), on(socket, \"close\", this.onclose.bind(this)), on(this.decoder, \"decoded\", this.ondecoded.bind(this)));\n }\n /**\n * Called upon a ping.\n *\n * @private\n */\n onping() {\n this.emitReserved(\"ping\");\n }\n /**\n * Called with data.\n *\n * @private\n */\n ondata(data) {\n try {\n this.decoder.add(data);\n }\n catch (e) {\n this.onclose(\"parse error\", e);\n }\n }\n /**\n * Called when parser fully decodes a packet.\n *\n * @private\n */\n ondecoded(packet) {\n // the nextTick call prevents an exception in a user-provided event listener from triggering a disconnection due to a \"parse error\"\n nextTick(() => {\n this.emitReserved(\"packet\", packet);\n }, this.setTimeoutFn);\n }\n /**\n * Called upon socket error.\n *\n * @private\n */\n onerror(err) {\n this.emitReserved(\"error\", err);\n }\n /**\n * Creates a new socket for the given `nsp`.\n *\n * @return {Socket}\n * @public\n */\n socket(nsp, opts) {\n let socket = this.nsps[nsp];\n if (!socket) {\n socket = new Socket(this, nsp, opts);\n this.nsps[nsp] = socket;\n }\n else if (this._autoConnect && !socket.active) {\n socket.connect();\n }\n return socket;\n }\n /**\n * Called upon a socket close.\n *\n * @param socket\n * @private\n */\n _destroy(socket) {\n const nsps = Object.keys(this.nsps);\n for (const nsp of nsps) {\n const socket = this.nsps[nsp];\n if (socket.active) {\n return;\n }\n }\n this._close();\n }\n /**\n * Writes a packet.\n *\n * @param packet\n * @private\n */\n _packet(packet) {\n const encodedPackets = this.encoder.encode(packet);\n for (let i = 0; i < encodedPackets.length; i++) {\n this.engine.write(encodedPackets[i], packet.options);\n }\n }\n /**\n * Clean up transport subscriptions and packet buffer.\n *\n * @private\n */\n cleanup() {\n this.subs.forEach((subDestroy) => subDestroy());\n this.subs.length = 0;\n this.decoder.destroy();\n }\n /**\n * Close the current socket.\n *\n * @private\n */\n _close() {\n this.skipReconnect = true;\n this._reconnecting = false;\n this.onclose(\"forced close\");\n if (this.engine)\n this.engine.close();\n }\n /**\n * Alias for close()\n *\n * @private\n */\n disconnect() {\n return this._close();\n }\n /**\n * Called upon engine close.\n *\n * @private\n */\n onclose(reason, description) {\n this.cleanup();\n this.backoff.reset();\n this._readyState = \"closed\";\n this.emitReserved(\"close\", reason, description);\n if (this._reconnection && !this.skipReconnect) {\n this.reconnect();\n }\n }\n /**\n * Attempt a reconnection.\n *\n * @private\n */\n reconnect() {\n if (this._reconnecting || this.skipReconnect)\n return this;\n const self = this;\n if (this.backoff.attempts >= this._reconnectionAttempts) {\n this.backoff.reset();\n this.emitReserved(\"reconnect_failed\");\n this._reconnecting = false;\n }\n else {\n const delay = this.backoff.duration();\n this._reconnecting = true;\n const timer = this.setTimeoutFn(() => {\n if (self.skipReconnect)\n return;\n this.emitReserved(\"reconnect_attempt\", self.backoff.attempts);\n // check again for the case socket closed in above events\n if (self.skipReconnect)\n return;\n self.open((err) => {\n if (err) {\n self._reconnecting = false;\n self.reconnect();\n this.emitReserved(\"reconnect_error\", err);\n }\n else {\n self.onreconnect();\n }\n });\n }, delay);\n if (this.opts.autoUnref) {\n timer.unref();\n }\n this.subs.push(() => {\n this.clearTimeoutFn(timer);\n });\n }\n }\n /**\n * Called upon successful reconnect.\n *\n * @private\n */\n onreconnect() {\n const attempt = this.backoff.attempts;\n this._reconnecting = false;\n this.backoff.reset();\n this.emitReserved(\"reconnect\", attempt);\n }\n}\n","import { url } from \"./url.js\";\nimport { Manager } from \"./manager.js\";\nimport { Socket } from \"./socket.js\";\n/**\n * Managers cache.\n */\nconst cache = {};\nfunction lookup(uri, opts) {\n if (typeof uri === \"object\") {\n opts = uri;\n uri = undefined;\n }\n opts = opts || {};\n const parsed = url(uri, opts.path || \"/socket.io\");\n const source = parsed.source;\n const id = parsed.id;\n const path = parsed.path;\n const sameNamespace = cache[id] && path in cache[id][\"nsps\"];\n const newConnection = opts.forceNew ||\n opts[\"force new connection\"] ||\n false === opts.multiplex ||\n sameNamespace;\n let io;\n if (newConnection) {\n io = new Manager(source, opts);\n }\n else {\n if (!cache[id]) {\n cache[id] = new Manager(source, opts);\n }\n io = cache[id];\n }\n if (parsed.query && !opts.query) {\n opts.query = parsed.queryKey;\n }\n return io.socket(parsed.path, opts);\n}\n// so that \"lookup\" can be used both as a function (e.g. `io(...)`) and as a\n// namespace (e.g. `io.connect(...)`), for backward compatibility\nObject.assign(lookup, {\n Manager,\n Socket,\n io: lookup,\n connect: lookup,\n});\n/**\n * Protocol version.\n *\n * @public\n */\nexport { protocol } from \"socket.io-parser\";\n/**\n * Expose constructors for standalone build.\n *\n * @public\n */\nexport { Manager, Socket, lookup as io, lookup as connect, lookup as default, };\n","import { parse } from \"engine.io-client\";\n/**\n * URL parser.\n *\n * @param uri - url\n * @param path - the request path of the connection\n * @param loc - An object meant to mimic window.location.\n * Defaults to window.location.\n * @public\n */\nexport function url(uri, path = \"\", loc) {\n let obj = uri;\n // default to window.location\n loc = loc || (typeof location !== \"undefined\" && location);\n if (null == uri)\n uri = loc.protocol + \"//\" + loc.host;\n // relative path support\n if (typeof uri === \"string\") {\n if (\"/\" === uri.charAt(0)) {\n if (\"/\" === uri.charAt(1)) {\n uri = loc.protocol + uri;\n }\n else {\n uri = loc.host + uri;\n }\n }\n if (!/^(https?|wss?):\\/\\//.test(uri)) {\n if (\"undefined\" !== typeof loc) {\n uri = loc.protocol + \"//\" + uri;\n }\n else {\n uri = \"https://\" + uri;\n }\n }\n // parse\n obj = parse(uri);\n }\n // make sure we treat `localhost:80` and `localhost` equally\n if (!obj.port) {\n if (/^(http|ws)$/.test(obj.protocol)) {\n obj.port = \"80\";\n }\n else if (/^(http|ws)s$/.test(obj.protocol)) {\n obj.port = \"443\";\n }\n }\n obj.path = obj.path || \"/\";\n const ipv6 = obj.host.indexOf(\":\") !== -1;\n const host = ipv6 ? \"[\" + obj.host + \"]\" : obj.host;\n // define unique id\n obj.id = obj.protocol + \"://\" + host + \":\" + obj.port + path;\n // define href\n obj.href =\n obj.protocol +\n \"://\" +\n host +\n (loc && loc.port === obj.port ? \"\" : \":\" + obj.port);\n return obj;\n}\n"],"names":["PACKET_TYPES","Object","create","PACKET_TYPES_REVERSE","keys","forEach","key","TEXT_ENCODER","ERROR_PACKET","type","data","withNativeBlob","Blob","prototype","toString","call","withNativeArrayBuffer","ArrayBuffer","isView","obj","buffer","encodePacket","_ref","supportsBinary","callback","encodeBlobAsBase64","fileReader","FileReader","onload","content","result","split","readAsDataURL","toArray","Uint8Array","byteOffset","byteLength","chars","lookup","i","charCodeAt","TEXT_DECODER","decodePacket","encodedPacket","binaryType","mapBinary","charAt","decodeBase64Packet","substring","length","decoded","base64","encoded1","encoded2","encoded3","encoded4","bufferLength","len","p","arraybuffer","bytes","decode","SEPARATOR","String","fromCharCode","createPacketEncoderStream","TransformStream","transform","packet","controller","arrayBuffer","then","encoded","TextEncoder","encode","encodePacketToBinary","header","payloadLength","DataView","setUint8","view","setUint16","setBigUint64","BigInt","enqueue","totalLength","chunks","reduce","acc","chunk","concatChunks","size","shift","j","slice","Emitter","mixin","on","addEventListener","event","fn","this","_callbacks","push","once","off","apply","arguments","removeListener","removeAllListeners","removeEventListener","cb","callbacks","splice","emit","args","Array","emitReserved","listeners","hasListeners","globalThisShim","self","window","Function","pick","_len","attr","_key","k","hasOwnProperty","NATIVE_SET_TIMEOUT","globalThis","setTimeout","NATIVE_CLEAR_TIMEOUT","clearTimeout","installTimerFunctions","opts","useNativeTimers","setTimeoutFn","bind","clearTimeoutFn","prev","TransportError","_Error","_inherits","_super","_createSuper","reason","description","context","_this","_classCallCheck","_createClass","_wrapNativeSuper","Error","Transport","_Emitter","_super2","_this2","writable","_assertThisInitialized","query","socket","value","_get","_getPrototypeOf","readyState","doOpen","doClose","onClose","packets","write","onPacket","details","onPause","schema","undefined","_hostname","_port","path","_query","hostname","indexOf","port","secure","Number","encodedQuery","str","encodeURIComponent","alphabet","map","seed","num","Math","floor","yeast","now","Date","XMLHttpRequest","err","hasCORS","XHR","xdomain","e","concat","join","empty","hasXHR2","responseType","Polling","_Transport","polling","location","isSSL","protocol","xd","forceBase64","withCredentials","cookieJar","createCookieJar","get","poll","pause","total","doPoll","_this3","encodedPayload","encodedPackets","decodedPacket","decodePayload","onOpen","_this4","close","_this5","count","encodePayload","doWrite","timestampRequests","timestampParam","sid","b64","createUri","_extends","Request","uri","_this6","req","request","method","xhrStatus","onError","_this7","onData","pollXhr","_this8","_a","_this9","xhr","open","extraHeaders","setDisableHeaderCheck","setRequestHeader","addCookies","requestTimeout","timeout","onreadystatechange","parseCookies","status","onLoad","send","document","index","requestsCount","requests","cleanup","fromError","abort","responseText","attachEvent","unloadHandler","nextTick","Promise","resolve","WebSocket","MozWebSocket","isReactNative","navigator","product","toLowerCase","WS","check","protocols","headers","ws","addEventListeners","onopen","autoUnref","_socket","unref","onclose","closeEvent","onmessage","ev","onerror","_loop","lastPacket","WT","WebTransport","transport","transportOptions","name","closed","ready","createBidirectionalStream","stream","decoderStream","maxPayload","TextDecoder","state","expectedLength","isBinary","headerArray","getUint16","n","getUint32","pow","createPacketDecoderStream","MAX_SAFE_INTEGER","reader","readable","pipeThrough","getReader","encoderStream","pipeTo","writer","getWriter","read","done","transports","websocket","webtransport","re","parts","parse","src","b","replace","m","exec","source","host","authority","ipv6uri","pathNames","regx","names","queryKey","$0","$1","$2","Socket","writeBuffer","_typeof","prevBufferLen","agent","upgrade","rememberUpgrade","addTrailingSlash","rejectUnauthorized","perMessageDeflate","threshold","closeOnBeforeunload","qs","qry","pairs","l","pair","decodeURIComponent","id","upgrades","pingInterval","pingTimeout","pingTimeoutTimer","beforeunloadEventListener","offlineEventListener","EIO","priorWebsocketSuccess","createTransport","setTransport","onDrain","failed","onTransportOpen","msg","upgrading","flush","freezeTransport","error","onTransportClose","onupgrade","to","probe","resetPingTimeout","onHandshake","JSON","sendPacket","code","filterUpgrades","getWritablePackets","payloadSize","c","utf8Length","ceil","options","compress","cleanupAndClose","waitForUpgrade","filteredUpgrades","Socket$1","withNativeFile","File","hasBinary","toJSON","isArray","deconstructPacket","buffers","packetData","pack","_deconstructPacket","attachments","placeholder","_placeholder","newData","reconstructPacket","_reconstructPacket","PacketType","RESERVED_EVENTS","Encoder","replacer","EVENT","ACK","encodeAsString","encodeAsBinary","BINARY_EVENT","BINARY_ACK","nsp","stringify","deconstruction","unshift","isObject","Decoder","reviver","reconstructor","isBinaryEvent","decodeString","BinaryReconstructor","takeBinaryData","start","buf","next","payload","tryParse","substr","isPayloadValid","finishedReconstruction","CONNECT","DISCONNECT","CONNECT_ERROR","reconPack","binData","freeze","connect","connect_error","disconnect","disconnecting","newListener","io","connected","recovered","receiveBuffer","sendBuffer","_queue","_queueSeq","ids","acks","flags","auth","_opts","_autoConnect","subs","onpacket","subEvents","_readyState","_len2","_key2","retries","fromQueue","_addToQueue","ack","pop","_registerAckCallback","isTransportWritable","engine","notifyOutgoingListeners","ackTimeout","timer","_len3","_key3","_len4","_key4","withErr","reject","arg1","arg2","tryCount","pending","_len5","responseArgs","_key5","_drainQueue","force","_packet","_sendConnectPacket","_pid","pid","offset","_lastOffset","onconnect","onevent","onack","ondisconnect","destroy","message","emitEvent","_anyListeners","_step","_iterator","_createForOfIteratorHelper","s","f","sent","_len6","_key6","emitBuffered","subDestroy","listener","_anyOutgoingListeners","_step2","_iterator2","Backoff","ms","min","max","factor","jitter","attempts","duration","rand","random","deviation","reset","setMin","setMax","setJitter","Manager","nsps","reconnection","reconnectionAttempts","Infinity","reconnectionDelay","reconnectionDelayMax","randomizationFactor","backoff","_parser","parser","encoder","decoder","autoConnect","v","_reconnection","_reconnectionAttempts","_reconnectionDelay","_randomizationFactor","_reconnectionDelayMax","_timeout","_reconnecting","reconnect","Engine","skipReconnect","openSubDestroy","maybeReconnectOnOpen","errorSub","onping","ondata","ondecoded","add","active","_i","_nsps","_close","delay","onreconnect","attempt","cache","parsed","loc","test","href","url","sameNamespace","forceNew","multiplex"],"mappings":";;;;;6lJAAA,IAAMA,EAAeC,OAAOC,OAAO,MACnCF,EAAmB,KAAI,IACvBA,EAAoB,MAAI,IACxBA,EAAmB,KAAI,IACvBA,EAAmB,KAAI,IACvBA,EAAsB,QAAI,IAC1BA,EAAsB,QAAI,IAC1BA,EAAmB,KAAI,IACvB,IAAMG,EAAuBF,OAAOC,OAAO,MAC3CD,OAAOG,KAAKJ,GAAcK,SAAQ,SAAAC,GAC9BH,EAAqBH,EAAaM,IAAQA,CAC9C,IACA,ICuCIC,EDvCEC,EAAe,CAAEC,KAAM,QAASC,KAAM,gBCXtCC,EAAiC,mBAATC,MACT,oBAATA,MACqC,6BAAzCX,OAAOY,UAAUC,SAASC,KAAKH,MACjCI,EAA+C,mBAAhBC,YAE/BC,EAAS,SAAAC,GACX,MAAqC,mBAAvBF,YAAYC,OACpBD,YAAYC,OAAOC,GACnBA,GAAOA,EAAIC,kBAAkBH,WACvC,EACMI,EAAe,SAAHC,EAAoBC,EAAgBC,GAAa,IAA3Cf,EAAIa,EAAJb,KAAMC,EAAIY,EAAJZ,KAC1B,OAAIC,GAAkBD,aAAgBE,KAC9BW,EACOC,EAASd,GAGTe,EAAmBf,EAAMc,GAG/BR,IACJN,aAAgBO,aAAeC,EAAOR,IACnCa,EACOC,EAASd,GAGTe,EAAmB,IAAIb,KAAK,CAACF,IAAQc,GAI7CA,EAASxB,EAAaS,IAASC,GAAQ,IAClD,EACMe,EAAqB,SAACf,EAAMc,GAC9B,IAAME,EAAa,IAAIC,WAKvB,OAJAD,EAAWE,OAAS,WAChB,IAAMC,EAAUH,EAAWI,OAAOC,MAAM,KAAK,GAC7CP,EAAS,KAAOK,GAAW,MAExBH,EAAWM,cAActB,EACpC,EACA,SAASuB,EAAQvB,GACb,OAAIA,aAAgBwB,WACTxB,EAEFA,aAAgBO,YACd,IAAIiB,WAAWxB,GAGf,IAAIwB,WAAWxB,EAAKU,OAAQV,EAAKyB,WAAYzB,EAAK0B,WAEjE,CC9CA,IAHA,IAAMC,EAAQ,mEAERC,EAA+B,oBAAfJ,WAA6B,GAAK,IAAIA,WAAW,KAC9DK,EAAI,EAAGA,EAAIF,GAAcE,IAC9BD,EAAOD,EAAMG,WAAWD,IAAMA,EAkB3B,ICyCHE,EC9DEzB,EAA+C,mBAAhBC,YACxByB,EAAe,SAACC,EAAeC,GACxC,GAA6B,iBAAlBD,EACP,MAAO,CACHlC,KAAM,UACNC,KAAMmC,EAAUF,EAAeC,IAGvC,IAAMnC,EAAOkC,EAAcG,OAAO,GAClC,MAAa,MAATrC,EACO,CACHA,KAAM,UACNC,KAAMqC,EAAmBJ,EAAcK,UAAU,GAAIJ,IAG1CzC,EAAqBM,GAIjCkC,EAAcM,OAAS,EACxB,CACExC,KAAMN,EAAqBM,GAC3BC,KAAMiC,EAAcK,UAAU,IAEhC,CACEvC,KAAMN,EAAqBM,IARxBD,CAUf,EACMuC,EAAqB,SAACrC,EAAMkC,GAC9B,GAAI5B,EAAuB,CACvB,IAAMkC,EFTQ,SAACC,GACnB,IAA8DZ,EAAUa,EAAUC,EAAUC,EAAUC,EAAlGC,EAA+B,IAAhBL,EAAOF,OAAeQ,EAAMN,EAAOF,OAAWS,EAAI,EACnC,MAA9BP,EAAOA,EAAOF,OAAS,KACvBO,IACkC,MAA9BL,EAAOA,EAAOF,OAAS,IACvBO,KAGR,IAAMG,EAAc,IAAI1C,YAAYuC,GAAeI,EAAQ,IAAI1B,WAAWyB,GAC1E,IAAKpB,EAAI,EAAGA,EAAIkB,EAAKlB,GAAK,EACtBa,EAAWd,EAAOa,EAAOX,WAAWD,IACpCc,EAAWf,EAAOa,EAAOX,WAAWD,EAAI,IACxCe,EAAWhB,EAAOa,EAAOX,WAAWD,EAAI,IACxCgB,EAAWjB,EAAOa,EAAOX,WAAWD,EAAI,IACxCqB,EAAMF,KAAQN,GAAY,EAAMC,GAAY,EAC5CO,EAAMF,MAAoB,GAAXL,IAAkB,EAAMC,GAAY,EACnDM,EAAMF,MAAoB,EAAXJ,IAAiB,EAAiB,GAAXC,EAE1C,OAAOI,CACX,CEVwBE,CAAOnD,GACvB,OAAOmC,EAAUK,EAASN,EAC9B,CAEI,MAAO,CAAEO,QAAQ,EAAMzC,KAAAA,EAE/B,EACMmC,EAAY,SAACnC,EAAMkC,GACrB,MACS,SADDA,EAEIlC,aAAgBE,KAETF,EAIA,IAAIE,KAAK,CAACF,IAIjBA,aAAgBO,YAETP,EAIAA,EAAKU,MAG5B,ED1DM0C,EAAYC,OAAOC,aAAa,IA4B/B,SAASC,IACZ,OAAO,IAAIC,gBAAgB,CACvBC,UAASA,SAACC,EAAQC,IFmBnB,SAA8BD,EAAQ5C,GACrCb,GAAkByD,EAAO1D,gBAAgBE,KAClCwD,EAAO1D,KACT4D,cACAC,KAAKtC,GACLsC,KAAK/C,GAELR,IACJoD,EAAO1D,gBAAgBO,aAAeC,EAAOkD,EAAO1D,OAC9Cc,EAASS,EAAQmC,EAAO1D,OAEnCW,EAAa+C,GAAQ,GAAO,SAAAI,GACnBjE,IACDA,EAAe,IAAIkE,aAEvBjD,EAASjB,EAAamE,OAAOF,GACjC,GACJ,CEnCYG,CAAqBP,GAAQ,SAAAzB,GACzB,IACIiC,EADEC,EAAgBlC,EAAcM,OAGpC,GAAI4B,EAAgB,IAChBD,EAAS,IAAI1C,WAAW,GACxB,IAAI4C,SAASF,EAAOxD,QAAQ2D,SAAS,EAAGF,QAEvC,GAAIA,EAAgB,MAAO,CAC5BD,EAAS,IAAI1C,WAAW,GACxB,IAAM8C,EAAO,IAAIF,SAASF,EAAOxD,QACjC4D,EAAKD,SAAS,EAAG,KACjBC,EAAKC,UAAU,EAAGJ,EACtB,KACK,CACDD,EAAS,IAAI1C,WAAW,GACxB,IAAM8C,EAAO,IAAIF,SAASF,EAAOxD,QACjC4D,EAAKD,SAAS,EAAG,KACjBC,EAAKE,aAAa,EAAGC,OAAON,GAChC,CAEIT,EAAO1D,MAA+B,iBAAhB0D,EAAO1D,OAC7BkE,EAAO,IAAM,KAEjBP,EAAWe,QAAQR,GACnBP,EAAWe,QAAQzC,EACvB,GACJ,GAER,CAEA,SAAS0C,EAAYC,GACjB,OAAOA,EAAOC,QAAO,SAACC,EAAKC,GAAK,OAAKD,EAAMC,EAAMxC,MAAM,GAAE,EAC7D,CACA,SAASyC,EAAaJ,EAAQK,GAC1B,GAAIL,EAAO,GAAGrC,SAAW0C,EACrB,OAAOL,EAAOM,QAIlB,IAFA,IAAMxE,EAAS,IAAIc,WAAWyD,GAC1BE,EAAI,EACCtD,EAAI,EAAGA,EAAIoD,EAAMpD,IACtBnB,EAAOmB,GAAK+C,EAAO,GAAGO,KAClBA,IAAMP,EAAO,GAAGrC,SAChBqC,EAAOM,QACPC,EAAI,GAMZ,OAHIP,EAAOrC,QAAU4C,EAAIP,EAAO,GAAGrC,SAC/BqC,EAAO,GAAKA,EAAO,GAAGQ,MAAMD,IAEzBzE,CACX,CE/EO,SAAS2E,EAAQ5E,GACtB,GAAIA,EAAK,OAWX,SAAeA,GACb,IAAK,IAAIb,KAAOyF,EAAQlF,UACtBM,EAAIb,GAAOyF,EAAQlF,UAAUP,GAE/B,OAAOa,CACT,CAhBkB6E,CAAM7E,EACxB,CA0BA4E,EAAQlF,UAAUoF,GAClBF,EAAQlF,UAAUqF,iBAAmB,SAASC,EAAOC,GAInD,OAHAC,KAAKC,WAAaD,KAAKC,YAAc,CAAA,GACpCD,KAAKC,WAAW,IAAMH,GAASE,KAAKC,WAAW,IAAMH,IAAU,IAC7DI,KAAKH,GACDC,IACT,EAYAN,EAAQlF,UAAU2F,KAAO,SAASL,EAAOC,GACvC,SAASH,IACPI,KAAKI,IAAIN,EAAOF,GAChBG,EAAGM,MAAML,KAAMM,UACjB,CAIA,OAFAV,EAAGG,GAAKA,EACRC,KAAKJ,GAAGE,EAAOF,GACRI,IACT,EAYAN,EAAQlF,UAAU4F,IAClBV,EAAQlF,UAAU+F,eAClBb,EAAQlF,UAAUgG,mBAClBd,EAAQlF,UAAUiG,oBAAsB,SAASX,EAAOC,GAItD,GAHAC,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAGjC,GAAKK,UAAU1D,OAEjB,OADAoD,KAAKC,WAAa,GACXD,KAIT,IAUIU,EAVAC,EAAYX,KAAKC,WAAW,IAAMH,GACtC,IAAKa,EAAW,OAAOX,KAGvB,GAAI,GAAKM,UAAU1D,OAEjB,cADOoD,KAAKC,WAAW,IAAMH,GACtBE,KAKT,IAAK,IAAI9D,EAAI,EAAGA,EAAIyE,EAAU/D,OAAQV,IAEpC,IADAwE,EAAKC,EAAUzE,MACJ6D,GAAMW,EAAGX,KAAOA,EAAI,CAC7BY,EAAUC,OAAO1E,EAAG,GACpB,KACF,CASF,OAJyB,IAArByE,EAAU/D,eACLoD,KAAKC,WAAW,IAAMH,GAGxBE,IACT,EAUAN,EAAQlF,UAAUqG,KAAO,SAASf,GAChCE,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAKrC,IAHA,IAAIa,EAAO,IAAIC,MAAMT,UAAU1D,OAAS,GACpC+D,EAAYX,KAAKC,WAAW,IAAMH,GAE7B5D,EAAI,EAAGA,EAAIoE,UAAU1D,OAAQV,IACpC4E,EAAK5E,EAAI,GAAKoE,UAAUpE,GAG1B,GAAIyE,EAEG,CAAIzE,EAAI,EAAb,IAAK,IAAWkB,GADhBuD,EAAYA,EAAUlB,MAAM,IACI7C,OAAQV,EAAIkB,IAAOlB,EACjDyE,EAAUzE,GAAGmE,MAAML,KAAMc,EADKlE,CAKlC,OAAOoD,IACT,EAGAN,EAAQlF,UAAUwG,aAAetB,EAAQlF,UAAUqG,KAUnDnB,EAAQlF,UAAUyG,UAAY,SAASnB,GAErC,OADAE,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAC9BD,KAAKC,WAAW,IAAMH,IAAU,EACzC,EAUAJ,EAAQlF,UAAU0G,aAAe,SAASpB,GACxC,QAAUE,KAAKiB,UAAUnB,GAAOlD,MAClC,ECxKO,IAAMuE,EACW,oBAATC,KACAA,KAEgB,oBAAXC,OACLA,OAGAC,SAAS,cAATA,GCPR,SAASC,EAAKzG,GAAc,IAAA0G,IAAAA,EAAAlB,UAAA1D,OAAN6E,MAAIV,MAAAS,EAAAA,EAAAA,OAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAJD,EAAIC,EAAApB,GAAAA,UAAAoB,GAC7B,OAAOD,EAAKvC,QAAO,SAACC,EAAKwC,GAIrB,OAHI7G,EAAI8G,eAAeD,KACnBxC,EAAIwC,GAAK7G,EAAI6G,IAEVxC,CACV,GAAE,CAAE,EACT,CAEA,IAAM0C,EAAqBC,EAAWC,WAChCC,EAAuBF,EAAWG,aACjC,SAASC,EAAsBpH,EAAKqH,GACnCA,EAAKC,iBACLtH,EAAIuH,aAAeR,EAAmBS,KAAKR,GAC3ChH,EAAIyH,eAAiBP,EAAqBM,KAAKR,KAG/ChH,EAAIuH,aAAeP,EAAWC,WAAWO,KAAKR,GAC9ChH,EAAIyH,eAAiBT,EAAWG,aAAaK,KAAKR,GAE1D,CClB8C,ICAzBU,EDCfC,WAAcC,GAAAC,EAAAF,EAAAC,GAAA,IAAAE,EAAAC,EAAAJ,GAChB,SAAAA,EAAYK,EAAQC,EAAaC,GAAS,IAAAC,EAIT,OAJSC,OAAAT,IACtCQ,EAAAL,EAAAlI,UAAMoI,IACDC,YAAcA,EACnBE,EAAKD,QAAUA,EACfC,EAAK7I,KAAO,iBAAiB6I,CACjC,CAAC,OAAAE,EAAAV,EAAA,EAAAW,EANwBC,QAQhBC,WAASC,GAAAZ,EAAAW,EAAAC,GAAA,IAAAC,EAAAX,EAAAS,GAOlB,SAAAA,EAAYnB,GAAM,IAAAsB,EAMY,OANZP,OAAAI,IACdG,EAAAD,EAAA9I,KAAAsF,OACK0D,UAAW,EAChBxB,EAAqByB,EAAAF,GAAOtB,GAC5BsB,EAAKtB,KAAOA,EACZsB,EAAKG,MAAQzB,EAAKyB,MAClBH,EAAKI,OAAS1B,EAAK0B,OAAOJ,CAC9B,CAiHC,OAhHDN,EAAAG,EAAA,CAAA,CAAArJ,IAAA,UAAA6J,MASA,SAAQhB,EAAQC,EAAaC,GAEzB,OADAe,EAAAC,EAAAV,EAAA9I,gCAAAE,KAAAsF,KAAmB,QAAS,IAAIyC,EAAeK,EAAQC,EAAaC,IAC7DhD,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAGA,WAGI,OAFA9D,KAAKiE,WAAa,UAClBjE,KAAKkE,SACElE,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAGA,WAKI,MAJwB,YAApB9D,KAAKiE,YAAgD,SAApBjE,KAAKiE,aACtCjE,KAAKmE,UACLnE,KAAKoE,WAEFpE,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAKA,SAAKO,GACuB,SAApBrE,KAAKiE,YACLjE,KAAKsE,MAAMD,EAKnB,GACA,CAAApK,IAAA,SAAA6J,MAKA,WACI9D,KAAKiE,WAAa,OAClBjE,KAAK0D,UAAW,EAChBK,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,OACvB,GACA,CAAA/F,IAAA,SAAA6J,MAMA,SAAOzJ,GACH,IAAM0D,EAAS1B,EAAahC,EAAM2F,KAAK6D,OAAOtH,YAC9CyD,KAAKuE,SAASxG,EAClB,GACA,CAAA9D,IAAA,WAAA6J,MAKA,SAAS/F,GACLgG,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,SAAUjC,EACjC,GACA,CAAA9D,IAAA,UAAA6J,MAKA,SAAQU,GACJxE,KAAKiE,WAAa,SAClBF,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,QAASwE,EAChC,GACA,CAAAvK,IAAA,QAAA6J,MAKA,SAAMW,GAAW,GAAC,CAAAxK,IAAA,YAAA6J,MAClB,SAAUY,GAAoB,IAAZd,EAAKtD,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EACtB,OAAQoE,EACJ,MACA1E,KAAK4E,YACL5E,KAAK6E,QACL7E,KAAKmC,KAAK2C,KACV9E,KAAK+E,OAAOnB,EACpB,GAAC,CAAA3J,IAAA,YAAA6J,MACD,WACI,IAAMkB,EAAWhF,KAAKmC,KAAK6C,SAC3B,OAAkC,IAA3BA,EAASC,QAAQ,KAAcD,EAAW,IAAMA,EAAW,GACtE,GAAC,CAAA/K,IAAA,QAAA6J,MACD,WACI,OAAI9D,KAAKmC,KAAK+C,OACRlF,KAAKmC,KAAKgD,QAAUC,OAA0B,MAAnBpF,KAAKmC,KAAK+C,QACjClF,KAAKmC,KAAKgD,QAAqC,KAA3BC,OAAOpF,KAAKmC,KAAK+C,OACpC,IAAMlF,KAAKmC,KAAK+C,KAGhB,EAEf,GAAC,CAAAjL,IAAA,SAAA6J,MACD,SAAOF,GACH,IAAMyB,EEjIP,SAAgBvK,GACnB,IAAIwK,EAAM,GACV,IAAK,IAAIpJ,KAAKpB,EACNA,EAAI8G,eAAe1F,KACfoJ,EAAI1I,SACJ0I,GAAO,KACXA,GAAOC,mBAAmBrJ,GAAK,IAAMqJ,mBAAmBzK,EAAIoB,KAGpE,OAAOoJ,CACX,CFuH6BjH,CAAOuF,GAC5B,OAAOyB,EAAazI,OAAS,IAAMyI,EAAe,EACtD,KAAC/B,CAAA,EA/H0B5D,GCVzB8F,EAAW,mEAAmE9J,MAAM,IAAKkB,EAAS,GAAI6I,EAAM,CAAA,EAC9GC,EAAO,EAAGxJ,EAAI,EAQX,SAASmC,EAAOsH,GACnB,IAAIxH,EAAU,GACd,GACIA,EAAUqH,EAASG,EAAM/I,GAAUuB,EACnCwH,EAAMC,KAAKC,MAAMF,EAAM/I,SAClB+I,EAAM,GACf,OAAOxH,CACX,CAqBO,SAAS2H,IACZ,IAAMC,EAAM1H,GAAQ,IAAI2H,MACxB,OAAID,IAAQvD,GACDkD,EAAO,EAAGlD,EAAOuD,GACrBA,EAAM,IAAM1H,EAAOqH,IAC9B,CAIA,KAAOxJ,EAAIU,EAAQV,IACfuJ,EAAID,EAAStJ,IAAMA,EEhDvB,IAAI4H,IAAQ,EACZ,IACIA,GAAkC,oBAAnBmC,gBACX,oBAAqB,IAAIA,cACjC,CACA,MAAOC,GAEH,CAEG,IAAMC,GAAUrC,GCPhB,SAASsC,GAAIjE,GAChB,IAAMkE,EAAUlE,EAAKkE,QAErB,IACI,GAAI,oBAAuBJ,kBAAoBI,GAAWF,IACtD,OAAO,IAAIF,cAEnB,CACA,MAAOK,GAAK,CACZ,IAAKD,EACD,IACI,OAAO,IAAIvE,EAAW,CAAC,UAAUyE,OAAO,UAAUC,KAAK,OAAM,oBACjE,CACA,MAAOF,GAAK,CAEpB,CCXA,SAASG,KAAU,CACnB,IAAMC,GAIK,MAHK,IAAIT,GAAe,CAC3BI,SAAS,IAEMM,aAEVC,YAAOC,GAAAlE,EAAAiE,EAAAC,GAAA,IAAAjE,EAAAC,EAAA+D,GAOhB,SAAAA,EAAYzE,GAAM,IAAAc,EAGd,GAHcC,OAAA0D,IACd3D,EAAAL,EAAAlI,UAAMyH,IACD2E,SAAU,EACS,oBAAbC,SAA0B,CACjC,IAAMC,EAAQ,WAAaD,SAASE,SAChC/B,EAAO6B,SAAS7B,KAEfA,IACDA,EAAO8B,EAAQ,MAAQ,MAE3B/D,EAAKiE,GACoB,oBAAbH,UACJ5E,EAAK6C,WAAa+B,SAAS/B,UAC3BE,IAAS/C,EAAK+C,IAC1B,CAIA,IAAMiC,EAAchF,GAAQA,EAAKgF,YAIhC,OAHDlE,EAAK/H,eAAiBwL,KAAYS,EAC9BlE,EAAKd,KAAKiF,kBACVnE,EAAKoE,eAAYC,GACpBrE,CACL,CAgLC,OAhLAE,EAAAyD,EAAA,CAAA,CAAA3M,IAAA,OAAAsN,IACD,WACI,MAAO,SACX,GACA,CAAAtN,IAAA,SAAA6J,MAMA,WACI9D,KAAKwH,MACT,GACA,CAAAvN,IAAA,QAAA6J,MAMA,SAAMW,GAAS,IAAAhB,EAAAzD,KACXA,KAAKiE,WAAa,UAClB,IAAMwD,EAAQ,WACVhE,EAAKQ,WAAa,SAClBQ,KAEJ,GAAIzE,KAAK8G,UAAY9G,KAAK0D,SAAU,CAChC,IAAIgE,EAAQ,EACR1H,KAAK8G,UACLY,IACA1H,KAAKG,KAAK,gBAAgB,aACpBuH,GAASD,GACf,KAECzH,KAAK0D,WACNgE,IACA1H,KAAKG,KAAK,SAAS,aACbuH,GAASD,GACf,IAER,MAEIA,GAER,GACA,CAAAxN,IAAA,OAAA6J,MAKA,WACI9D,KAAK8G,SAAU,EACf9G,KAAK2H,SACL3H,KAAKgB,aAAa,OACtB,GACA,CAAA/G,IAAA,SAAA6J,MAKA,SAAOzJ,GAAM,IAAAuN,EAAA5H,MVpFK,SAAC6H,EAAgBtL,GAGnC,IAFA,IAAMuL,EAAiBD,EAAenM,MAAM+B,GACtC4G,EAAU,GACPnI,EAAI,EAAGA,EAAI4L,EAAelL,OAAQV,IAAK,CAC5C,IAAM6L,EAAgB1L,EAAayL,EAAe5L,GAAIK,GAEtD,GADA8H,EAAQnE,KAAK6H,GACc,UAAvBA,EAAc3N,KACd,KAER,CACA,OAAOiK,CACX,EUwFQ2D,CAAc3N,EAAM2F,KAAK6D,OAAOtH,YAAYvC,SAd3B,SAAC+D,GAMd,GAJI,YAAc6J,EAAK3D,YAA8B,SAAhBlG,EAAO3D,MACxCwN,EAAKK,SAGL,UAAYlK,EAAO3D,KAEnB,OADAwN,EAAKxD,QAAQ,CAAErB,YAAa,oCACrB,EAGX6E,EAAKrD,SAASxG,MAKd,WAAaiC,KAAKiE,aAElBjE,KAAK8G,SAAU,EACf9G,KAAKgB,aAAa,gBACd,SAAWhB,KAAKiE,YAChBjE,KAAKwH,OAKjB,GACA,CAAAvN,IAAA,UAAA6J,MAKA,WAAU,IAAAoE,EAAAlI,KACAmI,EAAQ,WACVD,EAAK5D,MAAM,CAAC,CAAElK,KAAM,YAEpB,SAAW4F,KAAKiE,WAChBkE,IAKAnI,KAAKG,KAAK,OAAQgI,EAE1B,GACA,CAAAlO,IAAA,QAAA6J,MAMA,SAAMO,GAAS,IAAA+D,EAAApI,KACXA,KAAK0D,UAAW,EVxJF,SAACW,EAASlJ,GAE5B,IAAMyB,EAASyH,EAAQzH,OACjBkL,EAAiB,IAAI/G,MAAMnE,GAC7ByL,EAAQ,EACZhE,EAAQrK,SAAQ,SAAC+D,EAAQ7B,GAErBlB,EAAa+C,GAAQ,GAAO,SAAAzB,GACxBwL,EAAe5L,GAAKI,IACd+L,IAAUzL,GACZzB,EAAS2M,EAAetB,KAAK/I,GAErC,GACJ,GACJ,CU2IQ6K,CAAcjE,GAAS,SAAChK,GACpB+N,EAAKG,QAAQlO,GAAM,WACf+N,EAAK1E,UAAW,EAChB0E,EAAKpH,aAAa,QACtB,GACJ,GACJ,GACA,CAAA/G,IAAA,MAAA6J,MAKA,WACI,IAAMY,EAAS1E,KAAKmC,KAAKgD,OAAS,QAAU,OACtCvB,EAAQ5D,KAAK4D,OAAS,GAQ5B,OANI,IAAU5D,KAAKmC,KAAKqG,oBACpB5E,EAAM5D,KAAKmC,KAAKsG,gBAAkB3C,KAEjC9F,KAAK9E,gBAAmB0I,EAAM8E,MAC/B9E,EAAM+E,IAAM,GAET3I,KAAK4I,UAAUlE,EAAQd,EAClC,GACA,CAAA3J,IAAA,UAAA6J,MAMA,WAAmB,IAAX3B,EAAI7B,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EAEX,OADAuI,EAAc1G,EAAM,CAAE+E,GAAIlH,KAAKkH,GAAIG,UAAWrH,KAAKqH,WAAarH,KAAKmC,MAC9D,IAAI2G,GAAQ9I,KAAK+I,MAAO5G,EACnC,GACA,CAAAlI,IAAA,UAAA6J,MAOA,SAAQzJ,EAAM0F,GAAI,IAAAiJ,EAAAhJ,KACRiJ,EAAMjJ,KAAKkJ,QAAQ,CACrBC,OAAQ,OACR9O,KAAMA,IAEV4O,EAAIrJ,GAAG,UAAWG,GAClBkJ,EAAIrJ,GAAG,SAAS,SAACwJ,EAAWpG,GACxBgG,EAAKK,QAAQ,iBAAkBD,EAAWpG,EAC9C,GACJ,GACA,CAAA/I,IAAA,SAAA6J,MAKA,WAAS,IAAAwF,EAAAtJ,KACCiJ,EAAMjJ,KAAKkJ,UACjBD,EAAIrJ,GAAG,OAAQI,KAAKuJ,OAAOjH,KAAKtC,OAChCiJ,EAAIrJ,GAAG,SAAS,SAACwJ,EAAWpG,GACxBsG,EAAKD,QAAQ,iBAAkBD,EAAWpG,EAC9C,IACAhD,KAAKwJ,QAAUP,CACnB,KAACrC,CAAA,EA9MwBtD,GAgNhBwF,YAAOvF,GAAAZ,EAAAmG,EAAAvF,GAAA,IAAAC,EAAAX,EAAAiG,GAOhB,SAAAA,EAAYC,EAAK5G,GAAM,IAAAsH,EAOL,OAPKvG,OAAA4F,GAEnB5G,EAAqByB,EADrB8F,EAAAjG,EAAA9I,KAAAsF,OAC4BmC,GAC5BsH,EAAKtH,KAAOA,EACZsH,EAAKN,OAAShH,EAAKgH,QAAU,MAC7BM,EAAKV,IAAMA,EACXU,EAAKpP,UAAOsK,IAAcxC,EAAK9H,KAAO8H,EAAK9H,KAAO,KAClDoP,EAAK5P,SAAS4P,CAClB,CA8HC,OA7HDtG,EAAA2F,EAAA,CAAA,CAAA7O,IAAA,SAAA6J,MAKA,WAAS,IACD4F,EADCC,EAAA3J,KAECmC,EAAOZ,EAAKvB,KAAKmC,KAAM,QAAS,MAAO,MAAO,aAAc,OAAQ,KAAM,UAAW,qBAAsB,aACjHA,EAAKkE,UAAYrG,KAAKmC,KAAK+E,GAC3B,IAAM0C,EAAO5J,KAAK4J,IAAM,IAAI3D,GAAe9D,GAC3C,IACIyH,EAAIC,KAAK7J,KAAKmJ,OAAQnJ,KAAK+I,KAAK,GAChC,IACI,GAAI/I,KAAKmC,KAAK2H,aAEV,IAAK,IAAI5N,KADT0N,EAAIG,uBAAyBH,EAAIG,uBAAsB,GACzC/J,KAAKmC,KAAK2H,aAChB9J,KAAKmC,KAAK2H,aAAalI,eAAe1F,IACtC0N,EAAII,iBAAiB9N,EAAG8D,KAAKmC,KAAK2H,aAAa5N,GAI/D,CACA,MAAOoK,GAAK,CACZ,GAAI,SAAWtG,KAAKmJ,OAChB,IACIS,EAAII,iBAAiB,eAAgB,2BACzC,CACA,MAAO1D,GAAK,CAEhB,IACIsD,EAAII,iBAAiB,SAAU,MACnC,CACA,MAAO1D,GAAK,CACmB,QAA9BoD,EAAK1J,KAAKmC,KAAKkF,iBAA8B,IAAPqC,GAAyBA,EAAGO,WAAWL,GAE1E,oBAAqBA,IACrBA,EAAIxC,gBAAkBpH,KAAKmC,KAAKiF,iBAEhCpH,KAAKmC,KAAK+H,iBACVN,EAAIO,QAAUnK,KAAKmC,KAAK+H,gBAE5BN,EAAIQ,mBAAqB,WACrB,IAAIV,EACmB,IAAnBE,EAAI3F,aAC2B,QAA9ByF,EAAKC,EAAKxH,KAAKkF,iBAA8B,IAAPqC,GAAyBA,EAAGW,aAAaT,IAEhF,IAAMA,EAAI3F,aAEV,MAAQ2F,EAAIU,QAAU,OAASV,EAAIU,OACnCX,EAAKY,SAKLZ,EAAKtH,cAAa,WACdsH,EAAKN,QAA8B,iBAAfO,EAAIU,OAAsBV,EAAIU,OAAS,EAC9D,GAAE,KAGXV,EAAIY,KAAKxK,KAAK3F,KACjB,CACD,MAAOiM,GAOH,YAHAtG,KAAKqC,cAAa,WACdsH,EAAKN,QAAQ/C,EAChB,GAAE,EAEP,CACwB,oBAAbmE,WACPzK,KAAK0K,MAAQ5B,EAAQ6B,gBACrB7B,EAAQ8B,SAAS5K,KAAK0K,OAAS1K,KAEvC,GACA,CAAA/F,IAAA,UAAA6J,MAKA,SAAQoC,GACJlG,KAAKgB,aAAa,QAASkF,EAAKlG,KAAK4J,KACrC5J,KAAK6K,SAAQ,EACjB,GACA,CAAA5Q,IAAA,UAAA6J,MAKA,SAAQgH,GACJ,QAAI,IAAuB9K,KAAK4J,KAAO,OAAS5J,KAAK4J,IAArD,CAIA,GADA5J,KAAK4J,IAAIQ,mBAAqB3D,GAC1BqE,EACA,IACI9K,KAAK4J,IAAImB,OACb,CACA,MAAOzE,GAAK,CAEQ,oBAAbmE,iBACA3B,EAAQ8B,SAAS5K,KAAK0K,OAEjC1K,KAAK4J,IAAM,IAXX,CAYJ,GACA,CAAA3P,IAAA,SAAA6J,MAKA,WACI,IAAMzJ,EAAO2F,KAAK4J,IAAIoB,aACT,OAAT3Q,IACA2F,KAAKgB,aAAa,OAAQ3G,GAC1B2F,KAAKgB,aAAa,WAClBhB,KAAK6K,UAEb,GACA,CAAA5Q,IAAA,QAAA6J,MAKA,WACI9D,KAAK6K,SACT,KAAC/B,CAAA,EA7IwBpJ,GAsJ7B,GAPAoJ,GAAQ6B,cAAgB,EACxB7B,GAAQ8B,SAAW,CAAA,EAMK,oBAAbH,SAEP,GAA2B,mBAAhBQ,YAEPA,YAAY,WAAYC,SAEvB,GAAgC,mBAArBrL,iBAAiC,CAE7CA,iBADyB,eAAgBiC,EAAa,WAAa,SAChCoJ,IAAe,EACtD,CAEJ,SAASA,KACL,IAAK,IAAIhP,KAAK4M,GAAQ8B,SACd9B,GAAQ8B,SAAShJ,eAAe1F,IAChC4M,GAAQ8B,SAAS1O,GAAG6O,OAGhC,CCpYO,IAAMI,GACqC,mBAAZC,SAAqD,mBAApBA,QAAQC,QAEhE,SAAC3K,GAAE,OAAK0K,QAAQC,UAAUnN,KAAKwC,EAAG,EAGlC,SAACA,EAAI2B,GAAY,OAAKA,EAAa3B,EAAI,EAAE,EAG3C4K,GAAYxJ,EAAWwJ,WAAaxJ,EAAWyJ,aCJtDC,GAAqC,oBAAdC,WACI,iBAAtBA,UAAUC,SACmB,gBAApCD,UAAUC,QAAQC,cACTC,YAAE/E,GAAAlE,EAAAiJ,EAAA/E,GAAA,IAAAjE,EAAAC,EAAA+I,GAOX,SAAAA,EAAYzJ,GAAM,IAAAc,EAE0B,OAF1BC,OAAA0I,IACd3I,EAAAL,EAAAlI,UAAMyH,IACDjH,gBAAkBiH,EAAKgF,YAAYlE,CAC5C,CAmIC,OAnIAE,EAAAyI,EAAA,CAAA,CAAA3R,IAAA,OAAAsN,IACD,WACI,MAAO,WACX,GAAC,CAAAtN,IAAA,SAAA6J,MACD,WACI,GAAK9D,KAAK6L,QAAV,CAIA,IAAM9C,EAAM/I,KAAK+I,MACX+C,EAAY9L,KAAKmC,KAAK2J,UAEtB3J,EAAOqJ,GACP,CAAA,EACAjK,EAAKvB,KAAKmC,KAAM,QAAS,oBAAqB,MAAO,MAAO,aAAc,OAAQ,KAAM,UAAW,qBAAsB,eAAgB,kBAAmB,SAAU,aAAc,SAAU,uBAChMnC,KAAKmC,KAAK2H,eACV3H,EAAK4J,QAAU/L,KAAKmC,KAAK2H,cAE7B,IACI9J,KAAKgM,GACyBR,GAIpB,IAAIF,GAAUvC,EAAK+C,EAAW3J,GAH9B2J,EACI,IAAIR,GAAUvC,EAAK+C,GACnB,IAAIR,GAAUvC,EAE/B,CACD,MAAO7C,GACH,OAAOlG,KAAKgB,aAAa,QAASkF,EACtC,CACAlG,KAAKgM,GAAGzP,WAAayD,KAAK6D,OAAOtH,WACjCyD,KAAKiM,mBAtBL,CAuBJ,GACA,CAAAhS,IAAA,oBAAA6J,MAKA,WAAoB,IAAAL,EAAAzD,KAChBA,KAAKgM,GAAGE,OAAS,WACTzI,EAAKtB,KAAKgK,WACV1I,EAAKuI,GAAGI,QAAQC,QAEpB5I,EAAKwE,UAETjI,KAAKgM,GAAGM,QAAU,SAACC,GAAU,OAAK9I,EAAKW,QAAQ,CAC3CrB,YAAa,8BACbC,QAASuJ,GACX,EACFvM,KAAKgM,GAAGQ,UAAY,SAACC,GAAE,OAAKhJ,EAAK8F,OAAOkD,EAAGpS,KAAK,EAChD2F,KAAKgM,GAAGU,QAAU,SAACpG,GAAC,OAAK7C,EAAK4F,QAAQ,kBAAmB/C,EAAE,CAC/D,GAAC,CAAArM,IAAA,QAAA6J,MACD,SAAMO,GAAS,IAAAuD,EAAA5H,KACXA,KAAK0D,UAAW,EAGhB,IADA,IAAAiJ,EAAAA,WAEI,IAAM5O,EAASsG,EAAQnI,GACjB0Q,EAAa1Q,IAAMmI,EAAQzH,OAAS,EAC1C5B,EAAa+C,EAAQ6J,EAAK1M,gBAAgB,SAACb,GAmBvC,IAGQuN,EAAKoE,GAAGxB,KAAKnQ,EAKrB,CACA,MAAOiM,GACP,CACIsG,GAGAzB,IAAS,WACLvD,EAAKlE,UAAW,EAChBkE,EAAK5G,aAAa,QACtB,GAAG4G,EAAKvF,aAEhB,KAzCKnG,EAAI,EAAGA,EAAImI,EAAQzH,OAAQV,IAAGyQ,GA2C3C,GAAC,CAAA1S,IAAA,UAAA6J,MACD,gBAC2B,IAAZ9D,KAAKgM,KACZhM,KAAKgM,GAAG7D,QACRnI,KAAKgM,GAAK,KAElB,GACA,CAAA/R,IAAA,MAAA6J,MAKA,WACI,IAAMY,EAAS1E,KAAKmC,KAAKgD,OAAS,MAAQ,KACpCvB,EAAQ5D,KAAK4D,OAAS,GAS5B,OAPI5D,KAAKmC,KAAKqG,oBACV5E,EAAM5D,KAAKmC,KAAKsG,gBAAkB3C,KAGjC9F,KAAK9E,iBACN0I,EAAM+E,IAAM,GAET3I,KAAK4I,UAAUlE,EAAQd,EAClC,GACA,CAAA3J,IAAA,QAAA6J,MAMA,WACI,QAASwH,EACb,KAACM,CAAA,EA7ImBtI,GCNXuJ,YAAEhG,GAAAlE,EAAAkK,EAAAhG,GAAA,IAAAjE,EAAAC,EAAAgK,GAAA,SAAAA,IAAA,OAAA3J,OAAA2J,GAAAjK,EAAAvC,MAAAL,KAAAM,UAAA,CAkEV,OAlEU6C,EAAA0J,EAAA,CAAA,CAAA5S,IAAA,OAAAsN,IACX,WACI,MAAO,cACX,GAAC,CAAAtN,IAAA,SAAA6J,MACD,WAAS,IAAAb,EAAAjD,KAEuB,mBAAjB8M,eAIX9M,KAAK+M,UAAY,IAAID,aAAa9M,KAAK4I,UAAU,SAAU5I,KAAKmC,KAAK6K,iBAAiBhN,KAAKiN,OAC3FjN,KAAK+M,UAAUG,OACVhP,MAAK,WACN+E,EAAKmB,SACT,IAAE,OACS,SAAC8B,GACRjD,EAAKoG,QAAQ,qBAAsBnD,EACvC,IAEAlG,KAAK+M,UAAUI,MAAMjP,MAAK,WACtB+E,EAAK8J,UAAUK,4BAA4BlP,MAAK,SAACmP,GAC7C,IAAMC,Eb8Df,SAAmCC,EAAYhR,GAC7CH,IACDA,EAAe,IAAIoR,aAEvB,IAAMvO,EAAS,GACXwO,EAAQ,EACRC,GAAkB,EAClBC,GAAW,EACf,OAAO,IAAI9P,gBAAgB,CACvBC,UAASA,SAACsB,EAAOpB,GAEb,IADAiB,EAAOiB,KAAKd,KACC,CACT,GAAc,IAAVqO,EAA+B,CAC/B,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAMV,EAASc,EAAaJ,EAAQ,GACpC0O,EAAkC,MAAV,IAAZpP,EAAO,IACnBmP,EAA6B,IAAZnP,EAAO,GAEpBkP,EADAC,EAAiB,IACT,EAEgB,MAAnBA,EACG,EAGA,CAEhB,MACK,GAAc,IAAVD,EAA2C,CAChD,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAM2O,EAAcvO,EAAaJ,EAAQ,GACzCyO,EAAiB,IAAIjP,SAASmP,EAAY7S,OAAQ6S,EAAY9R,WAAY8R,EAAYhR,QAAQiR,UAAU,GACxGJ,EAAQ,CACZ,MACK,GAAc,IAAVA,EAA2C,CAChD,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAM2O,EAAcvO,EAAaJ,EAAQ,GACnCN,EAAO,IAAIF,SAASmP,EAAY7S,OAAQ6S,EAAY9R,WAAY8R,EAAYhR,QAC5EkR,EAAInP,EAAKoP,UAAU,GACzB,GAAID,EAAIlI,KAAKoI,IAAI,EAAG,IAAW,EAAG,CAE9BhQ,EAAWe,QAAQ5E,GACnB,KACJ,CACAuT,EAAiBI,EAAIlI,KAAKoI,IAAI,EAAG,IAAMrP,EAAKoP,UAAU,GACtDN,EAAQ,CACZ,KACK,CACD,GAAIzO,EAAYC,GAAUyO,EACtB,MAEJ,IAAMrT,EAAOgF,EAAaJ,EAAQyO,GAClC1P,EAAWe,QAAQ1C,EAAasR,EAAWtT,EAAO+B,EAAaoB,OAAOnD,GAAOkC,IAC7EkR,EAAQ,CACZ,CACA,GAAuB,IAAnBC,GAAwBA,EAAiBH,EAAY,CACrDvP,EAAWe,QAAQ5E,GACnB,KACJ,CACJ,CACJ,GAER,CajIsC8T,CAA0B7I,OAAO8I,iBAAkBjL,EAAKY,OAAOtH,YAC/E4R,EAASd,EAAOe,SAASC,YAAYf,GAAegB,YACpDC,EAAgB3Q,IACtB2Q,EAAcH,SAASI,OAAOnB,EAAO3J,UACrCT,EAAKwL,OAASF,EAAc7K,SAASgL,aACxB,SAAPC,IACFR,EACKQ,OACAzQ,MAAK,SAAAjD,GAAqB,IAAlB2T,EAAI3T,EAAJ2T,KAAM9K,EAAK7I,EAAL6I,MACX8K,IAGJ3L,EAAKsB,SAAST,GACd6K,IACH,WACU,SAACzI,GACX,IAELyI,GACA,IAAM5Q,EAAS,CAAE3D,KAAM,QACnB6I,EAAKW,MAAM8E,MACX3K,EAAO1D,KAAI,WAAAkM,OAActD,EAAKW,MAAM8E,IAAO,OAE/CzF,EAAKwL,OAAOnK,MAAMvG,GAAQG,MAAK,WAAA,OAAM+E,EAAKgF,WAC9C,GACJ,IACJ,GAAC,CAAAhO,IAAA,QAAA6J,MACD,SAAMO,GAAS,IAAAZ,EAAAzD,KACXA,KAAK0D,UAAW,EAChB,IADsB,IAAAiJ,EAAAA,WAElB,IAAM5O,EAASsG,EAAQnI,GACjB0Q,EAAa1Q,IAAMmI,EAAQzH,OAAS,EAC1C6G,EAAKgL,OAAOnK,MAAMvG,GAAQG,MAAK,WACvB0O,GACAzB,IAAS,WACL1H,EAAKC,UAAW,EAChBD,EAAKzC,aAAa,QACtB,GAAGyC,EAAKpB,aAEhB,KAVKnG,EAAI,EAAGA,EAAImI,EAAQzH,OAAQV,IAAGyQ,GAY3C,GAAC,CAAA1S,IAAA,UAAA6J,MACD,WACI,IAAI4F,EACsB,QAAzBA,EAAK1J,KAAK+M,iBAA8B,IAAPrD,GAAyBA,EAAGvB,OAClE,KAAC0E,CAAA,EAlEmBvJ,GCAXuL,GAAa,CACtBC,UAAWlD,GACXmD,aAAclC,GACd/F,QAASF,ICaPoI,GAAK,sPACLC,GAAQ,CACV,SAAU,WAAY,YAAa,WAAY,OAAQ,WAAY,OAAQ,OAAQ,WAAY,OAAQ,YAAa,OAAQ,QAAS,UAElI,SAASC,GAAM5J,GAClB,IAAM6J,EAAM7J,EAAK8J,EAAI9J,EAAIL,QAAQ,KAAMqB,EAAIhB,EAAIL,QAAQ,MAC7C,GAANmK,IAAiB,GAAN9I,IACXhB,EAAMA,EAAI3I,UAAU,EAAGyS,GAAK9J,EAAI3I,UAAUyS,EAAG9I,GAAG+I,QAAQ,KAAM,KAAO/J,EAAI3I,UAAU2J,EAAGhB,EAAI1I,SAG9F,IADA,IAwBmBgH,EACbvJ,EAzBFiV,EAAIN,GAAGO,KAAKjK,GAAO,IAAKyD,EAAM,CAAE,EAAE7M,EAAI,GACnCA,KACH6M,EAAIkG,GAAM/S,IAAMoT,EAAEpT,IAAM,GAU5B,OARU,GAANkT,IAAiB,GAAN9I,IACXyC,EAAIyG,OAASL,EACbpG,EAAI0G,KAAO1G,EAAI0G,KAAK9S,UAAU,EAAGoM,EAAI0G,KAAK7S,OAAS,GAAGyS,QAAQ,KAAM,KACpEtG,EAAI2G,UAAY3G,EAAI2G,UAAUL,QAAQ,IAAK,IAAIA,QAAQ,IAAK,IAAIA,QAAQ,KAAM,KAC9EtG,EAAI4G,SAAU,GAElB5G,EAAI6G,UAIR,SAAmB9U,EAAKgK,GACpB,IAAM+K,EAAO,WAAYC,EAAQhL,EAAKuK,QAAQQ,EAAM,KAAKnU,MAAM,KACvC,KAApBoJ,EAAKrF,MAAM,EAAG,IAA6B,IAAhBqF,EAAKlI,QAChCkT,EAAMlP,OAAO,EAAG,GAEE,KAAlBkE,EAAKrF,OAAO,IACZqQ,EAAMlP,OAAOkP,EAAMlT,OAAS,EAAG,GAEnC,OAAOkT,CACX,CAboBF,CAAU7G,EAAKA,EAAU,MACzCA,EAAIgH,UAaenM,EAbUmF,EAAW,MAclC1O,EAAO,CAAA,EACbuJ,EAAMyL,QAAQ,6BAA6B,SAAUW,EAAIC,EAAIC,GACrDD,IACA5V,EAAK4V,GAAMC,EAEnB,IACO7V,GAnBA0O,CACX,CClCaoH,IAAAA,YAAM5M,GAAAZ,EAAAwN,EAAA5M,GAAA,IAAAX,EAAAC,EAAAsN,GAOf,SAAAA,EAAYpH,GAAgB,IAAA9F,EAAXd,EAAI7B,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EAgGR,OAhGU4C,OAAAiN,IACtBlN,EAAAL,EAAAlI,KAAAsF,OACKzD,WLJoB,cKKzB0G,EAAKmN,YAAc,GACfrH,GAAO,WAAQsH,EAAYtH,KAC3B5G,EAAO4G,EACPA,EAAM,MAENA,GACAA,EAAMmG,GAAMnG,GACZ5G,EAAK6C,SAAW+D,EAAI0G,KACpBtN,EAAKgD,OAA0B,UAAjB4D,EAAI9B,UAAyC,QAAjB8B,EAAI9B,SAC9C9E,EAAK+C,KAAO6D,EAAI7D,KACZ6D,EAAInF,QACJzB,EAAKyB,MAAQmF,EAAInF,QAEhBzB,EAAKsN,OACVtN,EAAK6C,SAAWkK,GAAM/M,EAAKsN,MAAMA,MAErCvN,EAAqByB,EAAAV,GAAOd,GAC5Bc,EAAKkC,OACD,MAAQhD,EAAKgD,OACPhD,EAAKgD,OACe,oBAAb4B,UAA4B,WAAaA,SAASE,SAC/D9E,EAAK6C,WAAa7C,EAAK+C,OAEvB/C,EAAK+C,KAAOjC,EAAKkC,OAAS,MAAQ,MAEtClC,EAAK+B,SACD7C,EAAK6C,WACoB,oBAAb+B,SAA2BA,SAAS/B,SAAW,aAC/D/B,EAAKiC,KACD/C,EAAK+C,OACoB,oBAAb6B,UAA4BA,SAAS7B,KACvC6B,SAAS7B,KACTjC,EAAKkC,OACD,MACA,MAClBlC,EAAK4L,WAAa1M,EAAK0M,YAAc,CACjC,UACA,YACA,gBAEJ5L,EAAKmN,YAAc,GACnBnN,EAAKqN,cAAgB,EACrBrN,EAAKd,KAAO0G,EAAc,CACtB/D,KAAM,aACNyL,OAAO,EACPnJ,iBAAiB,EACjBoJ,SAAS,EACT/H,eAAgB,IAChBgI,iBAAiB,EACjBC,kBAAkB,EAClBC,oBAAoB,EACpBC,kBAAmB,CACfC,UAAW,MAEf7D,iBAAkB,CAAE,EACpB8D,qBAAqB,GACtB3O,GACHc,EAAKd,KAAK2C,KACN7B,EAAKd,KAAK2C,KAAKuK,QAAQ,MAAO,KACzBpM,EAAKd,KAAKuO,iBAAmB,IAAM,IACb,iBAApBzN,EAAKd,KAAKyB,QACjBX,EAAKd,KAAKyB,MTrDf,SAAgBmN,GAGnB,IAFA,IAAIC,EAAM,CAAA,EACNC,EAAQF,EAAGrV,MAAM,KACZQ,EAAI,EAAGgV,EAAID,EAAMrU,OAAQV,EAAIgV,EAAGhV,IAAK,CAC1C,IAAIiV,EAAOF,EAAM/U,GAAGR,MAAM,KAC1BsV,EAAII,mBAAmBD,EAAK,KAAOC,mBAAmBD,EAAK,GAC/D,CACA,OAAOH,CACX,CS6C8BxT,CAAOyF,EAAKd,KAAKyB,QAGvCX,EAAKoO,GAAK,KACVpO,EAAKqO,SAAW,KAChBrO,EAAKsO,aAAe,KACpBtO,EAAKuO,YAAc,KAEnBvO,EAAKwO,iBAAmB,KACQ,mBAArB5R,mBACHoD,EAAKd,KAAK2O,sBAIV7N,EAAKyO,0BAA4B,WACzBzO,EAAK8J,YAEL9J,EAAK8J,UAAUvM,qBACfyC,EAAK8J,UAAU5E,UAGvBtI,iBAAiB,eAAgBoD,EAAKyO,2BAA2B,IAE/C,cAAlBzO,EAAK+B,WACL/B,EAAK0O,qBAAuB,WACxB1O,EAAKmB,QAAQ,kBAAmB,CAC5BrB,YAAa,6BAGrBlD,iBAAiB,UAAWoD,EAAK0O,sBAAsB,KAG/D1O,EAAK4G,OAAO5G,CAChB,CAgeC,OA/dDE,EAAAgN,EAAA,CAAA,CAAAlW,IAAA,kBAAA6J,MAOA,SAAgBmJ,GACZ,IAAMrJ,EAAQiF,EAAc,CAAA,EAAI7I,KAAKmC,KAAKyB,OAE1CA,EAAMgO,IhBgCU,EgB9BhBhO,EAAMmJ,UAAYE,EAEdjN,KAAKqR,KACLzN,EAAM8E,IAAM1I,KAAKqR,IACrB,IAAMlP,EAAO0G,EAAc,GAAI7I,KAAKmC,KAAM,CACtCyB,MAAAA,EACAC,OAAQ7D,KACRgF,SAAUhF,KAAKgF,SACfG,OAAQnF,KAAKmF,OACbD,KAAMlF,KAAKkF,MACZlF,KAAKmC,KAAK6K,iBAAiBC,IAC9B,OAAO,IAAI4B,GAAW5B,GAAM9K,EAChC,GACA,CAAAlI,IAAA,OAAA6J,MAKA,WAAO,IACCiJ,EADDtJ,EAAAzD,KAEH,GAAIA,KAAKmC,KAAKsO,iBACVN,EAAO0B,wBACmC,IAA1C7R,KAAK6O,WAAW5J,QAAQ,aACxB8H,EAAY,gBAEX,IAAI,IAAM/M,KAAK6O,WAAWjS,OAK3B,YAHAoD,KAAKqC,cAAa,WACdoB,EAAKzC,aAAa,QAAS,0BAC9B,GAAE,GAIH+L,EAAY/M,KAAK6O,WAAW,EAChC,CACA7O,KAAKiE,WAAa,UAElB,IACI8I,EAAY/M,KAAK8R,gBAAgB/E,EACpC,CACD,MAAOzG,GAGH,OAFAtG,KAAK6O,WAAWtP,aAChBS,KAAK6J,MAET,CACAkD,EAAUlD,OACV7J,KAAK+R,aAAahF,EACtB,GACA,CAAA9S,IAAA,eAAA6J,MAKA,SAAaiJ,GAAW,IAAAnF,EAAA5H,KAChBA,KAAK+M,WACL/M,KAAK+M,UAAUvM,qBAGnBR,KAAK+M,UAAYA,EAEjBA,EACKnN,GAAG,QAASI,KAAKgS,QAAQ1P,KAAKtC,OAC9BJ,GAAG,SAAUI,KAAKuE,SAASjC,KAAKtC,OAChCJ,GAAG,QAASI,KAAKqJ,QAAQ/G,KAAKtC,OAC9BJ,GAAG,SAAS,SAACkD,GAAM,OAAK8E,EAAKxD,QAAQ,kBAAmBtB,KACjE,GACA,CAAA7I,IAAA,QAAA6J,MAMA,SAAMmJ,GAAM,IAAA/E,EAAAlI,KACJ+M,EAAY/M,KAAK8R,gBAAgB7E,GACjCgF,GAAS,EACb9B,EAAO0B,uBAAwB,EAC/B,IAAMK,EAAkB,WAChBD,IAEJlF,EAAUvC,KAAK,CAAC,CAAEpQ,KAAM,OAAQC,KAAM,WACtC0S,EAAU5M,KAAK,UAAU,SAACgS,GACtB,IAAIF,EAEJ,GAAI,SAAWE,EAAI/X,MAAQ,UAAY+X,EAAI9X,KAAM,CAG7C,GAFA6N,EAAKkK,WAAY,EACjBlK,EAAKlH,aAAa,YAAa+L,IAC1BA,EACD,OACJoD,EAAO0B,sBAAwB,cAAgB9E,EAAUE,KACzD/E,EAAK6E,UAAUtF,OAAM,WACbwK,GAEA,WAAa/J,EAAKjE,aAEtB4G,IACA3C,EAAK6J,aAAahF,GAClBA,EAAUvC,KAAK,CAAC,CAAEpQ,KAAM,aACxB8N,EAAKlH,aAAa,UAAW+L,GAC7BA,EAAY,KACZ7E,EAAKkK,WAAY,EACjBlK,EAAKmK,QACT,GACJ,KACK,CACD,IAAMnM,EAAM,IAAI7C,MAAM,eAEtB6C,EAAI6G,UAAYA,EAAUE,KAC1B/E,EAAKlH,aAAa,eAAgBkF,EACtC,CACJ,MAEJ,SAASoM,IACDL,IAGJA,GAAS,EACTpH,IACAkC,EAAU5E,QACV4E,EAAY,KAChB,CAEA,IAAML,EAAU,SAACxG,GACb,IAAMqM,EAAQ,IAAIlP,MAAM,gBAAkB6C,GAE1CqM,EAAMxF,UAAYA,EAAUE,KAC5BqF,IACApK,EAAKlH,aAAa,eAAgBuR,IAEtC,SAASC,IACL9F,EAAQ,mBACZ,CAEA,SAASJ,IACLI,EAAQ,gBACZ,CAEA,SAAS+F,EAAUC,GACX3F,GAAa2F,EAAGzF,OAASF,EAAUE,MACnCqF,GAER,CAEA,IAAMzH,EAAU,WACZkC,EAAUxM,eAAe,OAAQ2R,GACjCnF,EAAUxM,eAAe,QAASmM,GAClCK,EAAUxM,eAAe,QAASiS,GAClCtK,EAAK9H,IAAI,QAASkM,GAClBpE,EAAK9H,IAAI,YAAaqS,IAE1B1F,EAAU5M,KAAK,OAAQ+R,GACvBnF,EAAU5M,KAAK,QAASuM,GACxBK,EAAU5M,KAAK,QAASqS,GACxBxS,KAAKG,KAAK,QAASmM,GACnBtM,KAAKG,KAAK,YAAasS,IACwB,IAA3CzS,KAAKsR,SAASrM,QAAQ,iBACb,iBAATgI,EAEAjN,KAAKqC,cAAa,WACT4P,GACDlF,EAAUlD,MAEjB,GAAE,KAGHkD,EAAUlD,MAElB,GACA,CAAA5P,IAAA,SAAA6J,MAKA,WAOI,GANA9D,KAAKiE,WAAa,OAClBkM,EAAO0B,sBAAwB,cAAgB7R,KAAK+M,UAAUE,KAC9DjN,KAAKgB,aAAa,QAClBhB,KAAKqS,QAGD,SAAWrS,KAAKiE,YAAcjE,KAAKmC,KAAKqO,QAGxC,IAFA,IAAItU,EAAI,EACFgV,EAAIlR,KAAKsR,SAAS1U,OACjBV,EAAIgV,EAAGhV,IACV8D,KAAK2S,MAAM3S,KAAKsR,SAASpV,GAGrC,GACA,CAAAjC,IAAA,WAAA6J,MAKA,SAAS/F,GACL,GAAI,YAAciC,KAAKiE,YACnB,SAAWjE,KAAKiE,YAChB,YAAcjE,KAAKiE,WAKnB,OAJAjE,KAAKgB,aAAa,SAAUjD,GAE5BiC,KAAKgB,aAAa,aAClBhB,KAAK4S,mBACG7U,EAAO3D,MACX,IAAK,OACD4F,KAAK6S,YAAYC,KAAK5D,MAAMnR,EAAO1D,OACnC,MACJ,IAAK,OACD2F,KAAK+S,WAAW,QAChB/S,KAAKgB,aAAa,QAClBhB,KAAKgB,aAAa,QAClB,MACJ,IAAK,QACD,IAAMkF,EAAM,IAAI7C,MAAM,gBAEtB6C,EAAI8M,KAAOjV,EAAO1D,KAClB2F,KAAKqJ,QAAQnD,GACb,MACJ,IAAK,UACDlG,KAAKgB,aAAa,OAAQjD,EAAO1D,MACjC2F,KAAKgB,aAAa,UAAWjD,EAAO1D,MAMpD,GACA,CAAAJ,IAAA,cAAA6J,MAMA,SAAYzJ,GACR2F,KAAKgB,aAAa,YAAa3G,GAC/B2F,KAAKqR,GAAKhX,EAAKqO,IACf1I,KAAK+M,UAAUnJ,MAAM8E,IAAMrO,EAAKqO,IAChC1I,KAAKsR,SAAWtR,KAAKiT,eAAe5Y,EAAKiX,UACzCtR,KAAKuR,aAAelX,EAAKkX,aACzBvR,KAAKwR,YAAcnX,EAAKmX,YACxBxR,KAAKuN,WAAalT,EAAKkT,WACvBvN,KAAKiI,SAED,WAAajI,KAAKiE,YAEtBjE,KAAK4S,kBACT,GACA,CAAA3Y,IAAA,mBAAA6J,MAKA,WAAmB,IAAAsE,EAAApI,KACfA,KAAKuC,eAAevC,KAAKyR,kBACzBzR,KAAKyR,iBAAmBzR,KAAKqC,cAAa,WACtC+F,EAAKhE,QAAQ,eAChB,GAAEpE,KAAKuR,aAAevR,KAAKwR,aACxBxR,KAAKmC,KAAKgK,WACVnM,KAAKyR,iBAAiBpF,OAE9B,GACA,CAAApS,IAAA,UAAA6J,MAKA,WACI9D,KAAKoQ,YAAYxP,OAAO,EAAGZ,KAAKsQ,eAIhCtQ,KAAKsQ,cAAgB,EACjB,IAAMtQ,KAAKoQ,YAAYxT,OACvBoD,KAAKgB,aAAa,SAGlBhB,KAAKqS,OAEb,GACA,CAAApY,IAAA,QAAA6J,MAKA,WACI,GAAI,WAAa9D,KAAKiE,YAClBjE,KAAK+M,UAAUrJ,WACd1D,KAAKoS,WACNpS,KAAKoQ,YAAYxT,OAAQ,CACzB,IAAMyH,EAAUrE,KAAKkT,qBACrBlT,KAAK+M,UAAUvC,KAAKnG,GAGpBrE,KAAKsQ,cAAgBjM,EAAQzH,OAC7BoD,KAAKgB,aAAa,QACtB,CACJ,GACA,CAAA/G,IAAA,qBAAA6J,MAMA,WAII,KAH+B9D,KAAKuN,YACR,YAAxBvN,KAAK+M,UAAUE,MACfjN,KAAKoQ,YAAYxT,OAAS,GAE1B,OAAOoD,KAAKoQ,YAGhB,IADA,IZtZmBtV,EYsZfqY,EAAc,EACTjX,EAAI,EAAGA,EAAI8D,KAAKoQ,YAAYxT,OAAQV,IAAK,CAC9C,IAAM7B,EAAO2F,KAAKoQ,YAAYlU,GAAG7B,KAIjC,GAHIA,IACA8Y,GZzZO,iBADIrY,EY0ZeT,GZnZ1C,SAAoBiL,GAEhB,IADA,IAAI8N,EAAI,EAAGxW,EAAS,EACXV,EAAI,EAAGgV,EAAI5L,EAAI1I,OAAQV,EAAIgV,EAAGhV,KACnCkX,EAAI9N,EAAInJ,WAAWD,IACX,IACJU,GAAU,EAELwW,EAAI,KACTxW,GAAU,EAELwW,EAAI,OAAUA,GAAK,MACxBxW,GAAU,GAGVV,IACAU,GAAU,GAGlB,OAAOA,CACX,CAxBeyW,CAAWvY,GAGf8K,KAAK0N,KAPQ,MAOFxY,EAAIiB,YAAcjB,EAAIwE,QYuZ5BpD,EAAI,GAAKiX,EAAcnT,KAAKuN,WAC5B,OAAOvN,KAAKoQ,YAAY3Q,MAAM,EAAGvD,GAErCiX,GAAe,CACnB,CACA,OAAOnT,KAAKoQ,WAChB,GACA,CAAAnW,IAAA,QAAA6J,MAQA,SAAMqO,EAAKoB,EAASxT,GAEhB,OADAC,KAAK+S,WAAW,UAAWZ,EAAKoB,EAASxT,GAClCC,IACX,GAAC,CAAA/F,IAAA,OAAA6J,MACD,SAAKqO,EAAKoB,EAASxT,GAEf,OADAC,KAAK+S,WAAW,UAAWZ,EAAKoB,EAASxT,GAClCC,IACX,GACA,CAAA/F,IAAA,aAAA6J,MASA,SAAW1J,EAAMC,EAAMkZ,EAASxT,GAS5B,GARI,mBAAsB1F,IACtB0F,EAAK1F,EACLA,OAAOsK,GAEP,mBAAsB4O,IACtBxT,EAAKwT,EACLA,EAAU,MAEV,YAAcvT,KAAKiE,YAAc,WAAajE,KAAKiE,WAAvD,EAGAsP,EAAUA,GAAW,IACbC,UAAW,IAAUD,EAAQC,SACrC,IAAMzV,EAAS,CACX3D,KAAMA,EACNC,KAAMA,EACNkZ,QAASA,GAEbvT,KAAKgB,aAAa,eAAgBjD,GAClCiC,KAAKoQ,YAAYlQ,KAAKnC,GAClBgC,GACAC,KAAKG,KAAK,QAASJ,GACvBC,KAAKqS,OAZL,CAaJ,GACA,CAAApY,IAAA,QAAA6J,MAGA,WAAQ,IAAAkF,EAAAhJ,KACEmI,EAAQ,WACVa,EAAK5E,QAAQ,gBACb4E,EAAK+D,UAAU5E,SAEbsL,EAAkB,SAAlBA,IACFzK,EAAK5I,IAAI,UAAWqT,GACpBzK,EAAK5I,IAAI,eAAgBqT,GACzBtL,KAEEuL,EAAiB,WAEnB1K,EAAK7I,KAAK,UAAWsT,GACrBzK,EAAK7I,KAAK,eAAgBsT,IAqB9B,MAnBI,YAAczT,KAAKiE,YAAc,SAAWjE,KAAKiE,aACjDjE,KAAKiE,WAAa,UACdjE,KAAKoQ,YAAYxT,OACjBoD,KAAKG,KAAK,SAAS,WACX6I,EAAKoJ,UACLsB,IAGAvL,GAER,IAEKnI,KAAKoS,UACVsB,IAGAvL,KAGDnI,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAKA,SAAQoC,GACJiK,EAAO0B,uBAAwB,EAC/B7R,KAAKgB,aAAa,QAASkF,GAC3BlG,KAAKoE,QAAQ,kBAAmB8B,EACpC,GACA,CAAAjM,IAAA,UAAA6J,MAKA,SAAQhB,EAAQC,GACR,YAAc/C,KAAKiE,YACnB,SAAWjE,KAAKiE,YAChB,YAAcjE,KAAKiE,aAEnBjE,KAAKuC,eAAevC,KAAKyR,kBAEzBzR,KAAK+M,UAAUvM,mBAAmB,SAElCR,KAAK+M,UAAU5E,QAEfnI,KAAK+M,UAAUvM,qBACoB,mBAAxBC,sBACPA,oBAAoB,eAAgBT,KAAK0R,2BAA2B,GACpEjR,oBAAoB,UAAWT,KAAK2R,sBAAsB,IAG9D3R,KAAKiE,WAAa,SAElBjE,KAAKqR,GAAK,KAEVrR,KAAKgB,aAAa,QAAS8B,EAAQC,GAGnC/C,KAAKoQ,YAAc,GACnBpQ,KAAKsQ,cAAgB,EAE7B,GACA,CAAArW,IAAA,iBAAA6J,MAMA,SAAewN,GAIX,IAHA,IAAMqC,EAAmB,GACrBzX,EAAI,EACFsD,EAAI8R,EAAS1U,OACZV,EAAIsD,EAAGtD,KACL8D,KAAK6O,WAAW5J,QAAQqM,EAASpV,KAClCyX,EAAiBzT,KAAKoR,EAASpV,IAEvC,OAAOyX,CACX,KAACxD,CAAA,EAxkBuBzQ,GA0kBtBkU,GAAC3M,ShBvbiB,EiBxJAkJ,GAAOlJ,SCF/B,IAAMtM,GAA+C,mBAAhBC,YAC/BC,GAAS,SAACC,GACZ,MAAqC,mBAAvBF,YAAYC,OACpBD,YAAYC,OAAOC,GACnBA,EAAIC,kBAAkBH,WAChC,EACMH,GAAWb,OAAOY,UAAUC,SAC5BH,GAAiC,mBAATC,MACT,oBAATA,MACoB,6BAAxBE,GAASC,KAAKH,MAChBsZ,GAAiC,mBAATC,MACT,oBAATA,MACoB,6BAAxBrZ,GAASC,KAAKoZ,MAMf,SAASnG,GAAS7S,GACrB,OAASH,KAA0BG,aAAeF,aAAeC,GAAOC,KACnER,IAAkBQ,aAAeP,MACjCsZ,IAAkB/Y,aAAegZ,IAC1C,CACO,SAASC,GAAUjZ,EAAKkZ,GAC3B,IAAKlZ,GAAsB,WAAfuV,EAAOvV,GACf,OAAO,EAEX,GAAIiG,MAAMkT,QAAQnZ,GAAM,CACpB,IAAK,IAAIoB,EAAI,EAAGgV,EAAIpW,EAAI8B,OAAQV,EAAIgV,EAAGhV,IACnC,GAAI6X,GAAUjZ,EAAIoB,IACd,OAAO,EAGf,OAAO,CACX,CACA,GAAIyR,GAAS7S,GACT,OAAO,EAEX,GAAIA,EAAIkZ,QACkB,mBAAflZ,EAAIkZ,QACU,IAArB1T,UAAU1D,OACV,OAAOmX,GAAUjZ,EAAIkZ,UAAU,GAEnC,IAAK,IAAM/Z,KAAOa,EACd,GAAIlB,OAAOY,UAAUoH,eAAelH,KAAKI,EAAKb,IAAQ8Z,GAAUjZ,EAAIb,IAChE,OAAO,EAGf,OAAO,CACX,CCzCO,SAASia,GAAkBnW,GAC9B,IAAMoW,EAAU,GACVC,EAAarW,EAAO1D,KACpBga,EAAOtW,EAGb,OAFAsW,EAAKha,KAAOia,GAAmBF,EAAYD,GAC3CE,EAAKE,YAAcJ,EAAQvX,OACpB,CAAEmB,OAAQsW,EAAMF,QAASA,EACpC,CACA,SAASG,GAAmBja,EAAM8Z,GAC9B,IAAK9Z,EACD,OAAOA,EACX,GAAIsT,GAAStT,GAAO,CAChB,IAAMma,EAAc,CAAEC,cAAc,EAAM9O,IAAKwO,EAAQvX,QAEvD,OADAuX,EAAQjU,KAAK7F,GACNma,CACV,CACI,GAAIzT,MAAMkT,QAAQ5Z,GAAO,CAE1B,IADA,IAAMqa,EAAU,IAAI3T,MAAM1G,EAAKuC,QACtBV,EAAI,EAAGA,EAAI7B,EAAKuC,OAAQV,IAC7BwY,EAAQxY,GAAKoY,GAAmBja,EAAK6B,GAAIiY,GAE7C,OAAOO,CACX,CACK,GAAoB,WAAhBrE,EAAOhW,MAAuBA,aAAgB2L,MAAO,CAC1D,IAAM0O,EAAU,CAAA,EAChB,IAAK,IAAMza,KAAOI,EACVT,OAAOY,UAAUoH,eAAelH,KAAKL,EAAMJ,KAC3Cya,EAAQza,GAAOqa,GAAmBja,EAAKJ,GAAMka,IAGrD,OAAOO,CACX,CACA,OAAOra,CACX,CASO,SAASsa,GAAkB5W,EAAQoW,GAGtC,OAFApW,EAAO1D,KAAOua,GAAmB7W,EAAO1D,KAAM8Z,UACvCpW,EAAOwW,YACPxW,CACX,CACA,SAAS6W,GAAmBva,EAAM8Z,GAC9B,IAAK9Z,EACD,OAAOA,EACX,GAAIA,IAA8B,IAAtBA,EAAKoa,aAAuB,CAIpC,GAHyC,iBAAbpa,EAAKsL,KAC7BtL,EAAKsL,KAAO,GACZtL,EAAKsL,IAAMwO,EAAQvX,OAEnB,OAAOuX,EAAQ9Z,EAAKsL,KAGpB,MAAM,IAAItC,MAAM,sBAEvB,CACI,GAAItC,MAAMkT,QAAQ5Z,GACnB,IAAK,IAAI6B,EAAI,EAAGA,EAAI7B,EAAKuC,OAAQV,IAC7B7B,EAAK6B,GAAK0Y,GAAmBva,EAAK6B,GAAIiY,QAGzC,GAAoB,WAAhB9D,EAAOhW,GACZ,IAAK,IAAMJ,KAAOI,EACVT,OAAOY,UAAUoH,eAAelH,KAAKL,EAAMJ,KAC3CI,EAAKJ,GAAO2a,GAAmBva,EAAKJ,GAAMka,IAItD,OAAO9Z,CACX,CC5EA,IAcWwa,GAdLC,GAAkB,CACpB,UACA,gBACA,aACA,gBACA,cACA,mBASJ,SAAWD,GACPA,EAAWA,EAAoB,QAAI,GAAK,UACxCA,EAAWA,EAAuB,WAAI,GAAK,aAC3CA,EAAWA,EAAkB,MAAI,GAAK,QACtCA,EAAWA,EAAgB,IAAI,GAAK,MACpCA,EAAWA,EAA0B,cAAI,GAAK,gBAC9CA,EAAWA,EAAyB,aAAI,GAAK,eAC7CA,EAAWA,EAAuB,WAAI,GAAK,YAC9C,CARD,CAQGA,KAAeA,GAAa,CAAE,IAIjC,IAAaE,GAAO,WAMhB,SAAAA,EAAYC,GAAU9R,OAAA6R,GAClB/U,KAAKgV,SAAWA,CACpB,CA2DC,OA1DD7R,EAAA4R,EAAA,CAAA,CAAA9a,IAAA,SAAA6J,MAMA,SAAOhJ,GACH,OAAIA,EAAIV,OAASya,GAAWI,OAASna,EAAIV,OAASya,GAAWK,MACrDnB,GAAUjZ,GAWX,CAACkF,KAAKmV,eAAera,IAVbkF,KAAKoV,eAAe,CACvBhb,KAAMU,EAAIV,OAASya,GAAWI,MACxBJ,GAAWQ,aACXR,GAAWS,WACjBC,IAAKza,EAAIya,IACTlb,KAAMS,EAAIT,KACVgX,GAAIvW,EAAIuW,IAKxB,GACA,CAAApX,IAAA,iBAAA6J,MAGA,SAAehJ,GAEX,IAAIwK,EAAM,GAAKxK,EAAIV,KAmBnB,OAjBIU,EAAIV,OAASya,GAAWQ,cACxBva,EAAIV,OAASya,GAAWS,aACxBhQ,GAAOxK,EAAIyZ,YAAc,KAIzBzZ,EAAIya,KAAO,MAAQza,EAAIya,MACvBjQ,GAAOxK,EAAIya,IAAM,KAGjB,MAAQza,EAAIuW,KACZ/L,GAAOxK,EAAIuW,IAGX,MAAQvW,EAAIT,OACZiL,GAAOwN,KAAK0C,UAAU1a,EAAIT,KAAM2F,KAAKgV,WAElC1P,CACX,GACA,CAAArL,IAAA,iBAAA6J,MAKA,SAAehJ,GACX,IAAM2a,EAAiBvB,GAAkBpZ,GACnCuZ,EAAOrU,KAAKmV,eAAeM,EAAe1X,QAC1CoW,EAAUsB,EAAetB,QAE/B,OADAA,EAAQuB,QAAQrB,GACTF,CACX,KAACY,CAAA,CAnEe,GAsEpB,SAASY,GAAS7R,GACd,MAAiD,oBAA1ClK,OAAOY,UAAUC,SAASC,KAAKoJ,EAC1C,CAMa8R,IAAAA,YAAOrS,GAAAZ,EAAAiT,EAAArS,GAAA,IAAAX,EAAAC,EAAA+S,GAMhB,SAAAA,EAAYC,GAAS,IAAA5S,EAEM,OAFNC,OAAA0S,IACjB3S,EAAAL,EAAAlI,KAAAsF,OACK6V,QAAUA,EAAQ5S,CAC3B,CA4IC,OA3IDE,EAAAyS,EAAA,CAAA,CAAA3b,IAAA,MAAA6J,MAKA,SAAIhJ,GACA,IAAIiD,EACJ,GAAmB,iBAARjD,EAAkB,CACzB,GAAIkF,KAAK8V,cACL,MAAM,IAAIzS,MAAM,mDAGpB,IAAM0S,GADNhY,EAASiC,KAAKgW,aAAalb,IACEV,OAASya,GAAWQ,aAC7CU,GAAiBhY,EAAO3D,OAASya,GAAWS,YAC5CvX,EAAO3D,KAAO2b,EAAgBlB,GAAWI,MAAQJ,GAAWK,IAE5DlV,KAAK8V,cAAgB,IAAIG,GAAoBlY,GAElB,IAAvBA,EAAOwW,aACPxQ,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,IAKlCgG,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,EAErC,KACI,KAAI4P,GAAS7S,KAAQA,EAAIgC,OAe1B,MAAM,IAAIuG,MAAM,iBAAmBvI,GAbnC,IAAKkF,KAAK8V,cACN,MAAM,IAAIzS,MAAM,qDAGhBtF,EAASiC,KAAK8V,cAAcI,eAAepb,MAGvCkF,KAAK8V,cAAgB,KACrB/R,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,GAM1C,CACJ,GACA,CAAA9D,IAAA,eAAA6J,MAMA,SAAawB,GACT,IAAIpJ,EAAI,EAEFmB,EAAI,CACNjD,KAAMgL,OAAOE,EAAI7I,OAAO,KAE5B,QAA2BkI,IAAvBkQ,GAAWxX,EAAEjD,MACb,MAAM,IAAIiJ,MAAM,uBAAyBhG,EAAEjD,MAG/C,GAAIiD,EAAEjD,OAASya,GAAWQ,cACtBhY,EAAEjD,OAASya,GAAWS,WAAY,CAElC,IADA,IAAMa,EAAQja,EAAI,EACS,MAApBoJ,EAAI7I,SAASP,IAAcA,GAAKoJ,EAAI1I,SAC3C,IAAMwZ,EAAM9Q,EAAI3I,UAAUwZ,EAAOja,GACjC,GAAIka,GAAOhR,OAAOgR,IAA0B,MAAlB9Q,EAAI7I,OAAOP,GACjC,MAAM,IAAImH,MAAM,uBAEpBhG,EAAEkX,YAAcnP,OAAOgR,EAC3B,CAEA,GAAI,MAAQ9Q,EAAI7I,OAAOP,EAAI,GAAI,CAE3B,IADA,IAAMia,EAAQja,EAAI,IACTA,GAAG,CAER,GAAI,MADMoJ,EAAI7I,OAAOP,GAEjB,MACJ,GAAIA,IAAMoJ,EAAI1I,OACV,KACR,CACAS,EAAEkY,IAAMjQ,EAAI3I,UAAUwZ,EAAOja,EACjC,MAEImB,EAAEkY,IAAM,IAGZ,IAAMc,EAAO/Q,EAAI7I,OAAOP,EAAI,GAC5B,GAAI,KAAOma,GAAQjR,OAAOiR,IAASA,EAAM,CAErC,IADA,IAAMF,EAAQja,EAAI,IACTA,GAAG,CACR,IAAMkX,EAAI9N,EAAI7I,OAAOP,GACrB,GAAI,MAAQkX,GAAKhO,OAAOgO,IAAMA,EAAG,GAC3BlX,EACF,KACJ,CACA,GAAIA,IAAMoJ,EAAI1I,OACV,KACR,CACAS,EAAEgU,GAAKjM,OAAOE,EAAI3I,UAAUwZ,EAAOja,EAAI,GAC3C,CAEA,GAAIoJ,EAAI7I,SAASP,GAAI,CACjB,IAAMoa,EAAUtW,KAAKuW,SAASjR,EAAIkR,OAAOta,IACzC,IAAI0Z,EAAQa,eAAepZ,EAAEjD,KAAMkc,GAI/B,MAAM,IAAIjT,MAAM,mBAHhBhG,EAAEhD,KAAOic,CAKjB,CACA,OAAOjZ,CACX,GAAC,CAAApD,IAAA,WAAA6J,MACD,SAASwB,GACL,IACI,OAAOwN,KAAK5D,MAAM5J,EAAKtF,KAAK6V,QAC/B,CACD,MAAOvP,GACH,OAAO,CACX,CACJ,GAAC,CAAArM,IAAA,UAAA6J,MAuBD,WACQ9D,KAAK8V,gBACL9V,KAAK8V,cAAcY,yBACnB1W,KAAK8V,cAAgB,KAE7B,IAAC,CAAA,CAAA7b,IAAA,iBAAA6J,MA3BD,SAAsB1J,EAAMkc,GACxB,OAAQlc,GACJ,KAAKya,GAAW8B,QACZ,OAAOhB,GAASW,GACpB,KAAKzB,GAAW+B,WACZ,YAAmBjS,IAAZ2R,EACX,KAAKzB,GAAWgC,cACZ,MAA0B,iBAAZP,GAAwBX,GAASW,GACnD,KAAKzB,GAAWI,MAChB,KAAKJ,GAAWQ,aACZ,OAAQtU,MAAMkT,QAAQqC,KACK,iBAAfA,EAAQ,IACW,iBAAfA,EAAQ,KAC6B,IAAzCxB,GAAgB7P,QAAQqR,EAAQ,KAChD,KAAKzB,GAAWK,IAChB,KAAKL,GAAWS,WACZ,OAAOvU,MAAMkT,QAAQqC,GAEjC,KAACV,CAAA,EArJwBlW,GAwKvBuW,GAAmB,WACrB,SAAAA,EAAYlY,GAAQmF,OAAA+S,GAChBjW,KAAKjC,OAASA,EACdiC,KAAKmU,QAAU,GACfnU,KAAK8W,UAAY/Y,CACrB,CAyBC,OAxBDoF,EAAA8S,EAAA,CAAA,CAAAhc,IAAA,iBAAA6J,MAQA,SAAeiT,GAEX,GADA/W,KAAKmU,QAAQjU,KAAK6W,GACd/W,KAAKmU,QAAQvX,SAAWoD,KAAK8W,UAAUvC,YAAa,CAEpD,IAAMxW,EAAS4W,GAAkB3U,KAAK8W,UAAW9W,KAAKmU,SAEtD,OADAnU,KAAK0W,yBACE3Y,CACX,CACA,OAAO,IACX,GACA,CAAA9D,IAAA,yBAAA6J,MAGA,WACI9D,KAAK8W,UAAY,KACjB9W,KAAKmU,QAAU,EACnB,KAAC8B,CAAA,CA9BoB,6CApQD,sDCnBjB,SAASrW,GAAG9E,EAAK2R,EAAI1M,GAExB,OADAjF,EAAI8E,GAAG6M,EAAI1M,GACJ,WACHjF,EAAIsF,IAAIqM,EAAI1M,GAEpB,CCEA,IAAM+U,GAAkBlb,OAAOod,OAAO,CAClCC,QAAS,EACTC,cAAe,EACfC,WAAY,EACZC,cAAe,EAEfC,YAAa,EACb9W,eAAgB,IA0BP4P,YAAM5M,GAAAZ,EAAAwN,EAAA5M,GAAA,IAAAX,EAAAC,EAAAsN,GAIf,SAAAA,EAAYmH,EAAI/B,EAAKpT,GAAM,IAAAc,EAoDP,OApDOC,OAAAiN,IACvBlN,EAAAL,EAAAlI,KAAAsF,OAeKuX,WAAY,EAKjBtU,EAAKuU,WAAY,EAIjBvU,EAAKwU,cAAgB,GAIrBxU,EAAKyU,WAAa,GAOlBzU,EAAK0U,OAAS,GAKd1U,EAAK2U,UAAY,EACjB3U,EAAK4U,IAAM,EACX5U,EAAK6U,KAAO,GACZ7U,EAAK8U,MAAQ,GACb9U,EAAKqU,GAAKA,EACVrU,EAAKsS,IAAMA,EACPpT,GAAQA,EAAK6V,OACb/U,EAAK+U,KAAO7V,EAAK6V,MAErB/U,EAAKgV,MAAQpP,EAAc,CAAE,EAAE1G,GAC3Bc,EAAKqU,GAAGY,cACRjV,EAAK4G,OAAO5G,CACpB,CAmuBC,OAluBDE,EAAAgN,EAAA,CAAA,CAAAlW,IAAA,eAAAsN,IAcA,WACI,OAAQvH,KAAKuX,SACjB,GACA,CAAAtd,IAAA,YAAA6J,MAKA,WACI,IAAI9D,KAAKmY,KAAT,CAEA,IAAMb,EAAKtX,KAAKsX,GAChBtX,KAAKmY,KAAO,CACRvY,GAAG0X,EAAI,OAAQtX,KAAKkM,OAAO5J,KAAKtC,OAChCJ,GAAG0X,EAAI,SAAUtX,KAAKoY,SAAS9V,KAAKtC,OACpCJ,GAAG0X,EAAI,QAAStX,KAAK0M,QAAQpK,KAAKtC,OAClCJ,GAAG0X,EAAI,QAAStX,KAAKsM,QAAQhK,KAAKtC,OANlC,CAQR,GACA,CAAA/F,IAAA,SAAAsN,IAiBA,WACI,QAASvH,KAAKmY,IAClB,GACA,CAAAle,IAAA,UAAA6J,MAUA,WACI,OAAI9D,KAAKuX,YAETvX,KAAKqY,YACArY,KAAKsX,GAAkB,eACxBtX,KAAKsX,GAAGzN,OACR,SAAW7J,KAAKsX,GAAGgB,aACnBtY,KAAKkM,UALElM,IAOf,GACA,CAAA/F,IAAA,OAAA6J,MAGA,WACI,OAAO9D,KAAKiX,SAChB,GACA,CAAAhd,IAAA,OAAA6J,MAeA,WAAc,IAAA,IAAAtC,EAAAlB,UAAA1D,OAANkE,EAAIC,IAAAA,MAAAS,GAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAJZ,EAAIY,GAAApB,UAAAoB,GAGR,OAFAZ,EAAK4U,QAAQ,WACb1V,KAAKa,KAAKR,MAAML,KAAMc,GACfd,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAiBA,SAAK2I,GACD,GAAIqI,GAAgBlT,eAAe6K,GAC/B,MAAM,IAAIpJ,MAAM,IAAMoJ,EAAGhS,WAAa,8BACzC,IAAA8d,IAAAA,EAAAjY,UAAA1D,OAHOkE,MAAIC,MAAAwX,EAAAA,EAAAA,OAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJ1X,EAAI0X,EAAAlY,GAAAA,UAAAkY,GAKZ,GADA1X,EAAK4U,QAAQjJ,GACTzM,KAAKiY,MAAMQ,UAAYzY,KAAK+X,MAAMW,YAAc1Y,KAAK+X,eAErD,OADA/X,KAAK2Y,YAAY7X,GACVd,KAEX,IAAMjC,EAAS,CACX3D,KAAMya,GAAWI,MACjB5a,KAAMyG,EAEV/C,QAAiB,IAGjB,GAFAA,EAAOwV,QAAQC,UAAmC,IAAxBxT,KAAK+X,MAAMvE,SAEjC,mBAAsB1S,EAAKA,EAAKlE,OAAS,GAAI,CAC7C,IAAMyU,EAAKrR,KAAK6X,MACVe,EAAM9X,EAAK+X,MACjB7Y,KAAK8Y,qBAAqBzH,EAAIuH,GAC9B7a,EAAOsT,GAAKA,CAChB,CACA,IAAM0H,EAAsB/Y,KAAKsX,GAAG0B,QAChChZ,KAAKsX,GAAG0B,OAAOjM,WACf/M,KAAKsX,GAAG0B,OAAOjM,UAAUrJ,SAY7B,OAXsB1D,KAAK+X,MAAc,YAAMgB,IAAwB/Y,KAAKuX,aAGnEvX,KAAKuX,WACVvX,KAAKiZ,wBAAwBlb,GAC7BiC,KAAKjC,OAAOA,IAGZiC,KAAK0X,WAAWxX,KAAKnC,IAEzBiC,KAAK+X,MAAQ,GACN/X,IACX,GACA,CAAA/F,IAAA,uBAAA6J,MAGA,SAAqBuN,EAAIuH,GAAK,IACtBlP,EADsBjG,EAAAzD,KAEpBmK,EAAwC,QAA7BT,EAAK1J,KAAK+X,MAAM5N,eAA4B,IAAPT,EAAgBA,EAAK1J,KAAKiY,MAAMiB,WACtF,QAAgBvU,IAAZwF,EAAJ,CAKA,IAAMgP,EAAQnZ,KAAKsX,GAAGjV,cAAa,kBACxBoB,EAAKqU,KAAKzG,GACjB,IAAK,IAAInV,EAAI,EAAGA,EAAIuH,EAAKiU,WAAW9a,OAAQV,IACpCuH,EAAKiU,WAAWxb,GAAGmV,KAAOA,GAC1B5N,EAAKiU,WAAW9W,OAAO1E,EAAG,GAGlC0c,EAAIle,KAAK+I,EAAM,IAAIJ,MAAM,2BAC5B,GAAE8G,GACHnK,KAAK8X,KAAKzG,GAAM,WAEZ5N,EAAK6T,GAAG/U,eAAe4W,GAAO,IAAA,IAAAC,EAAA9Y,UAAA1D,OAFdkE,EAAIC,IAAAA,MAAAqY,GAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJvY,EAAIuY,GAAA/Y,UAAA+Y,GAGpBT,EAAIvY,MAAMoD,EAAI,CAAG,MAAI8C,OAAKzF,IAd9B,MAFId,KAAK8X,KAAKzG,GAAMuH,CAkBxB,GACA,CAAA3e,IAAA,cAAA6J,MAgBA,SAAY2I,GAAa,IAAA,IAAA7E,EAAA5H,KAAAsZ,EAAAhZ,UAAA1D,OAANkE,MAAIC,MAAAuY,EAAAA,EAAAA,OAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJzY,EAAIyY,EAAAjZ,GAAAA,UAAAiZ,GAEnB,IAAMC,OAAiC7U,IAAvB3E,KAAK+X,MAAM5N,cAAmDxF,IAA1B3E,KAAKiY,MAAMiB,WAC/D,OAAO,IAAI9N,SAAQ,SAACC,EAASoO,GACzB3Y,EAAKZ,MAAK,SAACwZ,EAAMC,GACb,OAAIH,EACOE,EAAOD,EAAOC,GAAQrO,EAAQsO,GAG9BtO,EAAQqO,EAEvB,IACA9R,EAAK/G,KAAIR,MAATuH,EAAU6E,CAAAA,GAAElG,OAAKzF,GACrB,GACJ,GACA,CAAA7G,IAAA,cAAA6J,MAKA,SAAYhD,GAAM,IACV8X,EADU1Q,EAAAlI,KAEuB,mBAA1Bc,EAAKA,EAAKlE,OAAS,KAC1Bgc,EAAM9X,EAAK+X,OAEf,IAAM9a,EAAS,CACXsT,GAAIrR,KAAK4X,YACTgC,SAAU,EACVC,SAAS,EACT/Y,KAAAA,EACAiX,MAAOlP,EAAc,CAAE6P,WAAW,GAAQ1Y,KAAK+X,QAEnDjX,EAAKZ,MAAK,SAACgG,GACP,GAAInI,IAAWmK,EAAKyP,OAAO,GAA3B,CAKA,GADyB,OAARzR,EAETnI,EAAO6b,SAAW1R,EAAK+P,MAAMQ,UAC7BvQ,EAAKyP,OAAOpY,QACRqZ,GACAA,EAAI1S,SAMZ,GADAgC,EAAKyP,OAAOpY,QACRqZ,EAAK,CAAA,IAAAkB,IAAAA,EAAAxZ,UAAA1D,OAhBEmd,MAAYhZ,MAAA+Y,EAAAA,EAAAA,OAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAZD,EAAYC,EAAA1Z,GAAAA,UAAA0Z,GAiBnBpB,EAAGvY,WAAC,EAAA,CAAA,MAAIkG,OAAKwT,GACjB,CAGJ,OADAhc,EAAO8b,SAAU,EACV3R,EAAK+R,aAjBZ,CAkBJ,IACAja,KAAK2X,OAAOzX,KAAKnC,GACjBiC,KAAKia,aACT,GACA,CAAAhgB,IAAA,cAAA6J,MAMA,WAA2B,IAAfoW,EAAK5Z,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,IAAAA,UAAA,GACb,GAAKN,KAAKuX,WAAoC,IAAvBvX,KAAK2X,OAAO/a,OAAnC,CAGA,IAAMmB,EAASiC,KAAK2X,OAAO,GACvB5Z,EAAO8b,UAAYK,IAGvBnc,EAAO8b,SAAU,EACjB9b,EAAO6b,WACP5Z,KAAK+X,MAAQha,EAAOga,MACpB/X,KAAKa,KAAKR,MAAML,KAAMjC,EAAO+C,MAR7B,CASJ,GACA,CAAA7G,IAAA,SAAA6J,MAMA,SAAO/F,GACHA,EAAOwX,IAAMvV,KAAKuV,IAClBvV,KAAKsX,GAAG6C,QAAQpc,EACpB,GACA,CAAA9D,IAAA,SAAA6J,MAKA,WAAS,IAAAsE,EAAApI,KACmB,mBAAbA,KAAKgY,KACZhY,KAAKgY,MAAK,SAAC3d,GACP+N,EAAKgS,mBAAmB/f,EAC5B,IAGA2F,KAAKoa,mBAAmBpa,KAAKgY,KAErC,GACA,CAAA/d,IAAA,qBAAA6J,MAMA,SAAmBzJ,GACf2F,KAAKjC,OAAO,CACR3D,KAAMya,GAAW8B,QACjBtc,KAAM2F,KAAKqa,KACLxR,EAAc,CAAEyR,IAAKta,KAAKqa,KAAME,OAAQva,KAAKwa,aAAengB,GAC5DA,GAEd,GACA,CAAAJ,IAAA,UAAA6J,MAMA,SAAQoC,GACClG,KAAKuX,WACNvX,KAAKgB,aAAa,gBAAiBkF,EAE3C,GACA,CAAAjM,IAAA,UAAA6J,MAOA,SAAQhB,EAAQC,GACZ/C,KAAKuX,WAAY,SACVvX,KAAKqR,GACZrR,KAAKgB,aAAa,aAAc8B,EAAQC,EAC5C,GACA,CAAA9I,IAAA,WAAA6J,MAMA,SAAS/F,GAEL,GADsBA,EAAOwX,MAAQvV,KAAKuV,IAG1C,OAAQxX,EAAO3D,MACX,KAAKya,GAAW8B,QACR5Y,EAAO1D,MAAQ0D,EAAO1D,KAAKqO,IAC3B1I,KAAKya,UAAU1c,EAAO1D,KAAKqO,IAAK3K,EAAO1D,KAAKigB,KAG5Cta,KAAKgB,aAAa,gBAAiB,IAAIqC,MAAM,8LAEjD,MACJ,KAAKwR,GAAWI,MAChB,KAAKJ,GAAWQ,aACZrV,KAAK0a,QAAQ3c,GACb,MACJ,KAAK8W,GAAWK,IAChB,KAAKL,GAAWS,WACZtV,KAAK2a,MAAM5c,GACX,MACJ,KAAK8W,GAAW+B,WACZ5W,KAAK4a,eACL,MACJ,KAAK/F,GAAWgC,cACZ7W,KAAK6a,UACL,IAAM3U,EAAM,IAAI7C,MAAMtF,EAAO1D,KAAKygB,SAElC5U,EAAI7L,KAAO0D,EAAO1D,KAAKA,KACvB2F,KAAKgB,aAAa,gBAAiBkF,GAG/C,GACA,CAAAjM,IAAA,UAAA6J,MAMA,SAAQ/F,GACJ,IAAM+C,EAAO/C,EAAO1D,MAAQ,GACxB,MAAQ0D,EAAOsT,IACfvQ,EAAKZ,KAAKF,KAAK4Y,IAAI7a,EAAOsT,KAE1BrR,KAAKuX,UACLvX,KAAK+a,UAAUja,GAGfd,KAAKyX,cAAcvX,KAAKtG,OAAOod,OAAOlW,GAE9C,GAAC,CAAA7G,IAAA,YAAA6J,MACD,SAAUhD,GACN,GAAId,KAAKgb,eAAiBhb,KAAKgb,cAAcpe,OAAQ,CACjD,IACgCqe,EADaC,EAAAC,EAA3Bnb,KAAKgb,cAAcvb,SACL,IAAhC,IAAAyb,EAAAE,MAAAH,EAAAC,EAAApN,KAAAc,MAAkC,CAAfqM,EAAAnX,MACNzD,MAAML,KAAMc,EACzB,CAAC,CAAA,MAAAoF,GAAAgV,EAAA5U,EAAAJ,EAAA,CAAA,QAAAgV,EAAAG,GAAA,CACL,CACAtX,EAAAC,EAAAmM,EAAA3V,WAAW6F,OAAAA,MAAAA,MAAML,KAAMc,GACnBd,KAAKqa,MAAQvZ,EAAKlE,QAA2C,iBAA1BkE,EAAKA,EAAKlE,OAAS,KACtDoD,KAAKwa,YAAc1Z,EAAKA,EAAKlE,OAAS,GAE9C,GACA,CAAA3C,IAAA,MAAA6J,MAKA,SAAIuN,GACA,IAAMjQ,EAAOpB,KACTsb,GAAO,EACX,OAAO,WAEH,IAAIA,EAAJ,CAEAA,GAAO,EAAK,IAAA,IAAAC,EAAAjb,UAAA1D,OAJIkE,EAAIC,IAAAA,MAAAwa,GAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJ1a,EAAI0a,GAAAlb,UAAAkb,GAKpBpa,EAAKrD,OAAO,CACR3D,KAAMya,GAAWK,IACjB7D,GAAIA,EACJhX,KAAMyG,GALN,EAQZ,GACA,CAAA7G,IAAA,QAAA6J,MAMA,SAAM/F,GACF,IAAM6a,EAAM5Y,KAAK8X,KAAK/Z,EAAOsT,IACzB,mBAAsBuH,IACtBA,EAAIvY,MAAML,KAAMjC,EAAO1D,aAChB2F,KAAK8X,KAAK/Z,EAAOsT,IAIhC,GACA,CAAApX,IAAA,YAAA6J,MAKA,SAAUuN,EAAIiJ,GACVta,KAAKqR,GAAKA,EACVrR,KAAKwX,UAAY8C,GAAOta,KAAKqa,OAASC,EACtCta,KAAKqa,KAAOC,EACZta,KAAKuX,WAAY,EACjBvX,KAAKyb,eACLzb,KAAKgB,aAAa,WAClBhB,KAAKia,aAAY,EACrB,GACA,CAAAhgB,IAAA,eAAA6J,MAKA,WAAe,IAAAkF,EAAAhJ,KACXA,KAAKyX,cAAczd,SAAQ,SAAC8G,GAAI,OAAKkI,EAAK+R,UAAUja,MACpDd,KAAKyX,cAAgB,GACrBzX,KAAK0X,WAAW1d,SAAQ,SAAC+D,GACrBiL,EAAKiQ,wBAAwBlb,GAC7BiL,EAAKjL,OAAOA,EAChB,IACAiC,KAAK0X,WAAa,EACtB,GACA,CAAAzd,IAAA,eAAA6J,MAKA,WACI9D,KAAK6a,UACL7a,KAAKsM,QAAQ,uBACjB,GACA,CAAArS,IAAA,UAAA6J,MAOA,WACQ9D,KAAKmY,OAELnY,KAAKmY,KAAKne,SAAQ,SAAC0hB,GAAU,OAAKA,OAClC1b,KAAKmY,UAAOxT,GAEhB3E,KAAKsX,GAAa,SAAEtX,KACxB,GACA,CAAA/F,IAAA,aAAA6J,MAgBA,WAUI,OATI9D,KAAKuX,WACLvX,KAAKjC,OAAO,CAAE3D,KAAMya,GAAW+B,aAGnC5W,KAAK6a,UACD7a,KAAKuX,WAELvX,KAAKsM,QAAQ,wBAEVtM,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAKA,WACI,OAAO9D,KAAKmX,YAChB,GACA,CAAAld,IAAA,WAAA6J,MASA,SAAS0P,GAEL,OADAxT,KAAK+X,MAAMvE,SAAWA,EACfxT,IACX,GACA,CAAA/F,IAAA,WAAAsN,IASA,WAEI,OADAvH,KAAK+X,MAAc,UAAG,EACf/X,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAaA,SAAQqG,GAEJ,OADAnK,KAAK+X,MAAM5N,QAAUA,EACdnK,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAWA,SAAM6X,GAGF,OAFA3b,KAAKgb,cAAgBhb,KAAKgb,eAAiB,GAC3Chb,KAAKgb,cAAc9a,KAAKyb,GACjB3b,IACX,GACA,CAAA/F,IAAA,aAAA6J,MAWA,SAAW6X,GAGP,OAFA3b,KAAKgb,cAAgBhb,KAAKgb,eAAiB,GAC3Chb,KAAKgb,cAActF,QAAQiG,GACpB3b,IACX,GACA,CAAA/F,IAAA,SAAA6J,MAkBA,SAAO6X,GACH,IAAK3b,KAAKgb,cACN,OAAOhb,KAEX,GAAI2b,GAEA,IADA,IAAM1a,EAAYjB,KAAKgb,cACd9e,EAAI,EAAGA,EAAI+E,EAAUrE,OAAQV,IAClC,GAAIyf,IAAa1a,EAAU/E,GAEvB,OADA+E,EAAUL,OAAO1E,EAAG,GACb8D,UAKfA,KAAKgb,cAAgB,GAEzB,OAAOhb,IACX,GACA,CAAA/F,IAAA,eAAA6J,MAIA,WACI,OAAO9D,KAAKgb,eAAiB,EACjC,GACA,CAAA/gB,IAAA,gBAAA6J,MAaA,SAAc6X,GAGV,OAFA3b,KAAK4b,sBAAwB5b,KAAK4b,uBAAyB,GAC3D5b,KAAK4b,sBAAsB1b,KAAKyb,GACzB3b,IACX,GACA,CAAA/F,IAAA,qBAAA6J,MAaA,SAAmB6X,GAGf,OAFA3b,KAAK4b,sBAAwB5b,KAAK4b,uBAAyB,GAC3D5b,KAAK4b,sBAAsBlG,QAAQiG,GAC5B3b,IACX,GACA,CAAA/F,IAAA,iBAAA6J,MAkBA,SAAe6X,GACX,IAAK3b,KAAK4b,sBACN,OAAO5b,KAEX,GAAI2b,GAEA,IADA,IAAM1a,EAAYjB,KAAK4b,sBACd1f,EAAI,EAAGA,EAAI+E,EAAUrE,OAAQV,IAClC,GAAIyf,IAAa1a,EAAU/E,GAEvB,OADA+E,EAAUL,OAAO1E,EAAG,GACb8D,UAKfA,KAAK4b,sBAAwB,GAEjC,OAAO5b,IACX,GACA,CAAA/F,IAAA,uBAAA6J,MAIA,WACI,OAAO9D,KAAK4b,uBAAyB,EACzC,GACA,CAAA3hB,IAAA,0BAAA6J,MAOA,SAAwB/F,GACpB,GAAIiC,KAAK4b,uBAAyB5b,KAAK4b,sBAAsBhf,OAAQ,CACjE,IACgCif,EADqBC,EAAAX,EAAnCnb,KAAK4b,sBAAsBnc,SACb,IAAhC,IAAAqc,EAAAV,MAAAS,EAAAC,EAAAhO,KAAAc,MAAkC,CAAfiN,EAAA/X,MACNzD,MAAML,KAAMjC,EAAO1D,KAChC,CAAC,CAAA,MAAA6L,GAAA4V,EAAAxV,EAAAJ,EAAA,CAAA,QAAA4V,EAAAT,GAAA,CACL,CACJ,KAAClL,CAAA,EA5xBuBzQ,GC7BrB,SAASqc,GAAQ5Z,GACpBA,EAAOA,GAAQ,GACfnC,KAAKgc,GAAK7Z,EAAK8Z,KAAO,IACtBjc,KAAKkc,IAAM/Z,EAAK+Z,KAAO,IACvBlc,KAAKmc,OAASha,EAAKga,QAAU,EAC7Bnc,KAAKoc,OAASja,EAAKia,OAAS,GAAKja,EAAKia,QAAU,EAAIja,EAAKia,OAAS,EAClEpc,KAAKqc,SAAW,CACpB,CAOAN,GAAQvhB,UAAU8hB,SAAW,WACzB,IAAIN,EAAKhc,KAAKgc,GAAKpW,KAAKoI,IAAIhO,KAAKmc,OAAQnc,KAAKqc,YAC9C,GAAIrc,KAAKoc,OAAQ,CACb,IAAIG,EAAO3W,KAAK4W,SACZC,EAAY7W,KAAKC,MAAM0W,EAAOvc,KAAKoc,OAASJ,GAChDA,EAAoC,IAAN,EAAxBpW,KAAKC,MAAa,GAAP0W,IAAuBP,EAAKS,EAAYT,EAAKS,CAClE,CACA,OAAgC,EAAzB7W,KAAKqW,IAAID,EAAIhc,KAAKkc,IAC7B,EAMAH,GAAQvhB,UAAUkiB,MAAQ,WACtB1c,KAAKqc,SAAW,CACpB,EAMAN,GAAQvhB,UAAUmiB,OAAS,SAAUV,GACjCjc,KAAKgc,GAAKC,CACd,EAMAF,GAAQvhB,UAAUoiB,OAAS,SAAUV,GACjClc,KAAKkc,IAAMA,CACf,EAMAH,GAAQvhB,UAAUqiB,UAAY,SAAUT,GACpCpc,KAAKoc,OAASA,CAClB,EC3DaU,IAAAA,YAAOvZ,GAAAZ,EAAAma,EAAAvZ,GAAA,IAAAX,EAAAC,EAAAia,GAChB,SAAAA,EAAY/T,EAAK5G,GAAM,IAAAc,EACfyG,EADexG,OAAA4Z,IAEnB7Z,EAAAL,EAAAlI,KAAAsF,OACK+c,KAAO,GACZ9Z,EAAKkV,KAAO,GACRpP,GAAO,WAAQsH,EAAYtH,KAC3B5G,EAAO4G,EACPA,OAAMpE,IAEVxC,EAAOA,GAAQ,IACV2C,KAAO3C,EAAK2C,MAAQ,aACzB7B,EAAKd,KAAOA,EACZD,EAAqByB,EAAAV,GAAOd,GAC5Bc,EAAK+Z,cAAmC,IAAtB7a,EAAK6a,cACvB/Z,EAAKga,qBAAqB9a,EAAK8a,sBAAwBC,KACvDja,EAAKka,kBAAkBhb,EAAKgb,mBAAqB,KACjDla,EAAKma,qBAAqBjb,EAAKib,sBAAwB,KACvDna,EAAKoa,oBAAwD,QAAnC3T,EAAKvH,EAAKkb,2BAAwC,IAAP3T,EAAgBA,EAAK,IAC1FzG,EAAKqa,QAAU,IAAIvB,GAAQ,CACvBE,IAAKhZ,EAAKka,oBACVjB,IAAKjZ,EAAKma,uBACVhB,OAAQnZ,EAAKoa,wBAEjBpa,EAAKkH,QAAQ,MAAQhI,EAAKgI,QAAU,IAAQhI,EAAKgI,SACjDlH,EAAKqV,YAAc,SACnBrV,EAAK8F,IAAMA,EACX,IAAMwU,EAAUpb,EAAKqb,QAAUA,GAKf,OAJhBva,EAAKwa,QAAU,IAAIF,EAAQxI,QAC3B9R,EAAKya,QAAU,IAAIH,EAAQ3H,QAC3B3S,EAAKiV,cAAoC,IAArB/V,EAAKwb,YACrB1a,EAAKiV,cACLjV,EAAK4G,OAAO5G,CACpB,CA6TC,OA7TAE,EAAA2Z,EAAA,CAAA,CAAA7iB,IAAA,eAAA6J,MACD,SAAa8Z,GACT,OAAKtd,UAAU1D,QAEfoD,KAAK6d,gBAAkBD,EAChB5d,MAFIA,KAAK6d,aAGpB,GAAC,CAAA5jB,IAAA,uBAAA6J,MACD,SAAqB8Z,GACjB,YAAUjZ,IAANiZ,EACO5d,KAAK8d,uBAChB9d,KAAK8d,sBAAwBF,EACtB5d,KACX,GAAC,CAAA/F,IAAA,oBAAA6J,MACD,SAAkB8Z,GACd,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAK+d,oBAChB/d,KAAK+d,mBAAqBH,EACF,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGiT,OAAOiB,GAC5D5d,KACX,GAAC,CAAA/F,IAAA,sBAAA6J,MACD,SAAoB8Z,GAChB,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAKge,sBAChBhe,KAAKge,qBAAuBJ,EACJ,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGmT,UAAUe,GAC/D5d,KACX,GAAC,CAAA/F,IAAA,uBAAA6J,MACD,SAAqB8Z,GACjB,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAKie,uBAChBje,KAAKie,sBAAwBL,EACL,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGkT,OAAOgB,GAC5D5d,KACX,GAAC,CAAA/F,IAAA,UAAA6J,MACD,SAAQ8Z,GACJ,OAAKtd,UAAU1D,QAEfoD,KAAKke,SAAWN,EACT5d,MAFIA,KAAKke,QAGpB,GACA,CAAAjkB,IAAA,uBAAA6J,MAMA,YAES9D,KAAKme,eACNne,KAAK6d,eACqB,IAA1B7d,KAAKsd,QAAQjB,UAEbrc,KAAKoe,WAEb,GACA,CAAAnkB,IAAA,OAAA6J,MAOA,SAAK/D,GAAI,IAAA0D,EAAAzD,KACL,IAAKA,KAAKsY,YAAYrT,QAAQ,QAC1B,OAAOjF,KACXA,KAAKgZ,OAAS,IAAIqF,GAAOre,KAAK+I,IAAK/I,KAAKmC,MACxC,IAAM0B,EAAS7D,KAAKgZ,OACd5X,EAAOpB,KACbA,KAAKsY,YAAc,UACnBtY,KAAKse,eAAgB,EAErB,IAAMC,EAAiB3e,GAAGiE,EAAQ,QAAQ,WACtCzC,EAAK8K,SACLnM,GAAMA,GACV,IACMsJ,EAAU,SAACnD,GACbzC,EAAKoH,UACLpH,EAAK6U,YAAc,SACnB7U,EAAKzC,aAAa,QAASkF,GACvBnG,EACAA,EAAGmG,GAIHzC,EAAK+a,wBAIPC,EAAW7e,GAAGiE,EAAQ,QAASwF,GACrC,IAAI,IAAUrJ,KAAKke,SAAU,CACzB,IAAM/T,EAAUnK,KAAKke,SAEf/E,EAAQnZ,KAAKqC,cAAa,WAC5Bkc,IACAlV,EAAQ,IAAIhG,MAAM,YAClBQ,EAAOsE,OACV,GAAEgC,GACCnK,KAAKmC,KAAKgK,WACVgN,EAAM9M,QAEVrM,KAAKmY,KAAKjY,MAAK,WACXuD,EAAKlB,eAAe4W,EACxB,GACJ,CAGA,OAFAnZ,KAAKmY,KAAKjY,KAAKqe,GACfve,KAAKmY,KAAKjY,KAAKue,GACRze,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAMA,SAAQ/D,GACJ,OAAOC,KAAK6J,KAAK9J,EACrB,GACA,CAAA9F,IAAA,SAAA6J,MAKA,WAEI9D,KAAK6K,UAEL7K,KAAKsY,YAAc,OACnBtY,KAAKgB,aAAa,QAElB,IAAM6C,EAAS7D,KAAKgZ,OACpBhZ,KAAKmY,KAAKjY,KAAKN,GAAGiE,EAAQ,OAAQ7D,KAAK0e,OAAOpc,KAAKtC,OAAQJ,GAAGiE,EAAQ,OAAQ7D,KAAK2e,OAAOrc,KAAKtC,OAAQJ,GAAGiE,EAAQ,QAAS7D,KAAK0M,QAAQpK,KAAKtC,OAAQJ,GAAGiE,EAAQ,QAAS7D,KAAKsM,QAAQhK,KAAKtC,OAAQJ,GAAGI,KAAK0d,QAAS,UAAW1d,KAAK4e,UAAUtc,KAAKtC,OACvP,GACA,CAAA/F,IAAA,SAAA6J,MAKA,WACI9D,KAAKgB,aAAa,OACtB,GACA,CAAA/G,IAAA,SAAA6J,MAKA,SAAOzJ,GACH,IACI2F,KAAK0d,QAAQmB,IAAIxkB,EACpB,CACD,MAAOiM,GACHtG,KAAKsM,QAAQ,cAAehG,EAChC,CACJ,GACA,CAAArM,IAAA,YAAA6J,MAKA,SAAU/F,GAAQ,IAAA6J,EAAA5H,KAEdmL,IAAS,WACLvD,EAAK5G,aAAa,SAAUjD,EAChC,GAAGiC,KAAKqC,aACZ,GACA,CAAApI,IAAA,UAAA6J,MAKA,SAAQoC,GACJlG,KAAKgB,aAAa,QAASkF,EAC/B,GACA,CAAAjM,IAAA,SAAA6J,MAMA,SAAOyR,EAAKpT,GACR,IAAI0B,EAAS7D,KAAK+c,KAAKxH,GAQvB,OAPK1R,EAII7D,KAAKkY,eAAiBrU,EAAOib,QAClCjb,EAAOoT,WAJPpT,EAAS,IAAIsM,GAAOnQ,KAAMuV,EAAKpT,GAC/BnC,KAAK+c,KAAKxH,GAAO1R,GAKdA,CACX,GACA,CAAA5J,IAAA,WAAA6J,MAMA,SAASD,GAEL,IADA,IACAkb,EAAA,EAAAC,EADaplB,OAAOG,KAAKiG,KAAK+c,MACRgC,EAAAC,EAAApiB,OAAAmiB,IAAE,CAAnB,IAAMxJ,EAAGyJ,EAAAD,GAEV,GADe/e,KAAK+c,KAAKxH,GACduJ,OACP,MAER,CACA9e,KAAKif,QACT,GACA,CAAAhlB,IAAA,UAAA6J,MAMA,SAAQ/F,GAEJ,IADA,IAAM+J,EAAiB9H,KAAKyd,QAAQpf,OAAON,GAClC7B,EAAI,EAAGA,EAAI4L,EAAelL,OAAQV,IACvC8D,KAAKgZ,OAAO1U,MAAMwD,EAAe5L,GAAI6B,EAAOwV,QAEpD,GACA,CAAAtZ,IAAA,UAAA6J,MAKA,WACI9D,KAAKmY,KAAKne,SAAQ,SAAC0hB,GAAU,OAAKA,OAClC1b,KAAKmY,KAAKvb,OAAS,EACnBoD,KAAK0d,QAAQ7C,SACjB,GACA,CAAA5gB,IAAA,SAAA6J,MAKA,WACI9D,KAAKse,eAAgB,EACrBte,KAAKme,eAAgB,EACrBne,KAAKsM,QAAQ,gBACTtM,KAAKgZ,QACLhZ,KAAKgZ,OAAO7Q,OACpB,GACA,CAAAlO,IAAA,aAAA6J,MAKA,WACI,OAAO9D,KAAKif,QAChB,GACA,CAAAhlB,IAAA,UAAA6J,MAKA,SAAQhB,EAAQC,GACZ/C,KAAK6K,UACL7K,KAAKsd,QAAQZ,QACb1c,KAAKsY,YAAc,SACnBtY,KAAKgB,aAAa,QAAS8B,EAAQC,GAC/B/C,KAAK6d,gBAAkB7d,KAAKse,eAC5Bte,KAAKoe,WAEb,GACA,CAAAnkB,IAAA,YAAA6J,MAKA,WAAY,IAAAoE,EAAAlI,KACR,GAAIA,KAAKme,eAAiBne,KAAKse,cAC3B,OAAOte,KACX,IAAMoB,EAAOpB,KACb,GAAIA,KAAKsd,QAAQjB,UAAYrc,KAAK8d,sBAC9B9d,KAAKsd,QAAQZ,QACb1c,KAAKgB,aAAa,oBAClBhB,KAAKme,eAAgB,MAEpB,CACD,IAAMe,EAAQlf,KAAKsd,QAAQhB,WAC3Btc,KAAKme,eAAgB,EACrB,IAAMhF,EAAQnZ,KAAKqC,cAAa,WACxBjB,EAAKkd,gBAETpW,EAAKlH,aAAa,oBAAqBI,EAAKkc,QAAQjB,UAEhDjb,EAAKkd,eAETld,EAAKyI,MAAK,SAAC3D,GACHA,GACA9E,EAAK+c,eAAgB,EACrB/c,EAAKgd,YACLlW,EAAKlH,aAAa,kBAAmBkF,IAGrC9E,EAAK+d,aAEb,IACH,GAAED,GACClf,KAAKmC,KAAKgK,WACVgN,EAAM9M,QAEVrM,KAAKmY,KAAKjY,MAAK,WACXgI,EAAK3F,eAAe4W,EACxB,GACJ,CACJ,GACA,CAAAlf,IAAA,cAAA6J,MAKA,WACI,IAAMsb,EAAUpf,KAAKsd,QAAQjB,SAC7Brc,KAAKme,eAAgB,EACrBne,KAAKsd,QAAQZ,QACb1c,KAAKgB,aAAa,YAAaoe,EACnC,KAACtC,CAAA,EA9VwBpd,GCAvB2f,GAAQ,CAAA,EACd,SAASpjB,GAAO8M,EAAK5G,GACE,WAAfkO,EAAOtH,KACP5G,EAAO4G,EACPA,OAAMpE,GAGV,IASI2S,EATEgI,ECHH,SAAavW,GAAqB,IAAhBjE,EAAIxE,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,GAAIif,EAAGjf,UAAA1D,OAAA0D,EAAAA,kBAAAqE,EAC/B7J,EAAMiO,EAEVwW,EAAMA,GAA4B,oBAAbxY,UAA4BA,SAC7C,MAAQgC,IACRA,EAAMwW,EAAItY,SAAW,KAAOsY,EAAI9P,MAEjB,iBAAR1G,IACH,MAAQA,EAAItM,OAAO,KAEfsM,EADA,MAAQA,EAAItM,OAAO,GACb8iB,EAAItY,SAAW8B,EAGfwW,EAAI9P,KAAO1G,GAGpB,sBAAsByW,KAAKzW,KAExBA,OADA,IAAuBwW,EACjBA,EAAItY,SAAW,KAAO8B,EAGtB,WAAaA,GAI3BjO,EAAMoU,GAAMnG,IAGXjO,EAAIoK,OACD,cAAcsa,KAAK1kB,EAAImM,UACvBnM,EAAIoK,KAAO,KAEN,eAAesa,KAAK1kB,EAAImM,YAC7BnM,EAAIoK,KAAO,QAGnBpK,EAAIgK,KAAOhK,EAAIgK,MAAQ,IACvB,IACM2K,GADkC,IAA3B3U,EAAI2U,KAAKxK,QAAQ,KACV,IAAMnK,EAAI2U,KAAO,IAAM3U,EAAI2U,KAS/C,OAPA3U,EAAIuW,GAAKvW,EAAImM,SAAW,MAAQwI,EAAO,IAAM3U,EAAIoK,KAAOJ,EAExDhK,EAAI2kB,KACA3kB,EAAImM,SACA,MACAwI,GACC8P,GAAOA,EAAIra,OAASpK,EAAIoK,KAAO,GAAK,IAAMpK,EAAIoK,MAChDpK,CACX,CD7CmB4kB,CAAI3W,GADnB5G,EAAOA,GAAQ,IACc2C,MAAQ,cAC/B0K,EAAS8P,EAAO9P,OAChB6B,EAAKiO,EAAOjO,GACZvM,EAAOwa,EAAOxa,KACd6a,EAAgBN,GAAMhO,IAAOvM,KAAQua,GAAMhO,GAAU,KAkB3D,OAjBsBlP,EAAKyd,UACvBzd,EAAK,0BACL,IAAUA,EAAK0d,WACfF,EAGArI,EAAK,IAAIwF,GAAQtN,EAAQrN,IAGpBkd,GAAMhO,KACPgO,GAAMhO,GAAM,IAAIyL,GAAQtN,EAAQrN,IAEpCmV,EAAK+H,GAAMhO,IAEXiO,EAAO1b,QAAUzB,EAAKyB,QACtBzB,EAAKyB,MAAQ0b,EAAOvP,UAEjBuH,EAAGzT,OAAOyb,EAAOxa,KAAM3C,EAClC,QAGA0G,EAAc5M,GAAQ,CAClB6gB,QAAAA,GACA3M,OAAAA,GACAmH,GAAIrb,GACJgb,QAAShb"} \ No newline at end of file diff --git a/flask__websocket/ajax_vs_websocket/ajax/main.py b/flask__websocket/ajax_vs_websocket/ajax/main.py index dd2539aba..d50cd8d0d 100644 --- a/flask__websocket/ajax_vs_websocket/ajax/main.py +++ b/flask__websocket/ajax_vs_websocket/ajax/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from timeit import default_timer @@ -9,50 +9,48 @@ from flask import Flask, render_template, request, jsonify -app = Flask(__name__, static_folder='../_static') -app.config['SECRET_KEY'] = 'secret!' + +app = Flask(__name__, static_folder="../_static") +app.config["SECRET_KEY"] = "secret!" lock = Lock() DATA = { - 'count': 0, - 'start_time': 0.0 + "count": 0, + "start_time": 0.0, } -@app.route('/') +@app.route("/") def index(): with lock: - DATA['count'] = 0 + DATA["count"] = 0 - return render_template('index.html') + return render_template("index.html") -@app.route("/post_method", methods=['POST']) +@app.route("/post_method", methods=["POST"]) def post_method(): data = request.get_json() # print(data) with lock: - if DATA['count'] == 0: - DATA['start_time'] = default_timer() + if DATA["count"] == 0: + DATA["start_time"] = default_timer() - DATA['count'] += 1 + DATA["count"] += 1 return jsonify({ - 'number': data['number'], - 'count': DATA['count'], - 'elapsed': round(default_timer() - DATA['start_time'], 3) + "number": data["number"], + "count": DATA["count"], + "elapsed": round(default_timer() - DATA["start_time"], 3), }) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost - HOST = '127.0.0.1' + HOST = "127.0.0.1" PORT = 12000 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") app.debug = True - app.run( - host=HOST, - port=PORT - ) + app.run(host=HOST, port=PORT) diff --git a/flask__websocket/ajax_vs_websocket/websocket/main.py b/flask__websocket/ajax_vs_websocket/websocket/main.py index 5a707a378..f39e0d5b1 100644 --- a/flask__websocket/ajax_vs_websocket/websocket/main.py +++ b/flask__websocket/ajax_vs_websocket/websocket/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from timeit import default_timer @@ -11,6 +11,7 @@ from flask_socketio import SocketIO, emit from engineio.payload import Payload + Payload.max_decode_packets = 1000 @@ -19,54 +20,55 @@ # the best option based on installed packages. async_mode = None -app = Flask(__name__, static_folder='../_static') -app.config['SECRET_KEY'] = 'secret!' +app = Flask(__name__, static_folder="../_static") +app.config["SECRET_KEY"] = "secret!" socketio = SocketIO(app, async_mode=async_mode) lock = Lock() DATA = { - 'count': 0, - 'start_time': 0.0 + "count": 0, + "start_time": 0.0, } -@app.route('/') +@app.route("/") def index(): with lock: - DATA['count'] = 0 + DATA["count"] = 0 - return render_template('index.html', async_mode=socketio.async_mode) + return render_template("index.html", async_mode=socketio.async_mode) -@socketio.on('post_method', namespace='/test') -def post_method(message): +@socketio.on("post_method", namespace="/test") +def post_method(message) -> None: # print(message) with lock: - if DATA['count'] == 0: - DATA['start_time'] = default_timer() + if DATA["count"] == 0: + DATA["start_time"] = default_timer() - DATA['count'] += 1 + DATA["count"] += 1 emit( - 'my_response', + "my_response", { - 'number': message['number'], - 'count': DATA['count'], - 'elapsed': round(default_timer() - DATA['start_time'], 3) - } + "number": message["number"], + "count": DATA["count"], + "elapsed": round(default_timer() - DATA["start_time"], 3), + }, ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost - HOST = '127.0.0.1' + HOST = "127.0.0.1" PORT = 12001 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") + app.debug = True socketio.run( app, - debug=True, host=HOST, port=PORT, + allow_unsafe_werkzeug=True, ) diff --git a/flask__websocket/ajax_vs_websocket/websocket/templates/index.html b/flask__websocket/ajax_vs_websocket/websocket/templates/index.html index eb2e246e0..dfa8e0d06 100644 --- a/flask__websocket/ajax_vs_websocket/websocket/templates/index.html +++ b/flask__websocket/ajax_vs_websocket/websocket/templates/index.html @@ -3,7 +3,7 @@ Ajax vs Websocket. Test Websocket - +

Ajax vs Websocket. Test Websocket

diff --git a/flask__websocket/commands__websocket__flask-socketio/main.py b/flask__websocket/commands__websocket__flask-socketio/main.py index fea7112d0..4324325e4 100644 --- a/flask__websocket/commands__websocket__flask-socketio/main.py +++ b/flask__websocket/commands__websocket__flask-socketio/main.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import uuid +from datetime import datetime + from flask import Flask, render_template, session, request from flask_socketio import SocketIO, emit @@ -14,62 +17,61 @@ async_mode = None app = Flask(__name__) -app.config['SECRET_KEY'] = 'secret!' +app.config["SECRET_KEY"] = "secret!" socketio = SocketIO(app, async_mode=async_mode) -@app.route('/') +@app.route("/") def index(): - return render_template('index.html', async_mode=socketio.async_mode) + return render_template("index.html", async_mode=socketio.async_mode) -@socketio.on('my_event', namespace='/test') -def test_message(message): - session['receive_count'] = session.get('receive_count', 0) + 1 +@socketio.on("my_event", namespace="/test") +def test_message(message) -> None: + session["receive_count"] = session.get("receive_count", 0) + 1 print(message) - data = message['data'] + data = message["data"] if data == "CURRENT_DATE_TIME": - import datetime as DT - response = DT.datetime.now().strftime('%d%m%y_%H%M%S') + response = datetime.now().strftime("%Y-%m-%d_%H%M%S") elif data == "UUID": - import uuid response = str(uuid.uuid4()) elif data.startswith("CALC"): + # TODO: don't use in production! response = str(eval(data[4:])) else: response = data emit( - 'my_response', - {'data': response, 'count': session['receive_count']} + "my_response", + {"data": response, "count": session["receive_count"]}, ) -@socketio.on('my_ping', namespace='/test') -def ping_pong(): - emit('my_pong') +@socketio.on("my_ping", namespace="/test") +def ping_pong() -> None: + emit("my_pong") -@socketio.on('connect', namespace='/test') -def test_connect(): - emit('my_response', {'data': 'Connected'}) +@socketio.on("connect", namespace="/test") +def test_connect() -> None: + emit("my_response", {"data": "Connected"}) -@socketio.on('disconnect', namespace='/test') -def test_disconnect(): - print('Client disconnected', request.sid) +@socketio.on("disconnect", namespace="/test") +def test_disconnect() -> None: + print("Client disconnected", request.sid) -if __name__ == '__main__': - HOST = '127.0.0.1' +if __name__ == "__main__": + HOST = "127.0.0.1" PORT = 12000 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") socketio.run( app, diff --git a/flask__websocket/sending_all_from_while_in_thread/main.py b/flask__websocket/sending_all_from_while_in_thread/main.py new file mode 100644 index 000000000..fce7334c0 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/main.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +from datetime import datetime +from threading import Thread + +from flask import Flask, render_template, session, request + +# pip install flask-socketio==5.3.6 +from flask_socketio import SocketIO, emit + + +# Set this variable to "threading", "eventlet" or "gevent" to test the +# different async modes, or leave it set to None for the application to choose +# the best option based on installed packages. +async_mode = None + +app = Flask(__name__) +app.config["SECRET_KEY"] = "secret!" +socketio = SocketIO(app, async_mode=async_mode) + + +def send_all_cycled() -> None: + with app.app_context(): + i = 0 + while True: + i += 1 + emit( + "my_response_all", + {"data": f"#{i}. {datetime.now().isoformat()}"}, + broadcast=True, # Send all clients + namespace="/", + ) + time.sleep(1) + + +thread = Thread(target=send_all_cycled) +thread.daemon = True +thread.start() + + +@app.route("/") +def index(): + return render_template("index.html") + + +@socketio.on("my_event") +def test_message(message) -> None: + session["receive_count"] = session.get("receive_count", 0) + 1 + print(message) + + response = message["data"] + emit( + "my_response", + {"data": response, "count": session["receive_count"], "sid": request.sid}, + broadcast=True, # Send all clients + ) + + +@socketio.on("my_ping") +def ping_pong() -> None: + emit("my_pong") + + +@socketio.on("connect") +def test_connect() -> None: + print("Client connected", request.sid) + emit("my_response", {"data": f"Connected!", "count": 0, "sid": request.sid}) + + +@socketio.on("disconnect") +def test_disconnect() -> None: + print("Client disconnected", request.sid) + + +if __name__ == "__main__": + # HOST = '127.0.0.1' + # HOST = "0.0.0.0" + PORT = 12000 + # print(f"http://{HOST}:{PORT}") + + socketio.run( + app, + # host=HOST, + port=PORT, + allow_unsafe_werkzeug=True, + ) diff --git a/flask__websocket/sending_all_from_while_in_thread/static/index.js b/flask__websocket/sending_all_from_while_in_thread/static/index.js new file mode 100644 index 000000000..d94c761e9 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/static/index.js @@ -0,0 +1,62 @@ +$(document).ready(function() { + // Connect to the Socket.IO server. + var socket = io(); + + // Event handler for new connections. + // The callback function is invoked when a connection with the + // server is established. + socket.on('connect', function() { + socket.emit('my_event', {data: "I'm connected!"}); + }); + + // Event handler for server sent data. + // The callback function is invoked whenever the server emits data + // to the client. The data is then displayed in the "Received" + // section of the page. + socket.on('my_response', function(msg, cb) { + console.log(msg); + $('#log').prepend( + '
' + $('
').text(`Received #${msg.count}: ${msg.data} from #${msg.sid}`).html() + ); + if (cb) + cb(); + }); + + socket.on('my_response_all', function(msg, cb) { + console.log(msg); + $('#server_data').text(msg.data); + if (cb) + cb(); + }); + + // Interval function that tests message latency by sending a "ping" + // message. The server then responds with a "pong" message and the + // round trip time is measured. + var ping_pong_times = []; + var start_time; + window.setInterval(function() { + start_time = (new Date).getTime(); + socket.emit('my_ping'); + }, 1000); + + // Handler for the "pong" message. When the pong is received, the + // time from the ping is stored, and the average of the last 30 + // samples is average and displayed. + socket.on('my_pong', function() { + var latency = (new Date).getTime() - start_time; + ping_pong_times.push(latency); + ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples + var sum = 0; + for (var i = 0; i < ping_pong_times.length; i++) + sum += ping_pong_times[i]; + $('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10); + }); + + // Handlers for the different forms in the page. + // These accept data from the user and send it to the server in a + // variety of ways + $('form#emit').submit(function(event) { + socket.emit('my_event', {data: $('#emit_data').val()}); + return false; + }); +}); \ No newline at end of file diff --git a/flask__websocket/sending_all_from_while_in_thread/static/jquery.js b/flask__websocket/sending_all_from_while_in_thread/static/jquery.js new file mode 100644 index 000000000..d4b67f7e6 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/static/jquery.js @@ -0,0 +1,10308 @@ +/*! + * jQuery JavaScript Library v1.11.1 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-05-01T17:42Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper window is present, + // execute the factory and get jQuery + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a jQuery-making factory as module.exports + // This accentuates the need for the creation of a real window + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +// + +var deletedIds = []; + +var slice = deletedIds.slice; + +var concat = deletedIds.concat; + +var push = deletedIds.push; + +var indexOf = deletedIds.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "1.11.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1, IE<9 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: deletedIds.sort, + splice: deletedIds.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + /* jshint eqeqeq: false */ + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + isPlainObject: function( obj ) { + var key; + + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Support: IE<9 + // Handle iteration over inherited properties before own properties. + if ( support.ownLast ) { + for ( key in obj ) { + return hasOwn.call( obj, key ); + } + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Support: Android<4.1, IE<9 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( indexOf ) { + return indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + while ( j < len ) { + first[ i++ ] = second[ j++ ]; + } + + // Support: IE<9 + // Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists) + if ( len !== len ) { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: function() { + return +( new Date() ); + }, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v1.10.19 + * http://sizzlejs.com/ + * + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-04-18 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + characterEncoding + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( documentIsHTML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document (jQuery #6963) + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // QSA path + if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = attrs.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== strundefined && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, + doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsHTML = !isXML( doc ); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if ( parent && parent !== parent.top ) { + // IE11 does not have attachEvent, so all must suffer + if ( parent.addEventListener ) { + parent.addEventListener( "unload", function() { + setDocument(); + }, false ); + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", function() { + setDocument(); + }); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { + div.innerHTML = "
"; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowclip^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return doc; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context !== document && context; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; + }); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + ret = [], + self = this, + len = self.length; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + } + + return this.pushStack( ret ); + }; +}); +var rnotwhite = (/\S+/g); + + + +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +}); + +/** + * Clean-up method for dom ready events + */ +function detach() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } +} + +/** + * The ready event handler and self cleanup method + */ +function completed() { + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + + +var strundefined = typeof undefined; + + + +// Support: IE<9 +// Iteration over object's inherited properties before its own +var i; +for ( i in jQuery( support ) ) { + break; +} +support.ownLast = i !== "0"; + +// Note: most support tests are defined in their respective modules. +// false until the test is run +support.inlineBlockNeedsLayout = false; + +// Execute ASAP in case we need to set body.style.zoom +jQuery(function() { + // Minified: var a,b,c,d + var val, div, body, container; + + body = document.getElementsByTagName( "body" )[ 0 ]; + if ( !body || !body.style ) { + // Return for frameset docs that don't have a body + return; + } + + // Setup + div = document.createElement( "div" ); + container = document.createElement( "div" ); + container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; + body.appendChild( container ).appendChild( div ); + + if ( typeof div.style.zoom !== strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.style.cssText = "display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1"; + + support.inlineBlockNeedsLayout = val = div.offsetWidth === 3; + if ( val ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); +}); + + + + +(function() { + var div = document.createElement( "div" ); + + // Execute the test only if not already executed in another module. + if (support.deleteExpando == null) { + // Support: IE<9 + support.deleteExpando = true; + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + } + + // Null elements to avoid leaks in IE. + div = null; +})(); + + +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( elem ) { + var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], + nodeType = +elem.nodeType || 1; + + // Do not set data on non-element DOM nodes because it will not be cleared (#8335). + return nodeType !== 1 && nodeType !== 9 ? + false : + + // Nodes accept data unless otherwise specified; rejection can be conditional + !noData || noData !== true && elem.getAttribute("classid") === noData; +}; + + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + +function internalData( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var ret, thisCache, + internalKey = jQuery.expando, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // The following elements (space-suffixed to avoid Object.prototype collisions) + // throw uncatchable exceptions if you attempt to set expando properties + noData: { + "applet ": true, + "embed ": true, + // ...but Flash objects (which have this classid) *can* handle expandos + "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var i, name, data, + elem = this[0], + attrs = elem && elem.attributes; + + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return arguments.length > 1 ? + + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + + +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; +}; +var rcheckableType = (/^(?:checkbox|radio)$/i); + + + +(function() { + // Minified: var a,b,c + var input = document.createElement( "input" ), + div = document.createElement( "div" ), + fragment = document.createDocumentFragment(); + + // Setup + div.innerHTML = "
a"; + + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName( "tbody" ).length; + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName( "link" ).length; + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = + document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav>"; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + input.type = "checkbox"; + input.checked = true; + fragment.appendChild( input ); + support.appendChecked = input.checked; + + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE6-IE11+ + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // #11217 - WebKit loses check when the name is after the checked attribute + fragment.appendChild( div ); + div.innerHTML = ""; + + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + support.noCloneEvent = true; + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Execute the test only if not already executed in another module. + if (support.deleteExpando == null) { + // Support: IE<9 + support.deleteExpando = true; + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + } +})(); + + +(function() { + var i, eventName, + div = document.createElement( "div" ); + + // Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event) + for ( i in { submit: true, change: true, focusin: true }) { + eventName = "on" + i; + + if ( !(support[ i + "Bubbles" ] = eventName in window) ) { + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + div.setAttribute( eventName, "t" ); + support[ i + "Bubbles" ] = div.attributes[ eventName ].expando === false; + } + } + + // Null elements to avoid leaks in IE. + div = null; +})(); + + +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: IE < 9, Android < 4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = jQuery._data( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = jQuery._data( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + jQuery._removeData( doc, fix ); + } else { + jQuery._data( doc, fix, attaches ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + col: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
", "
" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!support.noCloneEvent || !support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
" && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + deletedIds.push( id ); + } + } + } + } + } +}); + +jQuery.fn.extend({ + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + remove: function( selector, keepData /* Internal Use Only */ ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map(function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var arg = arguments[ 0 ]; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + arg = this.parentNode; + + jQuery.cleanData( getAll( this ) ); + + if ( arg ) { + arg.replaceChild( elem, this ); + } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, self.html() ); + } + self.domManip( args, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[i], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + + +var iframe, + elemdisplay = {}; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? + + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery( " +
ArabicHebrewPolish
BulgarianHindiPortuguese
CatalanHmong DawRomanian
Chinese SimplifiedHungarianRussian
Chinese TraditionalIndonesianSlovak
CzechItalianSlovenian
DanishJapaneseSpanish
DutchKlingonSwedish
EnglishKoreanThai
EstonianLatvianTurkish
FinnishLithuanianUkrainian
FrenchMalayUrdu
GermanMalteseVietnamese
GreekNorwegianWelsh
Haitian CreolePersian
\ No newline at end of file diff --git a/html_parsing/foxtrot_com_ua__ru__search/foxtrot_parser_2021-10-21.csv b/html_parsing/foxtrot_com_ua__ru__search/foxtrot_parser_2021-10-21.csv new file mode 100644 index 000000000..115387df6 --- /dev/null +++ b/html_parsing/foxtrot_com_ua__ru__search/foxtrot_parser_2021-10-21.csv @@ -0,0 +1,634 @@ +,Name,Price,Nal +0,Автомагнитола GAZER CM5007-GE6 для Honda Jazz (GE6) 2007-2013,13 299 ₴,"КУПИТЬ +Забрать в магазине" +1,Автомагнитола GAZER CM5510-ASX10 для Mitsubishi Lancer (CYA) 2007-2013,14 999 ₴,"КУПИТЬ +Забрать в магазине" +2,Автомагнитола GAZER CM6509-TYT для Toyota Land Cruiser Prado (J120) SUV 2002-2010,19 990 ₴,"КУПИТЬ +Забрать в магазине" +3,Автомагнитола GAZER CM5006-120 для Toyota Universal 2001-2012,12 999 ₴,"КУПИТЬ +Забрать в магазине" +4,"Автомагнитола GAZER CM5007-E39 для BMW 5 (E39), X5 (E53) 1996-2004",16 229 ₴,"КУПИТЬ +Забрать в магазине" +5,"Автомагнитола GAZER CM6510-J200 для Toyota Land Cruiser 200 (J200), 2007-2015",16 299 ₴,"КУПИТЬ +Забрать в магазине" +6,Автомагнитола GAZER CM5510-A40 для Toyota RAV4 (A40) 2013-2016,12 719 ₴,"КУПИТЬ +Забрать в магазине" +7,Автомагнитола GAZER CM5007-263 для Fiat Doblo (263) 2015-2017,15 899 ₴,"КУПИТЬ +Забрать в магазине" +8,Автомагнитола GAZER CM5508-CYA Mitsubishi Lancer (CYA) 2007-2013,13 879 ₴,"КУПИТЬ +Забрать в магазине" +9,Автомагнитола GAZER CM5509-RB Для Hyundai Accent (RB) 2010-2015,6 569 ₴,"КУПИТЬ +Забрать в магазине" +10,Автомагнитола GAZER CM6509-E39,20 901 ₴,"КУПИТЬ +Забрать в магазине" +11,Автомагнитола GAZER CM5510-LF,11 954 ₴,"КУПИТЬ +Забрать в магазине" +12,Автомагнитола GAZER ММ-система Gazer CM7012-J11,10 209 ₴,"КУПИТЬ +Забрать в магазине" +13,Автомагнитола GAZER CM5510-FA для Honda Civic (FA) 2006-2011,13 899 ₴,"КУПИТЬ +Забрать в магазине" +14,Автомагнитола GAZER CM5007-MD для Hyundai Elantra (MD) 2011-2016,13 899 ₴,"КУПИТЬ +Забрать в магазине" +15,Автомагнитола GAZER CM6508-Y62,9 539 ₴,"КУПИТЬ +Забрать в магазине" +16,Автомагнитола GAZER CM6508-MGH,21 990 ₴,"КУПИТЬ +Забрать в магазине" +17,Автомагнитола GAZER CM6008-JY,16 720 ₴,"КУПИТЬ +Забрать в магазине" +18,Автомагнитола GAZER CM5007-W164,16 999 ₴,"КУПИТЬ +Забрать в магазине" +19,Автомагнитола GAZER CM7012-J150H Toyota Prado LC150 HL,11 039 ₴,"КУПИТЬ +Забрать в магазине" +20,Автомагнитола GAZER CM6510-ZD Suzuki Swift (ZD) 2004-2010,8 819 ₴,"КУПИТЬ +Забрать в магазине" +21,Автомагнитола GAZER CM6510-V55,8 489 ₴,"КУПИТЬ +Забрать в магазине" +22,"Автомагнитола GAZER CM6509-T5 Volkswagen, Skoda, Seat, 2008-2016",18 392 ₴,"КУПИТЬ +Забрать в магазине" +23,Автомагнитола GAZER CM6008-J150,8 529 ₴,"КУПИТЬ +Забрать в магазине" +24,Автомагнитола GAZER CM5510-CMF для Renault Kadjar (CMF) 2015-2017,7 389 ₴,"КУПИТЬ +Забрать в магазине" +25,Автомагнитола GAZER CM6509-GV Suzuki Grand Vitara (GV) 2015-2017,8 849 ₴,"КУПИТЬ +Забрать в магазине" +26,Автомагнитола GAZER CM6509-CW6,18 392 ₴,"КУПИТЬ +Забрать в магазине" +27,Автомагнитола GAZER CM5510-TLN,6 609 ₴,"КУПИТЬ +Забрать в магазине" +28,Автомагнитола GAZER CM5509-RE,6 609 ₴,"КУПИТЬ +Забрать в магазине" +29,Автомагнитола GAZER CM5509-GV Suzuki Grand Vitara (GV) 2015-2017,7 399 ₴,"КУПИТЬ +Забрать в магазине" +30,Автомагнитола GAZER CM5509-CW6,6 689 ₴,"КУПИТЬ +Забрать в магазине" +31,Автомагнитола GAZER CM6510-JK Jeep Wrangler (JK) 2011-2014,20 901 ₴,"КУПИТЬ +Забрать в магазине" +32,Автомагнитола GAZER CM6008-Y62,9 539 ₴,"КУПИТЬ +Забрать в магазине" +33,Автомагнитола GAZER CM6006-CM Hyundai SantaFe (CM) 2006-2012,16 719 ₴,"КУПИТЬ +Забрать в магазине" +34,Автомагнитола GAZER CM5509-GH,6 569 ₴,"КУПИТЬ +Забрать в магазине" +35,Автомагнитола GAZER CM5507-QV,7 409 ₴,"КУПИТЬ +Забрать в магазине" +36,Автомагнитола GAZER CM5007-RE,11 119 ₴,"КУПИТЬ +Забрать в магазине" +37,Автомагнитола GAZER CM5007-ED Kia Ceed (ED) 2007-2012,11 103 ₴,"КУПИТЬ +Забрать в магазине" +38,Автомагнитола GAZER CM5007-CK,6 979 ₴,"КУПИТЬ +Забрать в магазине" +39,Автомагнитола GAZER CM6510-CR Honda Accord (CR) 2013 - 2017,18 392 ₴,"КУПИТЬ +Забрать в магазине" +40,Автомагнитола GAZER CM5510-CR Honda Accord (CR) 2013 - 2017,6 609 ₴,"КУПИТЬ +Забрать в магазине" +41,Автомагнитола GAZER CM5509-KX5 Kia Sportage (KX5) 2018 +,13 879 ₴,"КУПИТЬ +Забрать в магазине" +42,Автомагнитола GAZER CM6510-E21,7 679 ₴,"КУПИТЬ +Забрать в магазине" +43,Автомагнитола GAZER CM6510-J150N,20 901 ₴,"КУПИТЬ +Забрать в магазине" +44,Автомагнитола GAZER CM5510-A30,7 399 ₴,"КУПИТЬ +Забрать в магазине" +45,Автомагнитола GAZER CM5007-GAB,13 879 ₴,"КУПИТЬ +Забрать в магазине" +46,Автомагнитола GAZER CM5510-5LM,7 399 ₴,"КУПИТЬ +Забрать в магазине" +47,Автомагнитола GAZER CM6509-RB,7 959 ₴,"КУПИТЬ +Забрать в магазине" +48,Автомагнитола GAZER CM6007-RB,7 999 ₴,"КУПИТЬ +Забрать в магазине" +49,Автомагнитола GAZER CM5509-ER,7 019 ₴,"КУПИТЬ +Забрать в магазине" +50,Автомагнитола GAZER CM6509-P170,8 509 ₴,"КУПИТЬ +Забрать в магазине" +51,Автомагнитола GAZER CM6509-V40F,8 349 ₴,"КУПИТЬ +Забрать в магазине" +52,Автомагнитола GAZER CM6509-RW,18 392 ₴,"КУПИТЬ +Забрать в магазине" +53,Автомагнитола GAZER CM5509-RW,6 609 ₴,"КУПИТЬ +Забрать в магазине" +54,Автомагнитола GAZER CM5510-A50,6 609 ₴,"КУПИТЬ +Забрать в магазине" +55,Автомагнитола GAZER CM5510-J150N,8 329 ₴,"КУПИТЬ +Забрать в магазине" +56,Автомагнитола GAZER CM6509-CM,8 839 ₴,"КУПИТЬ +Забрать в магазине" +57,Автомагнитола GAZER CM6007-K14,7 199 ₴,"КУПИТЬ +Забрать в магазине" +58,Автомагнитола GAZER CM5007-K14,5 929 ₴,"КУПИТЬ +Забрать в магазине" +59,Автомагнитола GAZER CM6510-TLN,18 392 ₴,"КУПИТЬ +Забрать в магазине" +60,Автомагнитола GAZER CM5007-C6,13 879 ₴,"КУПИТЬ +Забрать в магазине" +61,Автомагнитола GAZER CM5510-J150,7 719 ₴,"КУПИТЬ +Забрать в магазине" +62,Автомагнитола GAZER CM6008-J100,7 990 ₴,"КУПИТЬ +Забрать в магазине" +63,Автомагнитола GAZER CM5006-N5,6 199 ₴,"КУПИТЬ +Забрать в магазине" +64,Автомагнитола GAZER CM6510-XU50,8 419 ₴,"КУПИТЬ +Забрать в магазине" +65,Автомагнитола GAZER CM7012-V55,9 859 ₴,"КУПИТЬ +Забрать в магазине" +66,Автомагнитола GAZER CM6007-B14,7 979 ₴,"КУПИТЬ +Забрать в магазине" +67,Автомагнитола GAZER CM5007-B14,6 649 ₴,"КУПИТЬ +Забрать в магазине" +68,Автомагнитола GAZER CM5007-L48,13 879 ₴,"КУПИТЬ +Забрать в магазине" +69,Автомагнитола GAZER CM5006-SC11,12 133 ₴,"КУПИТЬ +Забрать в магазине" +70,Автомагнитола GAZER CM6510-J32,8 527 ₴,"КУПИТЬ +Забрать в магазине" +71,Автомагнитола GAZER CM6008-D40,8 519 ₴,"КУПИТЬ +Забрать в магазине" +72,Автомагнитола GAZER CM6510-Z52R,8 489 ₴,"КУПИТЬ +Забрать в магазине" +73,Автомагнитола GAZER CM5006-L10,6 269 ₴,"КУПИТЬ +Забрать в магазине" +74,Автомагнитола GAZER CM6006-GFW,8 489 ₴,"КУПИТЬ +Забрать в магазине" +75,Автомагнитола GAZER CM5007-W163,8 099 ₴,"КУПИТЬ +Забрать в магазине" +76,Автомагнитола GAZER CM6509-KE,8 169 ₴,"КУПИТЬ +Забрать в магазине" +77,Автомагнитола GAZER CM5509-KE,6 619 ₴,"КУПИТЬ +Забрать в магазине" +78,Автомагнитола GAZER CM6509-GJ,8 529 ₴,"КУПИТЬ +Забрать в магазине" +79,Автомагнитола GAZER CM6509-QL,18 120 ₴,"КУПИТЬ +Забрать в магазине" +80,Автомагнитола GAZER CM6510-JF,8 489 ₴,"КУПИТЬ +Забрать в магазине" +81,Автомагнитола GAZER CM6007-XK,8 169 ₴,"КУПИТЬ +Забрать в магазине" +82,Автомагнитола GAZER CM5509-YF,6 659 ₴,"КУПИТЬ +Забрать в магазине" +83,Автомагнитола GAZER CM6509-EL,16 472 ₴,"КУПИТЬ +Забрать в магазине" +84,Автомагнитола GAZER CM5006-TQ,6 589 ₴,"КУПИТЬ +Забрать в магазине" +85,Автомагнитола GAZER CM5007-RB,11 954 ₴,"КУПИТЬ +Забрать в магазине" +86,Автомагнитола GAZER CM6509-GH,8 489 ₴,"КУПИТЬ +Забрать в магазине" +87,Автомагнитола GAZER CM5008-RE,6 719 ₴,"КУПИТЬ +Забрать в магазине" +88,Автомагнитола GAZER CM7012-BE,10 199 ₴,"КУПИТЬ +Забрать в магазине" +89,Автомагнитола GAZER CM7012-MA,9 869 ₴,"КУПИТЬ +Забрать в магазине" +90,Автомагнитола GAZER CM6007-263,7 899 ₴,"КУПИТЬ +Забрать в магазине" +91,Автомагнитола GAZER CM5007-T200,11 954 ₴,"КУПИТЬ +Забрать в магазине" +92,Автомагнитола GAZER CM5007-J300,6 609 ₴,"КУПИТЬ +Забрать в магазине" +93,Автомагнитола GAZER CM6007-E39,9 169 ₴,"КУПИТЬ +Забрать в магазине" +94,Автомагнитола GAZER CM5007-8E,6 609 ₴,"КУПИТЬ +Забрать в магазине" +95,Авторесивер GAZER CM7015-YF для Hyundai Sonata (YF) 2010-2015,10 589 ₴,"КУПИТЬ +Забрать в магазине" +96,Авторесивер GAZER CM7012-U387 для Ford Edge (U387) 2015-2017,10 869 ₴,"КУПИТЬ +Забрать в магазине" +97,Автомагнитола GAZER CM7012-J200 для Toyota LC 200 (J200) 2007-2015,10 149 ₴,"КУПИТЬ +Забрать в магазине" +98,Автомагнитола GAZER CM7010-UD для Hyundai Elantra (UD) 2016-2017,10 209 ₴,"КУПИТЬ +Забрать в магазине" +99,Автомагнитола GAZER CM7010-MD для Hyundai Elantra (MD) 2011-2016,10 589 ₴,"КУПИТЬ +Забрать в магазине" +100,Автомагнитола GAZER CM7010-BE1 для Volkswagen e-Golf VII (BE1) 2014+,10 119 ₴,"КУПИТЬ +Забрать в магазине" +101,Автомагнитола GAZER CM7010-3G2 для VW Passat B8 (3G\\4G2) 2014-2017,10 236 ₴,"КУПИТЬ +Забрать в магазине" +102,Авторесивер GAZER CM6510-YD для Kia Cerato (YD) 2015-2017,8 481 ₴,"КУПИТЬ +Забрать в магазине" +103,Авторесивер GAZER CM6510-XU40 для Toyota Highlander (XU40) 2008-2015,8 839 ₴,"КУПИТЬ +Забрать в магазине" +104,Автомагнитола GAZER CM6510-V70 для Toyota Camry (V70) 2018+,20 899 ₴,"КУПИТЬ +Забрать в магазине" +105,Автомагнитола GAZER CM6510-P150 для Toyota Yaris (P150) 2013-2016,8 258 ₴,"КУПИТЬ +Забрать в магазине" +106,Автомагнитола GAZER CM6510-MK49 для Jeep Compass (MK49) 2007-2016,8 859 ₴,"КУПИТЬ +Забрать в магазине" +107,Автомагнитола GAZER CM6510-MA для Ford Kuga (MA) - low version 2013-2017,8 089 ₴,"КУПИТЬ +Забрать в магазине" +108,Автомагнитола GAZER CM6510-LF для Hyundai Sonata (LF) 2015-2017,9 189 ₴,"КУПИТЬ +Забрать в магазине" +109,Автомагнитола GAZER CM6510-KX7 для Kia Sorento (KX7) 2017+,8 269 ₴,"КУПИТЬ +Забрать в магазине" +110,Автомагнитола GAZER CM6510-KL для Jeep Cherokee (KL) 2014-2017,16 720 ₴,"КУПИТЬ +Забрать в магазине" +111,Автомагнитола GAZER CM6510-J150 для Toyota Prado 2010-2013,23 499 ₴,"КУПИТЬ +Забрать в магазине" +112,"Автомагнитола GAZER CM6510-GP для Honda Fit, Jazz 2014-2017",8 455 ₴,"КУПИТЬ +Забрать в магазине" +113,Автомагнитола GAZER CM6510-FA для Honda Civic 2005-2011,8 506 ₴,"КУПИТЬ +Забрать в магазине" +114,Автомагнитола GAZER CM6510-CMF для Renault Kadjar 2015-2017,8 559 ₴,"КУПИТЬ +Забрать в магазине" +115,Автомагнитола GAZER CM6510-BE1 для Volkswagen Golf VII 2014-2017,20 899 ₴,"КУПИТЬ +Забрать в магазине" +116,Автомагнитола GAZER CM6510-AD1 для Volkswagen Tiguan 2016-2017,8 478 ₴,"КУПИТЬ +Забрать в магазине" +117,Автомагнитола GAZER CM6510-614 для Volkswagen Polo 2016-2017,8 559 ₴,"КУПИТЬ +Забрать в магазине" +118,Автомагнитола GAZER CM6510-5T1 для Volkswagen Touran 2016-2017,8 586 ₴,"КУПИТЬ +Забрать в магазине" +119,Автомагнитола GAZER CM6510-5N2 для Volkswagen Tiguan 2013-2016,16 999 ₴,"КУПИТЬ +Забрать в магазине" +120,Автомагнитола GAZER CM6510-5E3 для Skoda Octavia A7 2013-2016,8 839 ₴,"КУПИТЬ +Забрать в магазине" +121,Автомагнитола GAZER CM6510-3G2 для Volkswagen Passat B8 2014-2017,8 859 ₴,"КУПИТЬ +Забрать в магазине" +122,Автомагнитола GAZER CM6510-162 для Volkswagen Jetta 2013-2016,8 111 ₴,"КУПИТЬ +Забрать в магазине" +123,Автомагнитола GAZER CM6509-UB для Kia Rio 2012-2015,13 920 ₴,"КУПИТЬ +Забрать в магазине" +124,Автомагнитола GAZER CM6509-TF для Kia Optima 2010-2015,20 899 ₴,"КУПИТЬ +Забрать в магазине" +125,Автомагнитола GAZER CM6509-RP для Kia Carens 2013-2017,8 467 ₴,"КУПИТЬ +Забрать в магазине" +126,Автомагнитола GAZER CM6509-NJ для Skoda Fabia 2015-2017,8 178 ₴,"КУПИТЬ +Забрать в магазине" +127,"Авторесивер GAZER CM6509-KR0 для Renault Captur, Clio (KR0)",8 849 ₴,"КУПИТЬ +Забрать в магазине" +128,"Автомагнитола GAZER CM6509-J350 для Chevrolet Cruze (J350), Lacetti 2013-2017",8 489 ₴,"КУПИТЬ +Забрать в магазине" +129,"Автомагнитола GAZER CM6509-GS для Opel Astra (K) 2014-2017, Buick Verano (GS), 2015-2017",8 466 ₴,"КУПИТЬ +Забрать в магазине" +130,Автомагнитола GAZER CM6509-E140 для Toyota Corolla (E140) 2006-2012,8 489 ₴,"КУПИТЬ +Забрать в магазине" +131,Автомагнитола GAZER CM6509-DM для Hyundai SantaFe (DM) 2012-2016,19 990 ₴,"КУПИТЬ +Забрать в магазине" +132,Автомагнитола GAZER CM6009-XU50 для Toyota Highlander (XU50) 2015-2017,10 269 ₴,"КУПИТЬ +Забрать в магазине" +133,Автомагнитола GAZER CM6009-P170 для Toyota Yaris (P170) 2016-2017,7 981 ₴,"КУПИТЬ +Забрать в магазине" +134,Автомагнитола GAZER CM6009-FS для Renault Sandero (FS) 2008-2012,8 849 ₴,"КУПИТЬ +Забрать в магазине" +135,Автомагнитола GAZER CM6008-VST для Lada Vesta 2015-2017,7 665 ₴,"КУПИТЬ +Забрать в магазине" +136,Автомагнитола GAZER CM6008-V55 для Toyota Camry (V55) 2015-2017,7 919 ₴,"КУПИТЬ +Забрать в магазине" +137,Автомагнитола GAZER CM6008-V50 для Toyota Camry (V50) 2012-2015,16 999 ₴,"КУПИТЬ +Забрать в магазине" +138,Авторесивер GAZER CM6008-V40 для Toyota Camry (V40) 2007-2011,8 839 ₴,"КУПИТЬ +Забрать в магазине" +139,Авторесивер GAZER CM6008-UM для Kia Sorento (UM) 2015-2017,8 239 ₴,"КУПИТЬ +Забрать в магазине" +140,Авторесивер GAZER CM6008-UB15 для Kia Rio (UB15) 2015-2017,8 539 ₴,"КУПИТЬ +Забрать в магазине" +141,Авторесивер GAZER CM6008-TL для Hyundai Tucson (TL) 2015-2017,19 990 ₴,"КУПИТЬ +Забрать в магазине" +142,Авторесивер GAZER CM6008-T300 для Chevrolet Aveo (T300) 2011-2016,8 499 ₴,"КУПИТЬ +Забрать в магазине" +143,Автомагнитола GAZER CM6008-NK3 для Skoda Rapid (NK3) 2013-2016,9 493 ₴,"КУПИТЬ +Забрать в магазине" +144,Автомагнитола GAZER CM6008-J200 для Toyota LC 200 (J200) 2007-2015,8 570 ₴,"КУПИТЬ +Забрать в магазине" +145,"Автомагнитола GAZER CM6008-J150H для Toyota Prado, LC150 - High level, 2014-2016",8 579 ₴,"КУПИТЬ +Забрать в магазине" +146,Автомагнитола GAZER CM6008-GB для Hyundai i20 (GB) 2014-2017,8 506 ₴,"КУПИТЬ +Забрать в магазине" +147,Автомагнитола GAZER CM6008-3G2 для VW Passat B8 (3G\\4G2) 2014-2017,8 518 ₴,"КУПИТЬ +Забрать в магазине" +148,Авторесивер GAZER CM6007-ZCS для Suzuki Swift (ZCS) 2011-2016,8 492 ₴,"КУПИТЬ +Забрать в магазине" +149,Авторесивер GAZER CM6007-XM10 для Kia Sorento (XM10) 2010-2012,19 139 ₴,"КУПИТЬ +Забрать в магазине" +150,Авторесивер GAZER CM6007-W251 для Mercedes-Benz R-Class (W251) 2005-2013,9 719 ₴,"КУПИТЬ +Забрать в магазине" +151,Авторесивер GAZER CM6007-W220 для Mercedes-Benz S-Class (W220) 1998-2005,9 519 ₴,"КУПИТЬ +Забрать в магазине" +152,Авторесивер GAZER CM6007-W163 для Mercedes,21 389 ₴,"КУПИТЬ +Забрать в магазине" +153,Автомагнитола GAZER CM6007-VF для Hyundai i40 (VF) 2011-2016,8 215 ₴,"КУПИТЬ +Забрать в магазине" +154,"Автомагнитола GAZER CM6007-T200 для Chevrolet Epica, Captiva, Aveo 2002-2011",7 959 ₴,"КУПИТЬ +Забрать в магазине" +155,Автомагнитола GAZER CM6007-RM4 для Honda CR-V (RM4) 2012-2016,7 509 ₴,"КУПИТЬ +Забрать в магазине" +156,Автомагнитола GAZER CM6007-MD для Hyundai Elantra (MD) 2011-2016,15 719 ₴,"КУПИТЬ +Забрать в магазине" +157,"Автомагнитола GAZER CM6007-JUF для Ford Fusion (JU), Explorer (U251), Mustang, Edge, Escape II, 2006-2009",8 475 ₴,"КУПИТЬ +Забрать в магазине" +158,"Авторесивер GAZER CM6007-J300 для Chevrolet Cruze (J300), Lacetti, 2008-2012",8 129 ₴,"КУПИТЬ +Забрать в магазине" +159,Авторесивер GAZER CM6007-GD для Hyundai i30 (GD) 2012-2016,19 990 ₴,"КУПИТЬ +Забрать в магазине" +160,Авторесивер GAZER CM6007-FA для Honda Civic (FA) 2006-2011,8 859 ₴,"КУПИТЬ +Забрать в магазине" +161,Авторесивер GAZER CM6007-EL для Hyundai IX35 (EL) 2010-2015,19 990 ₴,"КУПИТЬ +Забрать в магазине" +162,Авторесивер GAZER CM6007-E180 для Toyota Auris (E180) 2006-2012,21 990 ₴,"КУПИТЬ +Забрать в магазине" +163,Авторесивер GAZER CM6007-E150 для Toyota Corolla (E150) 2013-2016,8 496 ₴,"КУПИТЬ +Забрать в магазине" +164,Автомагнитола GAZER CM6007-BA для Hyundai i10 (BA) 2013-2016,8 505 ₴,"КУПИТЬ +Забрать в магазине" +165,Автомагнитола GAZER CM6007-A30 для Toyota RAV4 (A30) 2006-2012,19 990 ₴,"КУПИТЬ +Забрать в магазине" +166,Автомагнитола GAZER CM6007-955 для Porsche Cayenne (955) 2002-2009,8 779 ₴,"КУПИТЬ +Забрать в магазине" +167,Автомагнитола GAZER CM6007-8P для Audi A3 2003-2008,8 829 ₴,"КУПИТЬ +Забрать в магазине" +168,Автомагнитола GAZER CM6007-8J для Audi TT 2006-2011,8 859 ₴,"КУПИТЬ +Забрать в магазине" +169,Автомагнитола GAZER CM6007-408W для Peugeot 408 2010-2013,8 488 ₴,"КУПИТЬ +Забрать в магазине" +170,Авторесивер GAZER CM6006-TQ для Hyundai H1 (TQ) 2007-2012,8 472 ₴,"КУПИТЬ +Забрать в магазине" +171,Авторесивер GAZER CM6006-NHP10 для Toyota Prius C (NHP10) 2011-2014,16 719 ₴,"КУПИТЬ +Забрать в магазине" +172,Авторесивер GAZER CM6006-N5 для Toyota Hilux (N5) 2012-2015,7 999 ₴,"КУПИТЬ +Забрать в магазине" +173,Авторесивер GAZER CM6006-L10 для Nissan Livina (L10) 2013-2016,7 671 ₴,"КУПИТЬ +Забрать в магазине" +174,Авторесивер GAZER CM5510-XU50 для Toyota Highlander (XU50) 2015-2017,7 389 ₴,"КУПИТЬ +Забрать в магазине" +175,Авторесивер GAZER CM5510-V55 для Toyota Camry (V55) 2015-2017,7 091 ₴,"КУПИТЬ +Забрать в магазине" +176,Авторесивер GAZER CM5510-UD для Hyundai Elantra (UD) 2016-2017,6 930 ₴,"КУПИТЬ +Забрать в магазине" +177,Авторесивер GAZER CM5510-TL для Hyundai Tucson (TL) 2015-2017,7 059 ₴,"КУПИТЬ +Забрать в магазине" +178,Автомагнитола GAZER CM5510-MK49 для Jeep Compass (MK49) 2007-2016,13 879 ₴,"КУПИТЬ +Забрать в магазине" +179,Автомагнитола GAZER CM5510-MA для Ford Kuga (MA) - low version 2013-2017,6 891 ₴,"КУПИТЬ +Забрать в магазине" +180,"Автомагнитола GAZER CM5510-KX7 для Kia Sorento (KX7), 2017+",7 085 ₴,"КУПИТЬ +Забрать в магазине" +181,Электропривод крышки багажника GAZER TG-NS7,25 999 ₴,"КУПИТЬ +Забрать в магазине" +182,Автомагнитола GAZER CM5510-J200 для Toyota Land Cruiser 200 (J200) 2007-2015,7 166 ₴,"КУПИТЬ +Забрать в магазине" +183,Электропривод крышки багажника GAZER TG-KF,25 999 ₴,"КУПИТЬ +Забрать в магазине" +184,"Автомагнитола GAZER CM5510-J150H/L для Toyota Prado, LC150 - High level, 2014-2016",8 161 ₴,"КУПИТЬ +Забрать в магазине" +185,Сервопривод багажника Gazer TG-GH для Honda HRV 2015+ (TG-UZ),25 999 ₴,"КУПИТЬ +Забрать в магазине" +186,Электропривод крышки багажника GAZER TG-GFW,25 999 ₴,"КУПИТЬ +Забрать в магазине" +187,"Автомагнитола GAZER CM5510-GP для Honda Fit (GP), Jazz 2014-2017",7 051 ₴,"КУПИТЬ +Забрать в магазине" +188,Электропривод крышки багажника GAZER TG-DZ,25 999 ₴,"КУПИТЬ +Забрать в магазине" +189,Сервопривод багажника Gazer TG-DM для Hyundai Santa Fe 2015-2016 (TG-W212),25 999 ₴,"КУПИТЬ +Забрать в магазине" +190,Автомагнитола GAZER CM5510-BE1 для Volkswagen Golf VII (BE1) 2014-2017,7 099 ₴,"КУПИТЬ +Забрать в магазине" +191,Электропривод крышки багажника GAZER TG-DK,25 999 ₴,"КУПИТЬ +Забрать в магазине" +192,Электропривод крышки багажника GAZER TG-B8,25 999 ₴,"КУПИТЬ +Забрать в магазине" +193,Автомагнитола GAZER CM5510-AD1 для Volkswagen Tiguan (AD1) 2016-2017,7 074 ₴,"КУПИТЬ +Забрать в магазине" +194,Сервопривод багажника Gazer TG-AD1 для Volkswagen Tiguan 2017+ (TG-W213D),25 999 ₴,"КУПИТЬ +Забрать в магазине" +195,Сервопривод багажника Gazer TG-A50 для Toyota RAV4 2019+ (TG-FTS),25 999 ₴,"КУПИТЬ +Забрать в магазине" +196,Автомагнитола GAZER CM5510-614 для Volkswagen Polo (614) 2016-2017,7 155 ₴,"КУПИТЬ +Забрать в магазине" +197,Сервопривод багажника Gazer TG-7P5 для Volkswagen Touareg 2010-2018 (TG-W639),25 999 ₴,"КУПИТЬ +Забрать в магазине" +198,Автомагнитола GAZER CM5510-5N2 для Volkswagen Tiguan (5N2) 2013-2016,12 299 ₴,"КУПИТЬ +Забрать в магазине" +199,Сервопривод багажника Gazer TG-5N2 для Volkswagen Tiguan 2007-2017 (TG-XZ),25 999 ₴,"КУПИТЬ +Забрать в магазине" +200,Автомагнитола GAZER CM5510-5LA для Skoda Yeti (5L) - auto conditioner 2009-2013,7 090 ₴,"КУПИТЬ +Забрать в магазине" +201,Электропривод крышки багажника GAZER TG-NU7,25 999 ₴,"КУПИТЬ +Забрать в магазине" +202,Автомагнитола GAZER CM5510-5E3 для Skoda Octavia A7 (5E3) 2013-2016,12 719 ₴,"КУПИТЬ +Забрать в магазине" +203,Электропривод крышки багажника GAZER TG-Q,25 999 ₴,"КУПИТЬ +Забрать в магазине" +204,Автомагнитола GAZER CM5510-3G2 для Volkswagen Passat B8 (3G2) 2014-2017,14 999 ₴,"КУПИТЬ +Забрать в магазине" +205,Сервопривод багажника Gazer TG-RM для Honda CRV 2012-2016 (TG-UM),25 999 ₴,"КУПИТЬ +Забрать в магазине" +206,Сервопривод багажника Gazer TG-RW для Honda CRV 2016-2019 (TG-TLN),25 999 ₴,"КУПИТЬ +Забрать в магазине" +207,Сервопривод багажника Gazer TG-TL для Hyundai Tucson 2015+ (TG-W213),25 999 ₴,"КУПИТЬ +Забрать в магазине" +208,Авторесивер GAZER CM5509-TYT для Toyota Prado (J120) 2002-2009,13 899 ₴,"КУПИТЬ +Забрать в магазине" +209,Сервопривод багажника Gazer TG-TL для Hyundai Tucson 2015+ (TG-V8WS),25 999 ₴,"КУПИТЬ +Забрать в магазине" +210,Электропривод крышки багажника GAZER TG-U,25 999 ₴,"КУПИТЬ +Забрать в магазине" +211,Автомагнитола GAZER CM5509-S13 для Subaru Forester (S13) 2015-2017,7 092 ₴,"КУПИТЬ +Забрать в магазине" +212,Электропривод крышки багажника GAZER TG-U,25 999 ₴,"КУПИТЬ +Забрать в магазине" +213,Автомагнитола GAZER CM5509-QL для Kia Sportage (QL) 2015-2017,15 899 ₴,"КУПИТЬ +Забрать в магазине" +214,Электропривод крышки багажника Gazer TG-V8WS,25 999 ₴,"КУПИТЬ +Забрать в магазине" +215,Электропривод крышки багажника GAZER TG-W212,25 999 ₴,"КУПИТЬ +Забрать в магазине" +216,Автомагнитола GAZER CM5509-N8 для Toyota Hilux (N8) 2015-2017,13 899 ₴,"КУПИТЬ +Забрать в магазине" +217,Электропривод крышки багажника GAZER TG-W213D,25 999 ₴,"КУПИТЬ +Забрать в магазине" +218,"Автомагнитола GAZER CM5509-KR0A для Renault Captur, Clio (KR0) - AC 2011-2015",7 079 ₴,"КУПИТЬ +Забрать в магазине" +219,Электропривод крышки багажника GAZER TG-W,25 999 ₴,"КУПИТЬ +Забрать в магазине" +220,"Автомагнитола GAZER CM5509-GS для Opel Astra (K), 2014-2017; Buick Verano (GS), 2015-2017",7 062 ₴,"КУПИТЬ +Забрать в магазине" +221,Электропривод крышки багажника GAZER TG-W,25 999 ₴,"КУПИТЬ +Забрать в магазине" +222,Автомагнитола GAZER CM5509-FB4 для Honda Civic (FB4) 2016-2017,7 050 ₴,"КУПИТЬ +Забрать в магазине" +223,Электропривод крышки багажника GAZER TG-X,25 999 ₴,"КУПИТЬ +Забрать в магазине" +224,Автомагнитола GAZER CM5509-EL для Hyundai IX35 (EL) 2010-2015,13 899 ₴,"КУПИТЬ +Забрать в магазине" +225,Сенсор электропривода крышки багажника GAZER TG-FTS,3 599 ₴,"КУПИТЬ +Забрать в магазине" +226,Автомагнитола GAZER CM5509-DM для Hyundai SantaFe (DM) 2012-2016,7 079 ₴,"КУПИТЬ +Забрать в магазине" +227,Сервопривод багажника Gazer TG-A40 для Toyota RAV4 2013-2018 (TG-GFW),25 999 ₴,"КУПИТЬ +Забрать в магазине" +228,Автомагнитола GAZER CM5508-7P6 для Volkswagen Touareg (7P6) 2016-2017,9 138 ₴,"КУПИТЬ +Забрать в магазине" +229,Автомагнитола GAZER CM5009-P170 для Toyota Yaris (P170) 2016-2017,6 713 ₴,"КУПИТЬ +Забрать в магазине" +230,"Автомагнитола GAZER CM5009-J150L для Toyota Prado, LC150 - Low level 2014-2016",6 619 ₴,"КУПИТЬ +Забрать в магазине" +231,Автомагнитола GAZER CM5009-BK для Ford Focus (BK) 2015-2017,7 139 ₴,"КУПИТЬ +Забрать в магазине" +232,Авторесивер GAZER CM5008-XW50 для Toyota Prius (XW50) 2014-2017,7 109 ₴,"КУПИТЬ +Забрать в магазине" +233,Авторесивер GAZER CM5008-XM для Kia Sorento (XM) 2013-2015,7 079 ₴,"КУПИТЬ +Забрать в магазине" +234,Авторесивер GAZER CM5008-VST для Lada Vesta 2015-2017,6 261 ₴,"КУПИТЬ +Забрать в магазине" +235,Авторесивер GAZER CM5008-V9W для Mitsubishi Pajero (V9W) 2016-2017,6 635 ₴,"КУПИТЬ +Забрать в магазине" +236,Авторесивер GAZER CM5008-V55 для Toyota Camry (V55) 2015-2017,7 139 ₴,"КУПИТЬ +Забрать в магазине" +237,Автомагнитола GAZER CM5008-V50USA для Toyota Camry (V50) - USA 2012-2015,6 439 ₴,"КУПИТЬ +Забрать в магазине" +238,Автомагнитола GAZER CM5008-UM для Kia Sorento (UM) 2015-2017,7 099 ₴,"КУПИТЬ +Забрать в магазине" +239,Автомагнитола GAZER CM5008-UB15 для Kia Rio (UB15) 2015-2017,7 439 ₴,"КУПИТЬ +Забрать в магазине" +240,Автомагнитола GAZER CM5008-TF для Kia Optima (TF) 2010-2015,7 429 ₴,"КУПИТЬ +Забрать в магазине" +241,Автомагнитола GAZER CM5008-T300 для Chevrolet Aveo (T300) 2011-2016,7 103 ₴,"КУПИТЬ +Забрать в магазине" +242,Автомагнитола GAZER CM5008-SL для Kia Sportage (SL) 2010-2015,6 849 ₴,"КУПИТЬ +Забрать в магазине" +243,Автомагнитола GAZER CM5008-NK3 для Skoda Rapid (NK3) 2013-2016,6 319 ₴,"КУПИТЬ +Забрать в магазине" +244,"Автомагнитола GAZER CM5008-KE для Mazda CX-5 (KE), 6 (GJ) 2012-2016",6 675 ₴,"КУПИТЬ +Забрать в магазине" +245,Автомагнитола GAZER CM5008-JD для Kia Ceed (JD) 2011-2017,6 708 ₴,"КУПИТЬ +Забрать в магазине" +246,"Автомагнитола GAZER CM5008-J350 для Chevrolet Cruze (J350), Lacetti 2013-2017",6 839 ₴,"КУПИТЬ +Забрать в магазине" +247,Автомагнитола GAZER CM5008-J100 для Toyota LC 100 (J100) 1998-2007,6 729 ₴,"КУПИТЬ +Забрать в магазине" +248,Автомагнитола GAZER CM5008-GB для Hyundai i20 (GB) 2014-2017,7 101 ₴,"КУПИТЬ +Забрать в магазине" +249,Авторесивер GAZER CM5008-CKN для SsangYong Korando (CKN) 2013-2016,6 689 ₴,"КУПИТЬ +Забрать в магазине" +250,Авторесивер GAZER CM5008-BL для Mazda 3 (BL) 2010-2014,6 916 ₴,"КУПИТЬ +Забрать в магазине" +251,Авторесивер GAZER CM5008-A40 для Toyota RAV4 (A40) 2013-2016,7 109 ₴,"КУПИТЬ +Забрать в магазине" +252,Авторесивер GAZER CM5008-5E3 для Skoda Octavia A7 (5E3) 2013-2016,7 661 ₴,"КУПИТЬ +Забрать в магазине" +253,Авторесивер GAZER CM5008-3G2 для Volkswagen Passat B8 (3G2) 2014-2017,13 899 ₴,"КУПИТЬ +Забрать в магазине" +254,Авторесивер GAZER CM5007-ZCS для Suzuki Swift (ZCS) 2011-2016,7 088 ₴,"КУПИТЬ +Забрать в магазине" +255,Автомагнитола GAZER CM5007-VF для Hyundai i40 (VF) 2011-2016,6 809 ₴,"КУПИТЬ +Забрать в магазине" +256,Автомагнитола GAZER CM5007-TB для Mazda CX-9 (TB) 2006-2012,7 099 ₴,"КУПИТЬ +Забрать в магазине" +257,Автомагнитола GAZER CM5007-TA для Kia Picanto (TA) 2011-2017,6 279 ₴,"КУПИТЬ +Забрать в магазине" +258,Автомагнитола GAZER CM5007-S12 для Subaru Forester (S12) 2007-2012,6 809 ₴,"КУПИТЬ +Забрать в магазине" +259,Автомагнитола GAZER CM5007-RM4 для Honda CR-V (RM4) 2012-2016,6 569 ₴,"КУПИТЬ +Забрать в магазине" +260,Автомагнитола GAZER CM5007-QV для Kia Mohave (QV) 2007-2012,13 899 ₴,"КУПИТЬ +Забрать в магазине" +261,Автомагнитола GAZER CM5007-PA для Hyundai i10 (PA) 2007-2013,6 264 ₴,"КУПИТЬ +Забрать в магазине" +262,Автомагнитола GAZER CM5007-P150 для Toyota Yaris (P150) 2013-2016,6 929 ₴,"КУПИТЬ +Забрать в магазине" +263,"Автомагнитола GAZER CM5007-JU для Ford Fuison (JU), Explorer (U251), Mustang, Edge, Escape II, 2006-2009",7 085 ₴,"КУПИТЬ +Забрать в магазине" +264,Автомагнитола GAZER CM5007-HSAM для Renault Duster (HSAM) 2010-2016,7 085 ₴,"КУПИТЬ +Забрать в магазине" +265,Автомагнитола GAZER CM5007-E46 для BMW 3 (E46) 1998-2004,12 119 ₴,"КУПИТЬ +Забрать в магазине" +266,Автомагнитола GAZER CM5007-E180 для Toyota Auris (E180) 2006-2012,14 999 ₴,"КУПИТЬ +Забрать в магазине" +267,Автомагнитола GAZER CM5007-E150 для Toyota Corolla (E150) 2013-2016,7 099 ₴,"КУПИТЬ +Забрать в магазине" +268,Автомагнитола GAZER CM5007-E140 для Toyota Corolla (E140) 2006-2012,12 133 ₴,"КУПИТЬ +Забрать в магазине" +269,"Автомагнитола GAZER CM5007-DB для Ford Focus, Mondeo, Connect, S-Max 2004-2010",13 899 ₴,"КУПИТЬ +Забрать в магазине" +270,Автомагнитола GAZER CM5007-BK для Mazda 3 (BK) 2004-2009,14 999 ₴,"КУПИТЬ +Забрать в магазине" +271,Автомагнитола GAZER CM5007-BA для Hyundai i10 (BA) 2013-2016,7 101 ₴,"КУПИТЬ +Забрать в магазине" +272,Автомагнитола GAZER CM5007-955 для Porsche Cayenne (955) 2002-2009,16 999 ₴,"КУПИТЬ +Забрать в магазине" +273,Автомагнитола GAZER CM5007-8J для Audi TT 2006-2011,7 059 ₴,"КУПИТЬ +Забрать в магазине" +274,Автомагнитола GAZER CM5007-408W для Peugeot 408 2010-2013,7 084 ₴,"КУПИТЬ +Забрать в магазине" +275,Автомагнитола GAZER CM5007-198 для Fiat Bravo (198) 2007-2015,7 079 ₴,"КУПИТЬ +Забрать в магазине" +276,Автомагнитола GAZER CM5006-PB для Hyundai i20 (PB) 2008-2013,6 086 ₴,"КУПИТЬ +Забрать в магазине" +277,Автомагнитола GAZER CM5006-P130 для Toyota Yaris (P130) 2011-2013,7 019 ₴,"КУПИТЬ +Забрать в магазине" +278,Авторесивер GAZER CM5006-NF для Hyundai Sonata (NF) 2009-2010,5 949 ₴,"КУПИТЬ +Забрать в магазине" +279,"Авторесивер GAZER CM5006-M11 для Chery A3 (M11) A5, Tiggo (T11) 2005-2014",12 119 ₴,"КУПИТЬ +Забрать в магазине" +280,Авторесивер GAZER CM5006-LD для Kia Cerato (LD) 2003-2009,12 719 ₴,"КУПИТЬ +Забрать в магазине" +281,"Автомагнитола GAZER CM5006-GFW для Mitsubishi Outlander (GFW), ASX, Lancer, 2013",7 089 ₴,"КУПИТЬ +Забрать в магазине" +282,Автомагнитола GAZER CM5006-CM для Hyundai Santa Fe (CM) 2006-2012,11 954 ₴,"КУПИТЬ +Забрать в магазине" +283,Автомагнитола GAZER CM5006-BF3 для BYD F3 2007-2012,7 059 ₴,"КУПИТЬ +Забрать в магазине" +284,Автомагнитола GAZER CM5006-152 для Fiat Doblo (152) 2009-2014,6 829 ₴,"КУПИТЬ +Забрать в магазине" +285,Автомагнитола GAZER CM5006-120F для Toyota Universal 2001-2012,6 285 ₴,"КУПИТЬ +Забрать в магазине" +286,"Автомагнитола GAZER CM6510-J150H/L для Toyota Prado, LC150 - High/Low level 2014-2016",8 939 ₴,"КУПИТЬ +Забрать в магазине" +287,"Автомагнитола GAZER CM6510-E150 для Toyota Corolla (E150), 2013+",16 719 ₴,"КУПИТЬ +Забрать в магазине" +288,Автомагнитола GAZER CM6510-A40 для Toyota RAV4 (A40) 2012+,8 859 ₴,"КУПИТЬ +Забрать в магазине" +289,Автомагнитола GAZER CM6509-V9W для Mitsubishi Pajero (V9W) 2016-2017,8 529 ₴,"КУПИТЬ +Забрать в магазине" +290,Автомагнитола GAZER CM6509-S13 для Subaru Forester (S13) 2015-2017,8 269 ₴,"КУПИТЬ +Забрать в магазине" +291,Автомагнитола GAZER CM6508-7P6 для VW Touareg (7P6) 2016-2017,10 489 ₴,"КУПИТЬ +Забрать в магазине" +292,Автомагнитола GAZER CM6008-XW50 для Toyota Prius (XW50),8 249 ₴,"КУПИТЬ +Забрать в магазине" +293,Автомагнитола GAZER CM6008-V50USA для Toyota Camry (V50) USA 2012-2015,8 329 ₴,"КУПИТЬ +Забрать в магазине" +294,Автомагнитола GAZER CM6008-DM для Hyundai SantaFe (DM) SUV 2012+,8 119 ₴,"КУПИТЬ +Забрать в магазине" +295,"Автомагнитола GAZER CM6008-A40 для Toyota RAV4 (A40), 2012+",8 269 ₴,"КУПИТЬ +Забрать в магазине" +296,Автомагнитола GAZER CM6007-W2 для SsangYong Rexton (W2) 2013-2017,16 720 ₴,"КУПИТЬ +Забрать в магазине" +297,Автомагнитола GAZER CM6007-V8W для Mitsubishi Pajero Wagon (V8) 2006-2015,8 539 ₴,"КУПИТЬ +Забрать в магазине" +298,"Автомагнитола GAZER CM6007-QV для Kia Mohave (QV), 2007-2012",8 489 ₴,"КУПИТЬ +Забрать в магазине" +299,Автомагнитола GAZER CM6007-HSAM для Renault Duster (HSAM) 2010-2016,8 079 ₴,"КУПИТЬ +Забрать в магазине" +300,Автомагнитола GAZER CM6007-GV10 для Toyota Venza (GV10) 2008-2016,18 392 ₴,"КУПИТЬ +Забрать в магазине" +301,"Автомагнитола GAZER CM6007-ED для Kia Ceed (ED), 2007-2012",19 300 ₴,"КУПИТЬ +Забрать в магазине" +302,"Автомагнитола GAZER CM6007-DB для Ford Focus (DB), Mondeo (BA7), Connect, S-Max, 2004-2010",8 329 ₴,"КУПИТЬ +Забрать в магазине" +303,Автомагнитола GAZER CM6007-BK для Mazda 3 (BK) (2004-2009),18 392 ₴,"КУПИТЬ +Забрать в магазине" +304,Автомагнитола GAZER CM6007-7L для VW Touareg (7L) 2002-2010,8 979 ₴,"КУПИТЬ +Забрать в магазине" +305,"Автомагнитола GAZER CM6007-1Z3 для Skoda Octavia A5 (1Z3), Fabia, Superb, 2005-2012",18 392 ₴,"КУПИТЬ +Забрать в магазине" +306,"Автомагнитола GAZER CM7012-J150 для Toyota Prado (J150), 2010-2013",9 819 ₴,"КУПИТЬ +Забрать в магазине" +307,Автомагнитола GAZER CM6510-TL для Hyundai Tucson (TL) 2015-2017,16 472 ₴,"КУПИТЬ +Забрать в магазине" +308,Автомагнитола GAZER CM6008-JD для Kia Ceed (JD) 2012+,8 519 ₴,"КУПИТЬ +Забрать в магазине" +309,Автомагнитола GAZER CM6007-TYT для Toyota Prado (J120) 2002-2009,9 249 ₴,"КУПИТЬ +Забрать в магазине" +310,Автомагнитола GAZER CM6007-S12 для Subaru Forester (S12) 2007-2012,16 719 ₴,"КУПИТЬ +Забрать в магазине" +311,Автомагнитола GAZER CM6509-FB4 для Honda Civic (FB4) 2016-2017,8 069 ₴,"КУПИТЬ +Забрать в магазине" +312,"Автомагнитола GAZER CM6510-A30 для Toyota RAV 4 (A30), 2005-2012",8 219 ₴,"КУПИТЬ +Забрать в магазине" +313,"Автомагнитола GAZER CM6006-120F для Toyota Universal, 2001-2012",7 949 ₴,"КУПИТЬ +Забрать в магазине" +314,"Автомагнитола GAZER CM6006-120 для Toyota Universal, 2001-2012",7 449 ₴,"КУПИТЬ +Забрать в магазине" +315,"Автомагнитола GAZER CM6006-SC11 для Nissan Tiida (SC11), Qashqai, X-Trail, Patrol, 2004-2010",7 669 ₴,"КУПИТЬ +Забрать в магазине" +316,Автомагнитола GAZER CM5008-MGH,-,СООБЩИТЬ О НАЛИЧИИ diff --git a/html_parsing/foxtrot_com_ua__ru__search/main.py b/html_parsing/foxtrot_com_ua__ru__search/main.py new file mode 100644 index 000000000..8368e2bf6 --- /dev/null +++ b/html_parsing/foxtrot_com_ua__ru__search/main.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt + +from pathlib import Path + +# pip install pandas +import pandas as pd + +# pip install selenium +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By + + +# TODO: Using logging instead print +# TODO: price must be decimal + + +def get_text_by_css(parent, css_selector: str, default: str) -> str: + try: + return parent.find_element(By.CSS_SELECTOR, css_selector).text + except: + return default + + +def parse(url_search: str) -> list[tuple[str, str, str]]: + options = Options() + options.add_argument("--headless") + + items = [] + + driver = webdriver.Firefox(options=options) + driver.implicitly_wait(5.5) + + try: + page = last_page = 1 + while page <= last_page: + url = url_search + if page > 1: + url = f"{url_search}&page={page}" + + print(f"Load: {url}") + driver.get(url) + + for item_el in driver.find_elements(By.CSS_SELECTOR, ".card[data-url]"): + name = get_text_by_css(item_el, ".card__title", "Null") + price = get_text_by_css(item_el, ".card-price", "-") + nal = get_text_by_css(item_el, ".card__buttons", "-") + + row = name, price, nal + print(row) + + items.append(row) + + # Обновление номера последней страницы + try: + pages_count_el = driver.find_element(By.CSS_SELECTOR, "*[data-pages-count]") + last_page = int(pages_count_el.get_attribute("data-pages-count")) + + except NoSuchElementException: + break + + page += 1 + + finally: + driver.quit() + + return items + + +def save_goods( + file_name: str | Path, + items: list[tuple[str, str, str]], + encoding="utf-8", +) -> None: + df = pd.DataFrame(items, columns=["Name", "Price", "Nal"]) + df.to_csv(file_name, encoding=encoding) + + +if __name__ == "__main__": + url = "https://www.foxtrot.com.ua/ru/search?query=gazer&filter=_195_588" + items = parse(url) + print(f"Total goods: {len(items)}") + + file_name = f"foxtrot_parser_{dt.datetime.now():%Y-%m-%d}.csv" + print(f"Saved to {file_name}") + save_goods(file_name, items) diff --git a/html_parsing/freelance_habr_com__tasks.py b/html_parsing/freelance_habr_com__tasks.py new file mode 100644 index 000000000..40d04b1fa --- /dev/null +++ b/html_parsing/freelance_habr_com__tasks.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from urllib.parse import urljoin + +import requests +from bs4 import BeautifulSoup + + +rs = requests.get("https://freelance.habr.com/tasks?q=python") +root = BeautifulSoup(rs.content, "html.parser") +urls = [ + (urljoin(rs.url, a["href"]), a.get_text(strip=True)) + for a in root.select(".task__title > a[href]") +] +print(len(urls), urls) +# 25 [('https://freelance.habr.com/tasks/349695', 'Парсер '), ..., 'Доработка бота Telegram на Python')] diff --git a/html_parsing/get_comments__russianfood_com.py b/html_parsing/get_comments__russianfood_com.py index 1c15d2151..31de4cd40 100644 --- a/html_parsing/get_comments__russianfood_com.py +++ b/html_parsing/get_comments__russianfood_com.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.parse import urljoin @@ -11,7 +11,7 @@ import requests -PATTERN_NEXT = re.compile('Следующая →') +PATTERN_NEXT = re.compile("Следующая →") def get_comments(url: str, no_quote=False) -> list: @@ -19,43 +19,43 @@ def get_comments(url: str, no_quote=False) -> list: while True: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") # Перебор комментариев - for comment_text_el in root.select('.comment > div'): + for comment_text_el in root.select(".comment > div"): # Перебор цитат - for blockquote_el in comment_text_el.select('blockquote'): + for blockquote_el in comment_text_el.select("blockquote"): if no_quote: blockquote_el.decompose() continue blockquote_el.replace_with( - "«" + blockquote_el.get_text(separator='\n', strip=True) + "»" + "«" + blockquote_el.get_text(separator="\n", strip=True) + "»" ) - for br in comment_text_el.select('br'): - br.replace_with('\n') + for br in comment_text_el.select("br"): + br.replace_with("\n") - comment_text = comment_text_el.get_text(separator='\n', strip=True) + comment_text = comment_text_el.get_text(separator="\n", strip=True) items.append(comment_text) - a_next = root.find(name='a', text=PATTERN_NEXT) + a_next = root.find(name="a", text=PATTERN_NEXT) if not a_next: break - url = urljoin(rs.url, a_next.get('href')) + url = urljoin(rs.url, a_next.get("href")) return items -if __name__ == '__main__': +if __name__ == "__main__": for url in [ - 'https://www.russianfood.com/recipes/recipe.php?rid=107392', - 'https://www.russianfood.com/recipes/recipe.php?rid=121323', + "https://www.russianfood.com/recipes/recipe.php?rid=107392", + "https://www.russianfood.com/recipes/recipe.php?rid=121323", ]: print(url) for i, comment_text in enumerate(get_comments(url), 1): print(i, repr(comment_text)) - print('\n' + '-' * 100 + '\n') \ No newline at end of file + print("\n" + "-" * 100 + "\n") diff --git a/html_parsing/get_comments__stopgame_ru.py b/html_parsing/get_comments__stopgame_ru.py index 98d34de69..c3d11d58f 100644 --- a/html_parsing/get_comments__stopgame_ru.py +++ b/html_parsing/get_comments__stopgame_ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup @@ -10,40 +10,40 @@ def get_comments(url: str, no_quote=False) -> list: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") items = [] # Перебор комментариев - for comment_text_el in root.select('.comment-text'): + for comment_text_el in root.select(".comment-text"): # Перебор цитат - for blockquote_el in comment_text_el.select('blockquote'): + for blockquote_el in comment_text_el.select("blockquote"): if no_quote: blockquote_el.decompose() continue blockquote_el.replace_with( - "«" + blockquote_el.get_text(separator='\n', strip=True) + "»" + "«" + blockquote_el.get_text(separator="\n", strip=True) + "»" ) - for br in comment_text_el.select('br'): - br.replace_with('\n') + for br in comment_text_el.select("br"): + br.replace_with("\n") - comment_text = comment_text_el.get_text(separator='\n', strip=True) + comment_text = comment_text_el.get_text(separator="\n", strip=True) items.append(comment_text) return items -if __name__ == '__main__': +if __name__ == "__main__": for url in [ - 'https://stopgame.ru/show/102770/atom_rpg_review', - 'https://stopgame.ru/show/43787/project_dark_review', - 'https://stopgame.ru/show/82379/dark_souls_iii_videoreview', + "https://stopgame.ru/show/102770/atom_rpg_review", + "https://stopgame.ru/show/43787/project_dark_review", + "https://stopgame.ru/show/82379/dark_souls_iii_videoreview", ]: print(url) for i, comment_text in enumerate(get_comments(url), 1): print(i, repr(comment_text)) - print('\n' + '-' * 100 + '\n') + print("\n" + "-" * 100 + "\n") diff --git a/html_parsing/get_customers_of_bink.py b/html_parsing/get_customers_of_bink.py new file mode 100644 index 000000000..9d7306231 --- /dev/null +++ b/html_parsing/get_customers_of_bink.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests +from bs4 import BeautifulSoup + + +url = "http://www.radgametools.com/binkgames.htm" +rs = requests.get(url) +root = BeautifulSoup(rs.content, "html.parser") + +items = [x.get_text(strip=True) for x in root.select(".gameslist > dl > dt")] +print(f"Items ({len(items)}): {items}") +# Items (403): ['1C', '2015 Inc', '2XL Games', ..., 'Zombie', 'Zoë Mode', 'Zoo Digital Publishing'] diff --git a/html_parsing/get_game_genres/_view_db_Dump.py b/html_parsing/get_game_genres/_view_db_Dump.py deleted file mode 100644 index 4a7783276..000000000 --- a/html_parsing/get_game_genres/_view_db_Dump.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from db import Dump - - -print('Total:', Dump.select().count()) - -genres = Dump.get_all_genres() -print(f'Genres ({len(genres)}): {genres}') - -games = Dump.get_all_games() -print(f'Games ({len(games)}): {games}') - -sites = Dump.get_all_sites() -print(f'Sites ({len(sites)}): {sites}') - -print() - -max_width = max(len(x.site) for x in Dump.select(Dump.site).distinct()) -fmt_str = ' {:<%d} : {}' % max_width - -game_by_dump = defaultdict(list) -for x in Dump.get(): - game_by_dump[x.name].append(x) - -for game, dumps in game_by_dump.items(): - print(game) - - for dump in dumps: - print( - fmt_str.format(dump.site, dump.genres) - ) - - print() diff --git a/html_parsing/get_game_genres/_view_db_site_by_genres.py b/html_parsing/get_game_genres/_view_db_site_by_genres.py deleted file mode 100644 index 72f4621d2..000000000 --- a/html_parsing/get_game_genres/_view_db_site_by_genres.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from db import Dump - - -genres = Dump.get_all_genres() -print(f'Genres ({len(genres)}): {genres}') - -sites = Dump.get_all_sites() -print(f'Sites ({len(sites)}): {sites}') - -print() - -max_width = max(len(x.site) for x in Dump.select(Dump.site).distinct()) -fmt_str = '{:<%d} : ({}) {}' % max_width - -site_by_genres = defaultdict(list) -for x in Dump.get(): - site_by_genres[x.site] += x.genres - -for k, v in site_by_genres.items(): - site_by_genres[k] = sorted(set(v)) - -print(f'Total {len(site_by_genres)}:') - -for site, genres in site_by_genres.items(): - print(fmt_str.format(site, len(genres), genres)) diff --git a/html_parsing/get_game_genres/common.py b/html_parsing/get_game_genres/common.py deleted file mode 100644 index 90ec51034..000000000 --- a/html_parsing/get_game_genres/common.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import re -from pathlib import Path - - -def smart_comparing_names(name_1: str, name_2: str) -> bool: - """ - Функция для сравнивания двух названий игр. - Возвращает True, если совпадают, иначе -- False. - - """ - - # Приведение строк к одному регистру - name_1 = name_1.lower() - name_2 = name_2.lower() - - def remove_postfix(text: str, postfix='(dlc)') -> str: - if text.endswith(postfix): - text = text[:-len(postfix)] - - return text - - # Удаление символов кроме буквенных, цифр и _: "the witcher®3:___ вася! wild hunt" -> "thewitcher3___васяwildhunt" - def clear_name(name: str) -> str: - import re - return re.sub(r'\W|the', '', name) - - name_1 = remove_postfix(name_1) - name_2 = remove_postfix(name_2) - - return clear_name(name_1) == clear_name(name_2) - - -def get_uniques(items: list) -> list: - return list(set(items)) - - -def pretty_path(path: str) -> str: - return str(Path(path).resolve()) - - -def get_current_datetime_str(fmt='%d%m%y%H%M%S') -> str: - return DT.datetime.now().strftime(fmt) - - -# SOURCE: https://github.com/django/django/blob/03dbdfd9bbbbd0b0172aad648c6bbe3f39541137/django/utils/text.py#L221 -def get_valid_filename(s): - """ - Return the given string converted to a string that can be used for a clean - filename. Remove leading and trailing spaces; convert other spaces to - underscores; and remove anything that is not an alphanumeric, dash, - underscore, or dot. - >>> get_valid_filename("john's portrait in 2004.jpg") - 'johns_portrait_in_2004.jpg' - """ - s = str(s).strip().replace(' ', '_') - return re.sub(r'(?u)[^-\w.]', '', s) - - -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0' - -DIR = Path(__file__).resolve().parent -DIR_ERRORS = str(DIR / 'errors') -DIR_LOGS = str(DIR / 'logs') - -NEED_LOGS = True -LOG_FORMAT = '[%(asctime)s] %(levelname)-8s %(message)s' - - -TEST_GAMES = [ - 'Hellgate: London', - 'The Incredible Adventures of Van Helsing', - 'Dark Souls: Prepare to Die Edition', - 'Twin Sector', - 'Call of Cthulhu: Dark Corners of the Earth', -] - - -def _common_test(get_game_genres, sleep=1, max_number=None): - if max_number is None: - max_number = len(TEST_GAMES) - - import time - - for name in TEST_GAMES[:max_number]: - print(f'Search {name!r}...') - print(f' Genres: {get_game_genres(name)}\n') - - time.sleep(sleep) diff --git a/html_parsing/get_game_genres/common_utils.py b/html_parsing/get_game_genres/common_utils.py deleted file mode 100644 index efe7c30a0..000000000 --- a/html_parsing/get_game_genres/common_utils.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from glob import glob -import importlib.util -from pathlib import Path -import sys -import threading -from typing import Dict, Callable - -# For import parsers/* -sys.path.append('parsers') - - -from common import DIR_LOGS - - -def module_from_file(file_path: str): - module_name = Path(file_path).stem - spec = importlib.util.spec_from_file_location(module_name, file_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - if module_name not in sys.modules: - sys.modules[module_name] = module - - return module - - -def get_funcs_parsers() -> Dict[str, Callable]: - data = dict() - - for file_name in glob('parsers/*.py'): - module = module_from_file(file_name) - if 'get_game_genres' not in dir(module): - continue - - data[module.__name__] = module.get_game_genres - - return data - - -def get_parsers() -> list: - items = [] - - for file_name in glob('parsers/*.py'): - module = module_from_file(file_name) - for attr in dir(module): - if not attr.endswith('_Parser'): - continue - - cls = getattr(module, attr) - items.append( - cls.instance() - ) - - return items - - -# Parser from https://github.com/gil9red/played_games/blob/f23777a1368f9124450bedac036791068d8ca099/mini_played_games_parser.py#L7 -def parse_played_games(text: str, silence: bool=False) -> dict: - """ - Функция для парсинга списка игр. - """ - - FINISHED_GAME = 'FINISHED_GAME' - NOT_FINISHED_GAME = 'NOT_FINISHED_GAME' - FINISHED_WATCHED = 'FINISHED_WATCHED' - NOT_FINISHED_WATCHED = 'NOT_FINISHED_WATCHED' - - FLAG_BY_CATEGORY = { - ' ': FINISHED_GAME, - '- ': NOT_FINISHED_GAME, - ' -': NOT_FINISHED_GAME, - ' @': FINISHED_WATCHED, - '@ ': FINISHED_WATCHED, - '-@': NOT_FINISHED_WATCHED, - '@-': NOT_FINISHED_WATCHED, - } - - # Регулярка вытаскивает выражения вида: 1, 2, 3 или 1-3, или римские цифры: III, IV - import re - PARSE_GAME_NAME_PATTERN = re.compile( - r'(\d+(, *?\d+)+)|(\d+ *?- *?\d+)|([MDCLXVI]+(, ?[MDCLXVI]+)+)', - flags=re.IGNORECASE - ) - - def parse_game_name(game_name: str) -> list: - """ - Функция принимает название игры и пытается разобрать его, после возвращает список названий. - У некоторых игр в названии может указываться ее части или диапазон частей, поэтому для правильного - составления списка игр такие случаи нужно обрабатывать. - Пример: - "Resident Evil 4, 5, 6" -> ["Resident Evil 4", "Resident Evil 5", "Resident Evil 6"] - "Resident Evil 1-3" -> ["Resident Evil", "Resident Evil 2", "Resident Evil 3"] - "Resident Evil 4" -> ["Resident Evil 4"] - """ - - match = PARSE_GAME_NAME_PATTERN.search(game_name) - if match is None: - return [game_name] - - seq_str = match.group(0) - - # "Resident Evil 4, 5, 6" -> "Resident Evil" - # For not valid "Trollface Quest 1-7-8" -> "Trollface Quest" - index = game_name.index(seq_str) - base_name = game_name[:index].strip() - - seq_str = seq_str.replace(' ', '') - - if ',' in seq_str: - # '1,2,3' -> ['1', '2', '3'] - seq = seq_str.split(',') - - elif '-' in seq_str: - seq = seq_str.split('-') - - # ['1', '7'] -> [1, 7] - seq = list(map(int, seq)) - - # [1, 7] -> ['1', '2', '3', '4', '5', '6', '7'] - seq = list(map(str, range(seq[0], seq[1] + 1))) - - else: - return [game_name] - - # Сразу проверяем номер игры в серии и если она первая, то не добавляем в названии ее номер - return [base_name if num == '1' else base_name + " " + num for num in seq] - - platforms = dict() - platform = None - - for line in text.splitlines(): - line = line.rstrip() - if not line: - continue - - flag = line[:2] - if flag not in FLAG_BY_CATEGORY and line.endswith(':'): - platform_name = line[:-1] - - platform = { - FINISHED_GAME: [], - NOT_FINISHED_GAME: [], - FINISHED_WATCHED: [], - NOT_FINISHED_WATCHED: [], - } - platforms[platform_name] = platform - - continue - - if not platform: - continue - - category_name = FLAG_BY_CATEGORY.get(flag) - if not category_name: - if not silence: - print('Странный формат строки: "{}"'.format(line)) - continue - - category = platform[category_name] - - game_name = line[2:] - for game in parse_game_name(game_name): - if game in category: - if not silence: - print('Предотвращено добавление дубликата игры "{}"'.format(game)) - continue - - category.append(game) - - return platforms - - -# SOURCE: https://github.com/gil9red/price_of_games/blob/5432514a3322d359ca8ef509f5ef173ce6969203/common.py#L482 -def get_games_list() -> list: - import requests - rs = requests.get('https://gist.github.com/gil9red/2f80a34fb601cd685353') - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'html.parser') - href = root.select_one('.file-actions > a')['href'] - - from urllib.parse import urljoin - raw_url = urljoin(rs.url, href) - - rs = requests.get(raw_url) - content_gist = rs.text - - platforms = parse_played_games(content_gist) - - # Пройденные игры - finished_game_list = platforms['PC']['FINISHED_GAME'] - - # Просмотренные игры - finished_watched_game_list = platforms['PC']['FINISHED_WATCHED'] - - return sorted(set(finished_game_list + finished_watched_game_list)) - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py#L7 -def wait(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0): - from datetime import timedelta, datetime - from itertools import cycle - import sys - import time - - try: - progress_bar = cycle('|/-\\|/-\\') - - today = datetime.today() - timeout_date = today + timedelta( - days=days, seconds=seconds, microseconds=microseconds, - milliseconds=milliseconds, minutes=minutes, hours=hours, weeks=weeks - ) - - def str_timedelta(td: timedelta) -> str: - td = str(td) - - # Remove ms - # 0:01:40.123000 -> 0:01:40 - if '.' in td: - td = td[:td.rindex('.')] - - # 0:01:40 -> 00:01:40 - if td.startswith('0:'): - td = '00:' + td[2:] - - return td - - while today <= timeout_date: - left = timeout_date - today - left = str_timedelta(left) - - print('\r' + ' ' * 100 + '\r', end='') - print('[{}] Time left to wait: {}'.format(next(progress_bar), left), end='') - sys.stdout.flush() - - # Delay 1 seconds - time.sleep(1) - - today = datetime.today() - - print('\r' + ' ' * 100 + '\r', end='') - - except KeyboardInterrupt: - print() - print('Waiting canceled') - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/163c91d6882b548c904ad40703dac00c0a64e5a2/logger_example.py#L7 -def get_logger(name='dump.txt', encoding='utf-8'): - Path(DIR_LOGS).mkdir(parents=True, exist_ok=True) - - file = DIR_LOGS + '/' + name - - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(levelname)-8s %(message)s') - - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10_000_000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/c06ff5fb6a0abfb5a41652f71d7adf7ee414e4c8/multithreading__threading__examples/atomic_counter.py#L14 -class AtomicCounter: - def __init__(self, initial=0): - self.value = initial - self._lock = threading.Lock() - - def inc(self, num=1): - with self._lock: - self.value += num - return self.value - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/f0403620f7948306ad9e34a373f2aabc0237fb2a/seconds_to_str.py -def seconds_to_str(seconds: int) -> str: - hh, mm = divmod(seconds, 3600) - mm, ss = divmod(mm, 60) - return "%02d:%02d:%02d" % (hh, mm, ss) - - -def print_parsers(parsers: list, log=print): - max_width = len(max([x.get_site_name() for x in parsers], key=len)) - fmt_str = ' {:<%d} : {}' % max_width - items = [ - fmt_str.format(parser.get_site_name(), parser.__class__) - for parser in parsers - ] - - log(f'Parsers ({len(parsers)}):\n' + "\n".join(items)) - - -if __name__ == "__main__": - items = get_games_list() - print(f'Games ({len(items)}): {", ".join(items[:5])}...') - # Games (743): 35MM, 60 Seconds!, A Bird Story, A Plague Tale: Innocence, A Story About My Uncle... - - print() - - parsers = get_parsers() - print_parsers(parsers) - # Parsers (14): - # ag_ru : - # gamebomb_ru : - # gamefaqs_gamespot_com : - # gameguru_ru : - # gamer_info_com : - # gamespot_com : - # igromania_ru : - # iwantgames_ru : - # metacritic_com : - # mobygames_com : - # playground_ru : - # spong_com : - # stopgame_ru : - # store_steampowered_com : - - print() - - game = 'Dead Space' - print(f'Search genres for {game!r}:') - - for parser in get_parsers(): - parser._need_logs = False - print(f" {parser.get_site_name():<25}: {parser.get_game_genres(game)}") - - # ag_ru : ['Экшены', 'Шутеры'] - # gamebomb_ru : ['Боевик-приключения', 'Шутер'] - # gamefaqs_gamespot_com : ['Action', 'Arcade', 'Shooter', 'Third-Person'] - # gameguru_ru : ['Экшен', 'Шутер'] - # gamer_info_com : ['ужасы', 'action'] - # gamespot_com : ['Action', '3D', 'Shooter', 'Third-Person'] - # igromania_ru : ['Боевик от третьего лица', 'Боевик', 'Ужасы'] - # iwantgames_ru : [] - # metacritic_com : ['Action', 'Shooter', 'Sci-Fi', 'Arcade', 'Third-Person'] - # mobygames_com : ['Action'] - # playground_ru : ['От третьего лица', 'Экшен', 'Космос', 'Ужасы'] - # spong_com : ['Adventure: Survival Horror'] - # stopgame_ru : [] - # store_steampowered_com : ['Action'] - - # print() - # - # for site, get_game_genres in get_funcs_parsers().items(): - # print(f"{site:<25}: {get_game_genres('Dead Space')}") - # # ag_ru : ['Шутеры', 'Экшены'] - # # gamebomb_ru : ['Боевик-приключения', 'Шутер'] - # # gamefaqs_gamespot_com : ['Arcade', 'Shooter', 'Action', 'Third-Person'] - # # gameguru_ru : ['Экшен', 'Шутер'] - # # gamer_info_com : ['ужасы', 'action'] - # # gamespot_com : ['3D', 'Shooter', 'Action', 'Third-Person'] - # # igromania_ru : ['Боевик', 'Ужасы', 'Боевик от третьего лица'] - # # iwantgames_ru : [] - # # metacritic_com : ['Arcade', 'Third-Person', 'Sci-Fi', 'Action', 'Shooter'] - # # mobygames_com : ['Action'] - # # playground_ru : ['Ужасы', 'От третьего лица', 'Космос', 'Экшен'] - # # spong_com : ['Adventure: Survival Horror'] - # # stopgame_ru : [] - # # store_steampowered_com : ['Action'] diff --git a/html_parsing/get_game_genres/db.py b/html_parsing/get_game_genres/db.py deleted file mode 100644 index 17f30968f..000000000 --- a/html_parsing/get_game_genres/db.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -import json -from typing import Dict, List, Iterable, Optional -from pathlib import Path - -# pip install peewee -from peewee import * -from playhouse.sqliteq import SqliteQueueDatabase - - -DB_DIR_NAME = str(Path(__file__).resolve().parent / 'database') -DB_FILE_NAME = str(Path(DB_DIR_NAME) / 'games.sqlite') - - -Path(DB_DIR_NAME).mkdir(parents=True, exist_ok=True) - - -def db_create_backup(backup_dir='backup', date_fmt='%d%m%y'): - import datetime as DT - import shutil - - backup_path = Path(backup_dir) - backup_path.mkdir(parents=True, exist_ok=True) - - zip_name = DT.datetime.today().strftime(date_fmt) - zip_name = backup_path / zip_name - - shutil.make_archive( - zip_name, - 'zip', - DB_DIR_NAME - ) - - -class ListField(Field): - def python_value(self, value: str) -> List: - return json.loads(value, encoding='utf-8') - - def db_value(self, value: Optional[Iterable]) -> str: - if value is not None: - if isinstance(value, str): - return value - - if not isinstance(value, list): - raise Exception('Type must be a list') - - return json.dumps(value, ensure_ascii=False) - - -# Simple Sqlite -# db = SqliteDatabase( -# DB_FILE_NAME, -# pragmas={ -# 'foreign_keys': 1, # Ensure foreign-key constraints are enforced. -# 'journal_mode': 'wal', # WAL-mode -# 'cache_size': -1024 * 64 # 64MB page-cache -# } -# ) -# -# This working with multithreading -# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq -db = SqliteQueueDatabase( - DB_FILE_NAME, - pragmas={ - 'foreign_keys': 1, - 'journal_mode': 'wal', # WAL-mode - 'cache_size': -1024 * 64 # 64MB page-cache - }, - use_gevent=False, # Use the standard library "threading" module. - autostart=True, - queue_max_size=64, # Max. # of pending writes that can accumulate. - results_timeout=5.0 # Max. time to wait for query to be executed. -) - - -class BaseModel(Model): - class Meta: - database = db - - -class Dump(BaseModel): - name = CharField() - site = CharField() - genres = ListField() - - @classmethod - def exists(cls, site: str, name: str) -> bool: - return cls.select().where( - cls.site == site, cls.name == name - ).exists() - - @classmethod - def add(cls, site: str, name: str, genres: list): - if not cls.exists(site, name): - cls.create(site=site, name=name, genres=genres) - - @classmethod - def get(cls) -> List['Dump']: - return cls.select().where(cls.genres != '[]').order_by(cls.name) - - @classmethod - def get_games_by_site(cls, site: str) -> List['Dump']: - return list(cls.select().where(cls.site == site).order_by(cls.name)) - - @classmethod - def get_genres_by_game(cls, game_name: str) -> List[str]: - items = [] - - for dump in cls.select().where(cls.name == game_name): - items += dump.genres - - return sorted(set(items)) - - @classmethod - def get_all_genres(cls) -> List[str]: - items = [] - - for dump in cls.select(): - items += dump.genres - - return sorted(set(items)) - - @classmethod - def get_all_games(cls) -> List[str]: - return [ - dump.name - for dump in cls.select(cls.name).order_by(cls.name).distinct() - ] - - @classmethod - def get_all_sites(cls) -> List[str]: - return [ - dump.site - for dump in cls.select(cls.site).order_by(cls.site).distinct() - ] - - @classmethod - def dump(cls) -> Dict[str, List[str]]: - game_by_genres = defaultdict(list) - - for dump in cls.select().order_by(cls.name): - game_by_genres[dump.name] += dump.genres - - for k, v in game_by_genres.items(): - game_by_genres[k] = sorted(set(v)) - - return game_by_genres - - class Meta: - indexes = ( - (("name", "site"), True), - ) - - def __repr__(self): - return f'Dump(name={self.name!r}, site={self.site!r}, genres={self.genres})' - - def __str__(self): - return repr(self) - - -db.connect() -db.create_tables([Dump]) - - -if __name__ == '__main__': - print('Total:', Dump.select().count()) - - genres = Dump.get_all_genres() - print(f'Genres ({len(genres)}): {genres}') - - games = Dump.get_all_games() - print(f'Games ({len(games)}): {games}') - - sites = Dump.get_all_sites() - print(f'Sites ({len(sites)}): {sites}') - - print() - - print(Dump.get_genres_by_game('Dead Space')) - # ['3D', 'Action', 'Adventure: Survival Horror', 'Arcade', 'Sci-Fi', 'Shooter', 'Third-Person', 'action', - # 'Боевик', 'Боевик от третьего лица', 'Боевик-приключения', 'Космос', 'От третьего лица', 'Ужасы', 'Шутер', - # 'Шутеры', 'Экшен', 'Экшены', 'ужасы'] - - # print() - # - # for x in Dump.get(): - # print(x) - # - # print() - - # - # For testing - # - # Dump.add(site='foo', name='123', genres=['RPG', 'Action']) - # Dump.add(site='foo', name='456', genres=['RPG']) - # - # for dump in Dump.select(): - # print(dump) - # - # # Dump(name='123', site='foo', genres=['RPG', 'Action']) - # # Dump(name='456', site='foo', genres=['RPG']) - # - # print() - # - # print(Dump.get_games_by_site('foo')) - # # [Dump(name='123', site='foo', genres=['RPG', 'Action']), Dump(name='456', site='foo', genres=['RPG'])] diff --git a/html_parsing/get_game_genres/export_import/data/games.json b/html_parsing/get_game_genres/export_import/data/games.json deleted file mode 100644 index fdc4e9037..000000000 --- a/html_parsing/get_game_genres/export_import/data/games.json +++ /dev/null @@ -1,85632 +0,0 @@ -[ - { - "id": 1, - "name": "35MM", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Квест" - ] - }, - { - "id": 2, - "name": "35MM", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 3, - "name": "35MM", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4, - "name": "35MM", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5, - "name": "35MM", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 6, - "name": "35MM", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 7, - "name": "60 Seconds!", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Стратегия", - "Симулятор", - "Квест" - ] - }, - { - "id": 8, - "name": "35MM", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 9, - "name": "35MM", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Постапокалипсис", - "Выживание", - "Экшен", - "От первого лица" - ] - }, - { - "id": 10, - "name": "60 Seconds!", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Симулятор" - ] - }, - { - "id": 11, - "name": "60 Seconds!", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 12, - "name": "60 Seconds!", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Казуальные", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 13, - "name": "35MM", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 14, - "name": "A Bird Story", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 15, - "name": "60 Seconds!", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 16, - "name": "60 Seconds!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 17, - "name": "60 Seconds!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 18, - "name": "35MM", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 19, - "name": "60 Seconds!", - "site": "playground_ru", - "genres": [ - "Инди", - "Аркада", - "Постапокалипсис", - "Выживание" - ] - }, - { - "id": 20, - "name": "A Bird Story", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 21, - "name": "A Plague Tale: Innocence", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 22, - "name": "A Bird Story", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 23, - "name": "A Bird Story", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 24, - "name": "60 Seconds!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 25, - "name": "35MM", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 26, - "name": "A Bird Story", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 27, - "name": "A Bird Story", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 28, - "name": "35MM", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 29, - "name": "A Story About My Uncle", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 30, - "name": "A Bird Story", - "site": "playground_ru", - "genres": [ - "Ретро", - "Адвенчура", - "Инди" - ] - }, - { - "id": 31, - "name": "A Bird Story", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 32, - "name": "A Plague Tale: Innocence", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 33, - "name": "35MM", - "site": "spong_com", - "genres": [] - }, - { - "id": 34, - "name": "A Plague Tale: Innocence", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 35, - "name": "A Plague Tale: Innocence", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 36, - "name": "Adam Wolfe", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 37, - "name": "A Plague Tale: Innocence", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 38, - "name": "A Bird Story", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 39, - "name": "60 Seconds!", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Casual", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 40, - "name": "60 Seconds!", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 41, - "name": "A Plague Tale: Innocence", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 42, - "name": "A Plague Tale: Innocence", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Стелс" - ] - }, - { - "id": 43, - "name": "A Plague Tale: Innocence", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 44, - "name": "A Story About My Uncle", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 45, - "name": "60 Seconds!", - "site": "spong_com", - "genres": [] - }, - { - "id": 46, - "name": "A Story About My Uncle", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 47, - "name": "A Story About My Uncle", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 48, - "name": "Alan Wake", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 49, - "name": "A Story About My Uncle", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Аркадные", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 50, - "name": "A Plague Tale: Innocence", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 51, - "name": "Adam Wolfe", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 52, - "name": "A Bird Story", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 53, - "name": "A Story About My Uncle", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Платформер" - ] - }, - { - "id": 54, - "name": "Alan Wake's American Nightmare", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 55, - "name": "A Story About My Uncle", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 56, - "name": "A Story About My Uncle", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 57, - "name": "A Bird Story", - "site": "spong_com", - "genres": [] - }, - { - "id": 58, - "name": "Adam Wolfe", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 59, - "name": "35MM", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 60, - "name": "Adam Wolfe", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 61, - "name": "A Bird Story", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 62, - "name": "Adam Wolfe", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 63, - "name": "60 Seconds!", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 64, - "name": "Alice: Madness Returns", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 65, - "name": "Adam Wolfe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 66, - "name": "A Story About My Uncle", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 67, - "name": "Alan Wake", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 68, - "name": "Alan Wake", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 69, - "name": "Adam Wolfe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 70, - "name": "A Plague Tale: Innocence", - "site": "spong_com", - "genres": [] - }, - { - "id": 71, - "name": "A Plague Tale: Innocence", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 72, - "name": "Adam Wolfe", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 73, - "name": "Alan Wake", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 74, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 75, - "name": "Alan Wake", - "site": "ag_ru", - "genres": [] - }, - { - "id": 76, - "name": "60 Seconds!", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 77, - "name": "Alan Wake's American Nightmare", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 78, - "name": "A Story About My Uncle", - "site": "spong_com", - "genres": [] - }, - { - "id": 79, - "name": "Alan Wake", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 80, - "name": "Alan Wake", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 81, - "name": "Adam Wolfe", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 82, - "name": "Alan Wake's American Nightmare", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 83, - "name": "Alan Wake's American Nightmare", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 84, - "name": "Alan Wake", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 85, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 86, - "name": "A Plague Tale: Innocence", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 87, - "name": "A Story About My Uncle", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 88, - "name": "Alice: Madness Returns", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 89, - "name": "Alan Wake's American Nightmare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 90, - "name": "Alien Rage - Unlimited", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 91, - "name": "Adam Wolfe", - "site": "spong_com", - "genres": [] - }, - { - "id": 92, - "name": "Alice: Madness Returns", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 93, - "name": "Alan Wake's American Nightmare", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 94, - "name": "Alan Wake's American Nightmare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 95, - "name": "Alice: Madness Returns", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 96, - "name": "Alan Wake", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 97, - "name": "Alan Wake's American Nightmare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 98, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 99, - "name": "Alien: Isolation", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 100, - "name": "Adam Wolfe", - "site": "gamespot_com", - "genres": [ - "Puzzle", - "Hidden Object" - ] - }, - { - "id": 101, - "name": "A Bird Story", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 102, - "name": "Alice: Madness Returns", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 103, - "name": "A Story About My Uncle", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 104, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 105, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 106, - "name": "Alice: Madness Returns", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 107, - "name": "Alice: Madness Returns", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 108, - "name": "Alan Wake's American Nightmare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 109, - "name": "Alice: Madness Returns", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 110, - "name": "Aliens vs. Predator (2010)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 111, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 112, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 113, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 114, - "name": "A Bird Story", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 115, - "name": "Alan Wake", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 116, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 117, - "name": "Aliens: Colonial Marines", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 118, - "name": "Alan Wake", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 119, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 120, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 121, - "name": "Alien Rage - Unlimited", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 122, - "name": "Alien Rage - Unlimited", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 123, - "name": "Adam Wolfe", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 124, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 125, - "name": "Alice: Madness Returns", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 126, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 127, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 128, - "name": "Alien Rage - Unlimited", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 129, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 130, - "name": "A Plague Tale: Innocence", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 131, - "name": "Alien: Isolation", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 132, - "name": "A Plague Tale: Innocence", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 133, - "name": "Alan Wake's American Nightmare", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 134, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 135, - "name": "Alone in the Dark (2008)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 136, - "name": "Alan Wake's American Nightmare", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 137, - "name": "Alien: Isolation", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 138, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 139, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 140, - "name": "Alien Rage - Unlimited", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 141, - "name": "Alien Rage - Unlimited", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 142, - "name": "Alan Wake", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 143, - "name": "Alien: Isolation", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 144, - "name": "Aliens vs. Predator (2010)", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 145, - "name": "Alice: Madness Returns", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 146, - "name": "Amnesia: A Machine for Pigs", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 147, - "name": "Alien Rage - Unlimited", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 148, - "name": "Aliens vs. Predator (2010)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 149, - "name": "Alien Rage - Unlimited", - "site": "playground_ru", - "genres": [] - }, - { - "id": 150, - "name": "Alien: Isolation", - "site": "ag_ru", - "genres": [] - }, - { - "id": 151, - "name": "Alien: Isolation", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 152, - "name": "Amnesia: The Dark Descent", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 153, - "name": "Aliens: Colonial Marines", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 154, - "name": "Aliens vs. Predator (2010)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 155, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 156, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 157, - "name": "Alien: Isolation", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 158, - "name": "Alan Wake's American Nightmare", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 159, - "name": "Among The Sleep", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 160, - "name": "Aliens vs. Predator (2010)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 161, - "name": "Alien: Isolation", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ужасы" - ] - }, - { - "id": 162, - "name": "A Story About My Uncle", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 163, - "name": "Alice: Madness Returns", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 164, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 165, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 166, - "name": "Aliens vs. Predator (2010)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 167, - "name": "Aliens: Colonial Marines", - "site": "gamer_info_com", - "genres": [ - "командные шутеры", - "FPS" - ] - }, - { - "id": 168, - "name": "Alien Rage - Unlimited", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 169, - "name": "Angry Birds", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 170, - "name": "Aliens vs. Predator (2010)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 171, - "name": "Alice: Madness Returns", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 172, - "name": "Aliens: Colonial Marines", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 173, - "name": "A Story About My Uncle", - "site": "metacritic_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 174, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 175, - "name": "Aliens: Colonial Marines", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 176, - "name": "Aliens vs. Predator (2010)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 177, - "name": "Alone in the Dark (2008)", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 178, - "name": "Angry Birds Rio", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 179, - "name": "Alien Rage - Unlimited", - "site": "spong_com", - "genres": [] - }, - { - "id": 180, - "name": "Aliens: Colonial Marines", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 181, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 182, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 183, - "name": "Alien: Isolation", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 184, - "name": "Aliens: Colonial Marines", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 185, - "name": "Angry Birds Seasons", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 186, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 187, - "name": "Adam Wolfe", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 188, - "name": "Amnesia: A Machine for Pigs", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 189, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 190, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 191, - "name": "Alien: Isolation", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure: Survival Horror" - ] - }, - { - "id": 192, - "name": "Aliens: Colonial Marines", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 193, - "name": "Alone in the Dark (2008)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 194, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 195, - "name": "Alone in the Dark (2008)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 196, - "name": "Angry Birds Space", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 197, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 198, - "name": "Aliens vs. Predator (2010)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 199, - "name": "Amnesia: The Dark Descent", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 200, - "name": "Alien Rage - Unlimited", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 201, - "name": "Alone in the Dark (2008)", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 202, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 203, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 204, - "name": "Aliens vs. Predator (2010)", - "site": "spong_com", - "genres": [] - }, - { - "id": 205, - "name": "Alan Wake", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 206, - "name": "Angry Birds Star Wars", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 207, - "name": "Alone in the Dark (2008)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 208, - "name": "Amnesia: A Machine for Pigs", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения" - ] - }, - { - "id": 209, - "name": "Amnesia: A Machine for Pigs", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 210, - "name": "Among The Sleep", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 211, - "name": "Alone in the Dark (2008)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 212, - "name": "Aliens: Colonial Marines", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 213, - "name": "Angry Birds Star Wars II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 214, - "name": "Aliens: Colonial Marines", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 215, - "name": "Adam Wolfe", - "site": "metacritic_com", - "genres": [ - "General", - "Hidden Object", - "Puzzle" - ] - }, - { - "id": 216, - "name": "Amnesia: A Machine for Pigs", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 217, - "name": "Alone in the Dark (2008)", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Адвенчура", - "Экшен", - "От первого лица" - ] - }, - { - "id": 218, - "name": "Amnesia: The Dark Descent", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 219, - "name": "Alien: Isolation", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "Survival", - "Adventure" - ] - }, - { - "id": 220, - "name": "Amnesia: A Machine for Pigs", - "site": "ag_ru", - "genres": [] - }, - { - "id": 221, - "name": "Amnesia: The Dark Descent", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 222, - "name": "Armored Kitten", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 223, - "name": "Alien Rage - Unlimited", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 224, - "name": "Angry Birds", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 225, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 226, - "name": "Amnesia: A Machine for Pigs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 227, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 228, - "name": "Aliens vs. Predator (2010)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 229, - "name": "Among The Sleep", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 230, - "name": "Amnesia: The Dark Descent", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 231, - "name": "Ashes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 232, - "name": "Amnesia: A Machine for Pigs", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 233, - "name": "Amnesia: The Dark Descent", - "site": "ag_ru", - "genres": [] - }, - { - "id": 234, - "name": "Among The Sleep", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 235, - "name": "Alan Wake's American Nightmare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 236, - "name": "Alone in the Dark (2008)", - "site": "spong_com", - "genres": [] - }, - { - "id": 237, - "name": "Amnesia: The Dark Descent", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 238, - "name": "Angry Birds Rio", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 239, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 240, - "name": "Alone in the Dark (2008)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 241, - "name": "Angry Birds", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 242, - "name": "Alien: Isolation", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 243, - "name": "Among The Sleep", - "site": "gamefaqs_gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 244, - "name": "Amnesia: The Dark Descent", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 245, - "name": "Among The Sleep", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 246, - "name": "Aliens: Colonial Marines", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 247, - "name": "Angry Birds", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Головоломки/Пазл" - ] - }, - { - "id": 248, - "name": "Angry Birds Seasons", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 249, - "name": "BLACK ROSE", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 250, - "name": "Among The Sleep", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 251, - "name": "Angry Birds Rio", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 252, - "name": "Amnesia: A Machine for Pigs", - "site": "spong_com", - "genres": [] - }, - { - "id": 253, - "name": "Amnesia: A Machine for Pigs", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 254, - "name": "Angry Birds", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 255, - "name": "Alice: Madness Returns", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 256, - "name": "Aliens vs. Predator (2010)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 257, - "name": "Alan Wake", - "site": "metacritic_com", - "genres": [ - "Modern", - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 258, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 259, - "name": "Angry Birds", - "site": "ag_ru", - "genres": [ - "Экшены", - "Головоломки", - "Аркадные" - ] - }, - { - "id": 260, - "name": "Among The Sleep", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Экшен", - "Виртуальная реальность", - "От первого лица" - ] - }, - { - "id": 261, - "name": "Bad Dream: Bridge", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 262, - "name": "Angry Birds Space", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 263, - "name": "Angry Birds Rio", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 264, - "name": "Angry Birds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 265, - "name": "Angry Birds Seasons", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 266, - "name": "Bad Dream: Butcher", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 267, - "name": "Amnesia: The Dark Descent", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 268, - "name": "Angry Birds Rio", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 269, - "name": "Amnesia: The Dark Descent", - "site": "spong_com", - "genres": [] - }, - { - "id": 270, - "name": "Angry Birds Star Wars", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 271, - "name": "Angry Birds Rio", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 272, - "name": "Aliens: Colonial Marines", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 273, - "name": "Angry Birds", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 274, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 275, - "name": "Angry Birds Seasons", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 276, - "name": "Alone in the Dark (2008)", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 277, - "name": "Bad Dream: Coma", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 278, - "name": "Angry Birds Rio", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 279, - "name": "Angry Birds Space", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 280, - "name": "Angry Birds Star Wars II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 281, - "name": "Among The Sleep", - "site": "spong_com", - "genres": [] - }, - { - "id": 282, - "name": "Angry Birds Seasons", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 283, - "name": "Angry Birds Seasons", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Головоломки", - "Аркадные" - ] - }, - { - "id": 284, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 285, - "name": "Bad Dream: Cyclops", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 286, - "name": "Angry Birds Rio", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 287, - "name": "Among The Sleep", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 288, - "name": "Angry Birds Space", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 289, - "name": "Amnesia: A Machine for Pigs", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 290, - "name": "Angry Birds Star Wars", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 291, - "name": "Angry Birds Seasons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 292, - "name": "Armored Kitten", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 293, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 294, - "name": "Bad Dream: Graveyard", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 295, - "name": "Angry Birds", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 296, - "name": "Alan Wake's American Nightmare", - "site": "metacritic_com", - "genres": [ - "Modern", - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 297, - "name": "Angry Birds Space", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 298, - "name": "Angry Birds Space", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 299, - "name": "Alone in the Dark (2008)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 300, - "name": "Angry Birds Seasons", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 301, - "name": "Angry Birds Star Wars", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 302, - "name": "Angry Birds", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 303, - "name": "Bad Dream: Hospital", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 304, - "name": "Angry Birds Star Wars II", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 305, - "name": "Angry Birds Rio", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 306, - "name": "Angry Birds Space", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 307, - "name": "Ashes", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Приключение", - "Боевик" - ] - }, - { - "id": 308, - "name": "Amnesia: The Dark Descent", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 309, - "name": "Angry Birds Star Wars", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 310, - "name": "Alien Rage - Unlimited", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 311, - "name": "Angry Birds Star Wars", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 312, - "name": "Angry Birds Star Wars II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 313, - "name": "Bad Dream: Memories", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 314, - "name": "Angry Birds Space", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 315, - "name": "Angry Birds Rio", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 316, - "name": "Armored Kitten", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 317, - "name": "Angry Birds Seasons", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 318, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 319, - "name": "Angry Birds Star Wars", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 320, - "name": "Armored Kitten", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 321, - "name": "Baldur's Gate", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 322, - "name": "Amnesia: A Machine for Pigs", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 323, - "name": "Angry Birds Star Wars II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 324, - "name": "Among The Sleep", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 325, - "name": "Angry Birds Space", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 326, - "name": "Angry Birds Star Wars", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 327, - "name": "Angry Birds Star Wars II", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 328, - "name": "Ashes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 329, - "name": "Ashes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 330, - "name": "Bastion", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 331, - "name": "BLACK ROSE", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 332, - "name": "Angry Birds Seasons", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 333, - "name": "Alien: Isolation", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 334, - "name": "Angry Birds Star Wars II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 335, - "name": "Armored Kitten", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Аркадные" - ] - }, - { - "id": 336, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 337, - "name": "Batman: The Telltale Series", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 338, - "name": "Angry Birds Star Wars", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 339, - "name": "Alice: Madness Returns", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action Adventure", - "Horror", - "Adventure" - ] - }, - { - "id": 340, - "name": "Bad Dream: Bridge", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 341, - "name": "Armored Kitten", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 342, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 343, - "name": "Angry Birds Star Wars II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 344, - "name": "Angry Birds", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 345, - "name": "Amnesia: The Dark Descent", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 346, - "name": "Aliens vs. Predator (2010)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 347, - "name": "Angry Birds Space", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 348, - "name": "Ashes", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 349, - "name": "BLACK ROSE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 350, - "name": "Armored Kitten", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 351, - "name": "Battlefield 1", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 352, - "name": "Angry Birds Star Wars II", - "site": "spong_com", - "genres": [] - }, - { - "id": 353, - "name": "Bad Dream: Butcher", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 354, - "name": "Armored Kitten", - "site": "playground_ru", - "genres": [] - }, - { - "id": 355, - "name": "Ashes", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 356, - "name": "BLACK ROSE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 357, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 358, - "name": "Bad Dream: Bridge", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 359, - "name": "Battlefield 1942", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 360, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 361, - "name": "Among The Sleep", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 362, - "name": "Angry Birds Star Wars", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 363, - "name": "Ashes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 364, - "name": "Aliens: Colonial Marines", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 365, - "name": "Angry Birds Rio", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 366, - "name": "Armored Kitten", - "site": "spong_com", - "genres": [] - }, - { - "id": 367, - "name": "Bad Dream: Coma", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 368, - "name": "Ashes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 369, - "name": "Bad Dream: Butcher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 370, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 371, - "name": "Battlefield 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 372, - "name": "Bad Dream: Bridge", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 373, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 374, - "name": "BLACK ROSE", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 375, - "name": "Angry Birds Star Wars II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 376, - "name": "Angry Birds", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 377, - "name": "Bad Dream: Cyclops", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 378, - "name": "Ashes", - "site": "spong_com", - "genres": [] - }, - { - "id": 379, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 380, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "playground_ru", - "genres": [] - }, - { - "id": 381, - "name": "Battlefield Hardline", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 382, - "name": "Bad Dream: Coma", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 383, - "name": "BLACK ROSE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 384, - "name": "Angry Birds Seasons", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 385, - "name": "Bad Dream: Butcher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 386, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 387, - "name": "Alien Rage - Unlimited", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 388, - "name": "Bad Dream: Graveyard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 389, - "name": "Battlefield V", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 390, - "name": "BLACK ROSE", - "site": "playground_ru", - "genres": [] - }, - { - "id": 391, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "spong_com", - "genres": [] - }, - { - "id": 392, - "name": "Bad Dream: Cyclops", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 393, - "name": "Angry Birds Rio", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 394, - "name": "Bad Dream: Bridge", - "site": "ag_ru", - "genres": [] - }, - { - "id": 395, - "name": "BLACK ROSE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 396, - "name": "Armored Kitten", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 397, - "name": "Bad Dream: Bridge", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 398, - "name": "Bad Dream: Coma", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 399, - "name": "Battlefield Vietnam", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 400, - "name": "Bad Dream: Hospital", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 401, - "name": "Angry Birds Space", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 402, - "name": "Alone in the Dark (2008)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 403, - "name": "Bad Dream: Graveyard", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 404, - "name": "Bad Dream: Bridge", - "site": "playground_ru", - "genres": [] - }, - { - "id": 405, - "name": "BLACK ROSE", - "site": "spong_com", - "genres": [] - }, - { - "id": 406, - "name": "Bad Dream: Butcher", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 407, - "name": "Bad Dream: Butcher", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 408, - "name": "Angry Birds Seasons", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 409, - "name": "Bad Dream: Bridge", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 410, - "name": "Bayonetta", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 411, - "name": "Ashes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 412, - "name": "Bad Dream: Hospital", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 413, - "name": "Bad Dream: Memories", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 414, - "name": "Bad Dream: Cyclops", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 415, - "name": "Bad Dream: Butcher", - "site": "playground_ru", - "genres": [] - }, - { - "id": 416, - "name": "Bad Dream: Bridge", - "site": "spong_com", - "genres": [] - }, - { - "id": 417, - "name": "Beholder", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 418, - "name": "Angry Birds Star Wars", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 419, - "name": "Bad Dream: Coma", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 420, - "name": "Angry Birds Space", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 421, - "name": "Amnesia: A Machine for Pigs", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 422, - "name": "Bad Dream: Memories", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 423, - "name": "Bad Dream: Butcher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 424, - "name": "Bad Dream: Coma", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 425, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 426, - "name": "Bad Dream: Graveyard", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 427, - "name": "Baldur's Gate", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 428, - "name": "Beholder (Beta)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 429, - "name": "Bad Dream: Butcher", - "site": "spong_com", - "genres": [] - }, - { - "id": 430, - "name": "Bad Dream: Coma", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 431, - "name": "Bad Dream: Cyclops", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 432, - "name": "Alien: Isolation", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Survival", - "General", - "Action Adventure" - ] - }, - { - "id": 433, - "name": "Baldur's Gate", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 434, - "name": "Bad Dream: Coma", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 435, - "name": "Angry Birds Star Wars", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 436, - "name": "Angry Birds Star Wars II", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 437, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Инди", - "Квест" - ] - }, - { - "id": 438, - "name": "BLACK ROSE", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 439, - "name": "Bad Dream: Hospital", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 440, - "name": "Bastion", - "site": "igromania_ru", - "genres": [ - "Логика", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 441, - "name": "Bad Dream: Cyclops", - "site": "ag_ru", - "genres": [] - }, - { - "id": 442, - "name": "Bad Dream: Coma", - "site": "spong_com", - "genres": [] - }, - { - "id": 443, - "name": "Bad Dream: Cyclops", - "site": "playground_ru", - "genres": [] - }, - { - "id": 444, - "name": "Amnesia: The Dark Descent", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 445, - "name": "Bad Dream: Graveyard", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 446, - "name": "Aliens vs. Predator (2010)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 447, - "name": "Ben and Ed", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 448, - "name": "Bastion", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 449, - "name": "Bad Dream: Cyclops", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 450, - "name": "Bad Dream: Memories", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 451, - "name": "Batman: The Telltale Series", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 452, - "name": "Bad Dream: Hospital", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 453, - "name": "Angry Birds Star Wars II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 454, - "name": "Bad Dream: Cyclops", - "site": "spong_com", - "genres": [] - }, - { - "id": 455, - "name": "Bad Dream: Bridge", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 456, - "name": "Bad Dream: Graveyard", - "site": "playground_ru", - "genres": [] - }, - { - "id": 457, - "name": "Bad Dream: Graveyard", - "site": "ag_ru", - "genres": [] - }, - { - "id": 458, - "name": "Ben and Ed: Blood Party", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 459, - "name": "Among The Sleep", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 460, - "name": "Armored Kitten", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 461, - "name": "Batman: The Telltale Series", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 462, - "name": "Battlefield 1", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 463, - "name": "Baldur's Gate", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 464, - "name": "Bad Dream: Graveyard", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 465, - "name": "Bad Dream: Graveyard", - "site": "spong_com", - "genres": [] - }, - { - "id": 466, - "name": "Bad Dream: Memories", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 467, - "name": "Besiege", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 468, - "name": "Bad Dream: Hospital", - "site": "playground_ru", - "genres": [] - }, - { - "id": 469, - "name": "Bad Dream: Butcher", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 470, - "name": "Bad Dream: Hospital", - "site": "ag_ru", - "genres": [] - }, - { - "id": 471, - "name": "Armored Kitten", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 472, - "name": "Battlefield 1942", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 473, - "name": "Battlefield 1", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 474, - "name": "Beyond Good and Evil", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 475, - "name": "Bastion", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 476, - "name": "Bad Dream: Hospital", - "site": "spong_com", - "genres": [] - }, - { - "id": 477, - "name": "Bad Dream: Memories", - "site": "playground_ru", - "genres": [] - }, - { - "id": 478, - "name": "Angry Birds", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 479, - "name": "Bad Dream: Hospital", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 480, - "name": "Baldur's Gate", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 481, - "name": "Bad Dream: Coma", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 482, - "name": "Bad Dream: Memories", - "site": "ag_ru", - "genres": [] - }, - { - "id": 483, - "name": "Battlefield 4", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 484, - "name": "Ashes", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 485, - "name": "Battlefield 1942", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер" - ] - }, - { - "id": 486, - "name": "Bad Dream: Memories", - "site": "spong_com", - "genres": [] - }, - { - "id": 487, - "name": "Batman: The Telltale Series", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 488, - "name": "Aliens: Colonial Marines", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Sci-Fi", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 489, - "name": "Baldur's Gate", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 490, - "name": "Bad Dream: Memories", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 491, - "name": "Bastion", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 492, - "name": "Angry Birds Rio", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 493, - "name": "Baldur's Gate", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Стратегии" - ] - }, - { - "id": 494, - "name": "Bad Dream: Cyclops", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 495, - "name": "Ashes", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 496, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 497, - "name": "Battlefield Hardline", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 498, - "name": "Binary Domain", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 499, - "name": "Baldur's Gate", - "site": "spong_com", - "genres": [] - }, - { - "id": 500, - "name": "Battlefield 1", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 501, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 502, - "name": "Bastion", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Платформер", - "Вид сверху" - ] - }, - { - "id": 503, - "name": "Baldur's Gate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 504, - "name": "Battlefield V", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 505, - "name": "BioShock", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 506, - "name": "Bastion", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 507, - "name": "Bad Dream: Graveyard", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 508, - "name": "Batman: The Telltale Series", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 509, - "name": "Angry Birds Seasons", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 510, - "name": "Battlefield 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 511, - "name": "Bastion", - "site": "spong_com", - "genres": [] - }, - { - "id": 512, - "name": "Battlefield 1942", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 513, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 514, - "name": "Alone in the Dark (2008)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 515, - "name": "Batman: The Telltale Series", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 516, - "name": "BioShock 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 517, - "name": "Battlefield Vietnam", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 518, - "name": "Bastion", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 519, - "name": "Bad Dream: Hospital", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 520, - "name": "Batman: The Telltale Series", - "site": "spong_com", - "genres": [] - }, - { - "id": 521, - "name": "Battlefield 1", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 522, - "name": "BLACK ROSE", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 523, - "name": "Angry Birds Space", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 524, - "name": "BioShock Infinite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 525, - "name": "Battlefield 4", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 526, - "name": "Batman: The Telltale Series", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 527, - "name": "Battlefield Hardline", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 528, - "name": "Bayonetta", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 529, - "name": "Battlefield 1", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 530, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 531, - "name": "Bad Dream: Bridge", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 532, - "name": "Battlefield 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 533, - "name": "Batman: The Telltale Series", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 534, - "name": "BLACK ROSE", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Indie", - "Adventure" - ] - }, - { - "id": 535, - "name": "Battlefield 1942", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 536, - "name": "Battlefield Hardline", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 537, - "name": "Bad Dream: Memories", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 538, - "name": "Beholder", - "site": "igromania_ru", - "genres": [ - "Симулятор жизни" - ] - }, - { - "id": 539, - "name": "Battlefield 1", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 540, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 541, - "name": "Battlefield V", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 542, - "name": "Bad Dream: Butcher", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 543, - "name": "Angry Birds Star Wars", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 544, - "name": "Battlefield 1942", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Strategy: Combat" - ] - }, - { - "id": 545, - "name": "Battlefield V", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 546, - "name": "Bad Dream: Bridge", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 547, - "name": "Battlefield 4", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 548, - "name": "Battlefield 1", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 549, - "name": "Beholder (Beta)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 550, - "name": "Bionic Commando", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 551, - "name": "Baldur's Gate", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 552, - "name": "Battlefield 1942", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 553, - "name": "Battlefield 4", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 554, - "name": "Battlefield Vietnam", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 555, - "name": "Amnesia: A Machine for Pigs", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 556, - "name": "Angry Birds Star Wars II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 557, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 558, - "name": "Battlefield Vietnam", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 559, - "name": "Black Mesa", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Ремейк", - "Шутер" - ] - }, - { - "id": 560, - "name": "Battlefield Hardline", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 561, - "name": "Battlefield 1942", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 562, - "name": "Bad Dream: Butcher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 563, - "name": "Bastion", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 564, - "name": "Battlefield Hardline", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 565, - "name": "Battlefield 1942", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 566, - "name": "Bayonetta", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 567, - "name": "Bad Dream: Coma", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 568, - "name": "BlackSite: Area 51", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 569, - "name": "Ben and Ed", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 570, - "name": "Battlefield 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 571, - "name": "Armored Kitten", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 572, - "name": "Bayonetta", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 573, - "name": "Battlefield V", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 574, - "name": "Battlefield 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 575, - "name": "Blades of Time", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 576, - "name": "Battlefield V", - "site": "spong_com", - "genres": [] - }, - { - "id": 577, - "name": "Bad Dream: Cyclops", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 578, - "name": "Amnesia: The Dark Descent", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 579, - "name": "Batman: The Telltale Series", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 580, - "name": "Ben and Ed: Blood Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 581, - "name": "Beholder", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 582, - "name": "Bad Dream: Coma", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 583, - "name": "Battlefield 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 584, - "name": "Beholder", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 585, - "name": "Battlefield Hardline", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 586, - "name": "Battlefield Vietnam", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 587, - "name": "Blades of Time: Dismal Swamp", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 588, - "name": "Ashes", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 589, - "name": "Bad Dream: Graveyard", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 590, - "name": "Battlefield Hardline", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 591, - "name": "Battlefield Vietnam", - "site": "spong_com", - "genres": [ - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 592, - "name": "Beholder (Beta)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 593, - "name": "Besiege", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 594, - "name": "Battlefield 1", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 595, - "name": "Bad Dream: Cyclops", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 596, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 597, - "name": "Battlefield Hardline", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 598, - "name": "Beholder (Beta)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 599, - "name": "Battlefield V", - "site": "ag_ru", - "genres": [ - "Шутеры", - "ММО", - "Экшены", - "Приключения" - ] - }, - { - "id": 600, - "name": "Bayonetta", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 601, - "name": "Bad Dream: Hospital", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 602, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 603, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 604, - "name": "Beyond Good and Evil", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 605, - "name": "Bayonetta", - "site": "spong_com", - "genres": [ - "Combat Game" - ] - }, - { - "id": 606, - "name": "Battlefield V", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 607, - "name": "BloodRayne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 608, - "name": "Bad Dream: Graveyard", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 609, - "name": "Battlefield 1942", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 610, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 611, - "name": "Bad Dream: Memories", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 612, - "name": "Battlefield V", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 613, - "name": "Ben and Ed", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 614, - "name": "Battlefield Vietnam", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 615, - "name": "Beholder", - "site": "spong_com", - "genres": [] - }, - { - "id": 616, - "name": "Beholder", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 617, - "name": "BloodRayne 2", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 618, - "name": "Binary Domain", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 619, - "name": "BLACK ROSE", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 620, - "name": "Battlefield Vietnam", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 621, - "name": "Ben and Ed: Blood Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 622, - "name": "Ben and Ed", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 623, - "name": "BloodRayne: Betrayal", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 624, - "name": "Beholder (Beta)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 625, - "name": "Bad Dream: Hospital", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 626, - "name": "Battlefield 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 627, - "name": "Beholder (Beta)", - "site": "spong_com", - "genres": [] - }, - { - "id": 628, - "name": "Battlefield Vietnam", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 629, - "name": "Baldur's Gate", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 630, - "name": "Bad Dream: Bridge", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 631, - "name": "BioShock", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 632, - "name": "Bayonetta", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 633, - "name": "Bayonetta", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 634, - "name": "Bloodbath kavkaz", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 635, - "name": "Besiege", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 636, - "name": "Ben and Ed: Blood Party", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 637, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 638, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 639, - "name": "Among The Sleep", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 640, - "name": "Bad Dream: Memories", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 641, - "name": "BioShock 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 642, - "name": "Bad Dream: Butcher", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 643, - "name": "Beholder", - "site": "ag_ru", - "genres": [ - "Приключения", - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 644, - "name": "Battlefield Hardline", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 645, - "name": "Bloodline: Линия крови", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 646, - "name": "Beyond Good and Evil", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 647, - "name": "Beholder", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 648, - "name": "Besiege", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "инди" - ] - }, - { - "id": 649, - "name": "Ben and Ed", - "site": "spong_com", - "genres": [] - }, - { - "id": 650, - "name": "Ben and Ed", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 651, - "name": "BioShock Infinite", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 652, - "name": "Blue Estate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 653, - "name": "Baldur's Gate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 654, - "name": "Bastion", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 655, - "name": "Beholder (Beta)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 656, - "name": "Battlefield V", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 657, - "name": "Binary Domain", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 658, - "name": "Bayonetta", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 659, - "name": "Ben and Ed: Blood Party", - "site": "spong_com", - "genres": [] - }, - { - "id": 660, - "name": "Bad Dream: Coma", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 661, - "name": "Beholder (Beta)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 662, - "name": "Beyond Good and Evil", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 663, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 664, - "name": "Blues and Bullets: Episode 1", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 665, - "name": "Ben and Ed: Blood Party", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 666, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 667, - "name": "Battlefield Vietnam", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 668, - "name": "Besiege", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 669, - "name": "Batman: The Telltale Series", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 670, - "name": "BioShock", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 671, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 672, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 673, - "name": "Bad Dream: Cyclops", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 674, - "name": "Binary Domain", - "site": "gamer_info_com", - "genres": [ - "командные шутеры" - ] - }, - { - "id": 675, - "name": "Beholder", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Стратегия" - ] - }, - { - "id": 676, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 677, - "name": "Besiege", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 678, - "name": "Bastion", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 679, - "name": "Boogeyman", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 680, - "name": "Beyond Good and Evil", - "site": "spong_com", - "genres": [] - }, - { - "id": 681, - "name": "Beholder (Beta)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 682, - "name": "Ben and Ed", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 683, - "name": "Bionic Commando", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 684, - "name": "BioShock 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 685, - "name": "BioShock", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG", - "action" - ] - }, - { - "id": 686, - "name": "Bad Dream: Graveyard", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 687, - "name": "Bayonetta", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 688, - "name": "Beyond Good and Evil", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 689, - "name": "Ben and Ed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 690, - "name": "Battlefield 1", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 691, - "name": "Borderlands", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 692, - "name": "Binary Domain", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 693, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 694, - "name": "Angry Birds", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy", - "Miscellaneous", - "Puzzle", - "Action" - ] - }, - { - "id": 695, - "name": "Batman: The Telltale Series", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 696, - "name": "Ben and Ed: Blood Party", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 697, - "name": "Borderlands 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 698, - "name": "BioShock 2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG", - "action" - ] - }, - { - "id": 699, - "name": "Black Mesa", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 700, - "name": "Binary Domain", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 701, - "name": "Beholder", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 702, - "name": "Bad Dream: Hospital", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 703, - "name": "Ben and Ed: Blood Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 704, - "name": "BioShock", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 705, - "name": "Battlefield 1942", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 706, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 707, - "name": "Ben and Ed", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура" - ] - }, - { - "id": 708, - "name": "BlackSite: Area 51", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 709, - "name": "BioShock Infinite", - "site": "gamer_info_com", - "genres": [ - "FPS", - "action" - ] - }, - { - "id": 710, - "name": "BioShock", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 711, - "name": "Battlefield 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 712, - "name": "Besiege", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 713, - "name": "Bad Dream: Memories", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 714, - "name": "Besiege", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 715, - "name": "Beholder (Beta)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 716, - "name": "BioShock 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 717, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 718, - "name": "Ben and Ed: Blood Party", - "site": "playground_ru", - "genres": [] - }, - { - "id": 719, - "name": "Blades of Time", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 720, - "name": "Battlefield 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 721, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 722, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 723, - "name": "Beyond Good and Evil", - "site": "ag_ru", - "genres": [] - }, - { - "id": 724, - "name": "Battlefield 1942", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 725, - "name": "BioShock Infinite", - "site": "spong_com", - "genres": [] - }, - { - "id": 726, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 727, - "name": "Beyond Good and Evil", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 728, - "name": "BioShock 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 729, - "name": "Blades of Time: Dismal Swamp", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 730, - "name": "Besiege", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 731, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 732, - "name": "Baldur's Gate", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 733, - "name": "BioShock Infinite", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 734, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 735, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 736, - "name": "Blood Omen 2: Legacy of Kain", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 737, - "name": "Binary Domain", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 738, - "name": "Angry Birds Rio", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Puzzle" - ] - }, - { - "id": 739, - "name": "Ben and Ed", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 740, - "name": "BioShock Infinite", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 741, - "name": "Binary Domain", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 742, - "name": "Battlefield 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 743, - "name": "Battlefield Hardline", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 744, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 745, - "name": "Beyond Good and Evil", - "site": "playground_ru", - "genres": [] - }, - { - "id": 746, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 747, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 748, - "name": "BloodRayne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 749, - "name": "Bionic Commando", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 750, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 751, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 752, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 753, - "name": "Bastion", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 754, - "name": "Battlefield Hardline", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 755, - "name": "BioShock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 756, - "name": "Ben and Ed: Blood Party", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 757, - "name": "Binary Domain", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Киберпанк" - ] - }, - { - "id": 758, - "name": "Battlefield V", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "First-Person", - "Tactical" - ] - }, - { - "id": 759, - "name": "Bionic Commando", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 760, - "name": "BloodRayne 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 761, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер", - "Аддон" - ] - }, - { - "id": 762, - "name": "Black Mesa", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 763, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 764, - "name": "Bionic Commando", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 765, - "name": "BioShock", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 766, - "name": "BioShock 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 767, - "name": "Battlefield V", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 768, - "name": "Black Mesa", - "site": "spong_com", - "genres": [] - }, - { - "id": 769, - "name": "Besiege", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 770, - "name": "BloodRayne: Betrayal", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 771, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 772, - "name": "BioShock", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "Шутер" - ] - }, - { - "id": 773, - "name": "Angry Birds Seasons", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Miscellaneous", - "Puzzle" - ] - }, - { - "id": 774, - "name": "BlackSite: Area 51", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 775, - "name": "Battlefield Vietnam", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 776, - "name": "Bionic Commando", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 777, - "name": "Black Mesa", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 778, - "name": "BioShock 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 779, - "name": "Borderlands: The Pre-Sequel!", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 780, - "name": "BlackSite: Area 51", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 781, - "name": "Batman: The Telltale Series", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 782, - "name": "Bloodbath kavkaz", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 783, - "name": "Battlefield Vietnam", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 784, - "name": "BioShock Infinite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 785, - "name": "Beyond Good and Evil", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 786, - "name": "BioShock 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "От первого лица", - "Шутер" - ] - }, - { - "id": 787, - "name": "Blades of Time", - "site": "gamer_info_com", - "genres": [ - "hack & slash", - "action" - ] - }, - { - "id": 788, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 789, - "name": "Black Mesa", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 790, - "name": "BlackSite: Area 51", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 791, - "name": "Blades of Time", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 792, - "name": "Bloodline: Линия крови", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 793, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 794, - "name": "Binary Domain", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 795, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 796, - "name": "BioShock Infinite", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 797, - "name": "BioShock Infinite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "От первого лица", - "Шутер" - ] - }, - { - "id": 798, - "name": "Blades of Time: Dismal Swamp", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 799, - "name": "Bayonetta", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 800, - "name": "BlackSite: Area 51", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 801, - "name": "Bayonetta", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 802, - "name": "Battlefield 1", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 803, - "name": "Blades of Time: Dismal Swamp", - "site": "spong_com", - "genres": [] - }, - { - "id": 804, - "name": "Blades of Time", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 805, - "name": "Blue Estate", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 806, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 807, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 808, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 809, - "name": "BioShock", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 810, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 811, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 812, - "name": "Angry Birds Space", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Puzzle" - ] - }, - { - "id": 813, - "name": "Blades of Time: Dismal Swamp", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 814, - "name": "Blues and Bullets: Episode 1", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 815, - "name": "Blades of Time", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 816, - "name": "Blood Omen 2: Legacy of Kain", - "site": "spong_com", - "genres": [] - }, - { - "id": 817, - "name": "Brink", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 818, - "name": "Battlefield 1942", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 819, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 820, - "name": "BloodRayne", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 821, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 822, - "name": "Bionic Commando", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 823, - "name": "BioShock 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 824, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 825, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 826, - "name": "Beholder", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 827, - "name": "Broforce", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 828, - "name": "Blades of Time: Dismal Swamp", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 829, - "name": "BloodRayne", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 830, - "name": "Beholder", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 831, - "name": "Broken Age", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 832, - "name": "BloodRayne 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 833, - "name": "BloodRayne", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 834, - "name": "Bionic Commando", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 835, - "name": "Black Mesa", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 836, - "name": "Boogeyman", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 837, - "name": "BioShock Infinite", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 838, - "name": "Bionic Commando", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 839, - "name": "Beholder (Beta)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 840, - "name": "BloodRayne 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 841, - "name": "Beholder (Beta)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 842, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 843, - "name": "Brothers: A Tale Of Two Sons", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 844, - "name": "Battlefield 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 845, - "name": "BloodRayne: Betrayal", - "site": "gamer_info_com", - "genres": [ - "beat 'em up" - ] - }, - { - "id": 846, - "name": "BloodRayne 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 847, - "name": "Borderlands", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Ролевая игра" - ] - }, - { - "id": 848, - "name": "BlackSite: Area 51", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 849, - "name": "BloodRayne: Betrayal", - "site": "spong_com", - "genres": [] - }, - { - "id": 850, - "name": "Black Mesa", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 851, - "name": "Black Mesa", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди" - ] - }, - { - "id": 852, - "name": "Angry Birds Star Wars", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Strategy", - "Puzzle" - ] - }, - { - "id": 853, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 854, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 855, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 856, - "name": "Brutal Legend", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 857, - "name": "BloodRayne", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Arcade", - "Shooter" - ] - }, - { - "id": 858, - "name": "Bloodbath kavkaz", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 859, - "name": "Borderlands 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 860, - "name": "BloodRayne: Betrayal", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 861, - "name": "Bloodbath kavkaz", - "site": "spong_com", - "genres": [] - }, - { - "id": 862, - "name": "Buff Knight Advanced", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 863, - "name": "BlackSite: Area 51", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 864, - "name": "Battlefield Hardline", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 865, - "name": "Blades of Time", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 866, - "name": "BlackSite: Area 51", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 867, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 868, - "name": "BloodRayne 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Arcade", - "Shooter" - ] - }, - { - "id": 869, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 870, - "name": "Bloodline: Линия крови", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 871, - "name": "Bulb Boy", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 872, - "name": "Ben and Ed", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 873, - "name": "Bloodbath kavkaz", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 874, - "name": "Bloodline: Линия крови", - "site": "spong_com", - "genres": [] - }, - { - "id": 875, - "name": "Blades of Time: Dismal Swamp", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 876, - "name": "Blades of Time", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 877, - "name": "Ben and Ed", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 878, - "name": "Blades of Time", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 879, - "name": "Angry Birds Star Wars II", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Strategy", - "Puzzle" - ] - }, - { - "id": 880, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 881, - "name": "Bulletstorm", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 882, - "name": "BloodRayne: Betrayal", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 883, - "name": "Bionic Commando", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 884, - "name": "Blue Estate", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 885, - "name": "Bloodline: Линия крови", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 886, - "name": "Battlefield V", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 887, - "name": "Blades of Time: Dismal Swamp", - "site": "playground_ru", - "genres": [] - }, - { - "id": 888, - "name": "Call of Cthulhu (2018)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 889, - "name": "Blue Estate", - "site": "spong_com", - "genres": [] - }, - { - "id": 890, - "name": "Blood Omen 2: Legacy of Kain", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 891, - "name": "Ben and Ed: Blood Party", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 892, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 893, - "name": "Blades of Time: Dismal Swamp", - "site": "ag_ru", - "genres": [] - }, - { - "id": 894, - "name": "Blues and Bullets: Episode 1", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 895, - "name": "Blue Estate", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 896, - "name": "Bloodbath kavkaz", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 897, - "name": "Black Mesa", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 898, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Квест" - ] - }, - { - "id": 899, - "name": "Blues and Bullets: Episode 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 900, - "name": "Battlefield Vietnam", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 901, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 902, - "name": "Blood Omen 2: Legacy of Kain", - "site": "playground_ru", - "genres": [] - }, - { - "id": 903, - "name": "BloodRayne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 904, - "name": "Blues and Bullets: Episode 1", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 905, - "name": "Bloodline: Линия крови", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 906, - "name": "Ben and Ed: Blood Party", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 907, - "name": "Call of Duty", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 908, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 909, - "name": "Blood Omen 2: Legacy of Kain", - "site": "ag_ru", - "genres": [] - }, - { - "id": 910, - "name": "Armored Kitten", - "site": "metacritic_com", - "genres": [ - "Top-Down", - "Action", - "Shoot-'Em-Up", - "Shooter" - ] - }, - { - "id": 911, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 912, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 913, - "name": "Besiege", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Simulation", - "Indie" - ] - }, - { - "id": 914, - "name": "BlackSite: Area 51", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 915, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 916, - "name": "BloodRayne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 917, - "name": "Bayonetta", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 918, - "name": "BloodRayne 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 919, - "name": "Call of Duty 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 920, - "name": "Blue Estate", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Light Gun", - "Shooter" - ] - }, - { - "id": 921, - "name": "Boogeyman", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 922, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 923, - "name": "Boogeyman", - "site": "spong_com", - "genres": [] - }, - { - "id": 924, - "name": "Boogeyman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 925, - "name": "BloodRayne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 926, - "name": "Blades of Time", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 927, - "name": "Call of Duty 4: Modern Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 928, - "name": "BloodRayne 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 929, - "name": "Beholder", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Adventure" - ] - }, - { - "id": 930, - "name": "Blues and Bullets: Episode 1", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 931, - "name": "Borderlands", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 932, - "name": "Besiege", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 933, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 934, - "name": "BloodRayne: Betrayal", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 935, - "name": "Beyond Good and Evil", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 936, - "name": "Borderlands", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 937, - "name": "Ashes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 938, - "name": "Call of Duty: Advanced Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 939, - "name": "BloodRayne 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 940, - "name": "Borderlands", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 941, - "name": "Beholder (Beta)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 942, - "name": "BloodRayne: Betrayal", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Слэшер" - ] - }, - { - "id": 943, - "name": "Blades of Time: Dismal Swamp", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 944, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 945, - "name": "Beyond Good and Evil", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 946, - "name": "Borderlands 2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 947, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 948, - "name": "Bloodbath kavkaz", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 949, - "name": "Call of Duty: Black Ops", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 950, - "name": "Borderlands 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 951, - "name": "Borderlands: The Pre-Sequel!", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 952, - "name": "BloodRayne: Betrayal", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 953, - "name": "Binary Domain", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 954, - "name": "Call of Duty: Black Ops II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 955, - "name": "Bloodbath kavkaz", - "site": "playground_ru", - "genres": [ - "Инди", - "Вид сверху", - "Мультиплеер", - "Адвенчура", - "Экшен" - ] - }, - { - "id": 956, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 957, - "name": "Boogeyman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 958, - "name": "Blood Omen 2: Legacy of Kain", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 959, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 960, - "name": "Bloodline: Линия крови", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 961, - "name": "Binary Domain", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 962, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 963, - "name": "Call of Duty: Black Ops III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 964, - "name": "Bloodline: Линия крови", - "site": "playground_ru", - "genres": [] - }, - { - "id": 965, - "name": "Borderlands 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 966, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 967, - "name": "Bloodbath kavkaz", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 968, - "name": "BloodRayne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 969, - "name": "Borderlands", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 970, - "name": "Ben and Ed", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 971, - "name": "Call of Duty: Ghosts", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 972, - "name": "Blue Estate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 973, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 974, - "name": "Blue Estate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 975, - "name": "BioShock", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 976, - "name": "BioShock", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 977, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 978, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 979, - "name": "Call of Duty: Infinite Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 980, - "name": "Bloodline: Линия крови", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 981, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 982, - "name": "BloodRayne 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 983, - "name": "Blues and Bullets: Episode 1", - "site": "playground_ru", - "genres": [] - }, - { - "id": 984, - "name": "Blues and Bullets: Episode 1", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 985, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 986, - "name": "Borderlands 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 987, - "name": "Call of Duty: Modern Warfare 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 988, - "name": "Ben and Ed: Blood Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 989, - "name": "Blue Estate", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 990, - "name": "BioShock 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 991, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 992, - "name": "Brink", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 993, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 994, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 995, - "name": "BioShock 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 996, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 997, - "name": "BloodRayne: Betrayal", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 998, - "name": "Call of Duty: Modern Warfare 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 999, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1000, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1001, - "name": "Blues and Bullets: Episode 1", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1002, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1003, - "name": "Boogeyman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1004, - "name": "Broforce", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 1005, - "name": "Besiege", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 1006, - "name": "Call of Duty: United Offensive", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1007, - "name": "BioShock Infinite", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1008, - "name": "Boogeyman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1009, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1010, - "name": "Bloodbath kavkaz", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1011, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1012, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1013, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1014, - "name": "Broken Age", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1015, - "name": "BioShock Infinite", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1016, - "name": "Call of Duty: WWII", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1017, - "name": "Beyond Good and Evil", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1018, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1019, - "name": "Borderlands", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1020, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1021, - "name": "Borderlands", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1022, - "name": "Call of Duty: World at War", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1023, - "name": "Brothers: A Tale Of Two Sons", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1024, - "name": "Bloodline: Линия крови", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1025, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1026, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1027, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1028, - "name": "Boogeyman", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 1029, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1030, - "name": "Binary Domain", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1031, - "name": "Canabalt", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1032, - "name": "Borderlands 2", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Шутер", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 1033, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1034, - "name": "Brutal Legend", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1035, - "name": "Borderlands 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 1036, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1037, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1038, - "name": "Blue Estate", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1039, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1040, - "name": "Castlevania: Lords of Shadow", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1041, - "name": "Borderlands", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1042, - "name": "BioShock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1043, - "name": "Buff Knight Advanced", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1044, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1045, - "name": "Bionic Commando", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1046, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 1047, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1048, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1049, - "name": "Blues and Bullets: Episode 1", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1050, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1051, - "name": "Borderlands 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1052, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1053, - "name": "Bulb Boy", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 1054, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1055, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1056, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1057, - "name": "Bionic Commando", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1058, - "name": "BioShock 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1059, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1060, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1061, - "name": "Black Mesa", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1062, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1063, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1064, - "name": "Castlevania: Lords of Shadow 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1065, - "name": "Bulletstorm", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1066, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1067, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1068, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1069, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1070, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1071, - "name": "Cat Mario", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1072, - "name": "BioShock Infinite", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1073, - "name": "Call of Cthulhu (2018)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1074, - "name": "Boogeyman", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1075, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1076, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1077, - "name": "Black Mesa", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1078, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1079, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1080, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1081, - "name": "BlackSite: Area 51", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1082, - "name": "Cat Quest", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Инди" - ] - }, - { - "id": 1083, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1084, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1085, - "name": "Borderlands", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1086, - "name": "Child Of Light", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 1087, - "name": "BlackSite: Area 51", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1088, - "name": "Brink", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1089, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1090, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1091, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1092, - "name": "Brink", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1093, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1094, - "name": "Call of Duty", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1095, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1096, - "name": "Children of Morta", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Инди" - ] - }, - { - "id": 1097, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1098, - "name": "Broforce", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 1099, - "name": "Broforce", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1100, - "name": "Borderlands 2", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 1101, - "name": "Blades of Time", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1102, - "name": "Call of Duty 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1103, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1104, - "name": "Choice Chamber", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1105, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1106, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1107, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1108, - "name": "Blades of Time", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1109, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1110, - "name": "Broken Age", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 1111, - "name": "Claire", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1112, - "name": "Broken Age", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1113, - "name": "Blades of Time: Dismal Swamp", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1114, - "name": "Call of Duty 4: Modern Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1115, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1116, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1117, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1118, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1119, - "name": "Blades of Time: Dismal Swamp", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1120, - "name": "Clustertruck", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 1121, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1122, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 1123, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1124, - "name": "Call of Duty: Advanced Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1125, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 1126, - "name": "Bionic Commando", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1127, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1128, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1129, - "name": "Cold Fear", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1130, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1131, - "name": "Brutal Legend", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 1132, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1133, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1134, - "name": "Costume Quest", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 1135, - "name": "Call of Duty: Black Ops", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1136, - "name": "BloodRayne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 1137, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1138, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1139, - "name": "Blood Omen 2: Legacy of Kain", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1140, - "name": "Black Mesa", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1141, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1142, - "name": "Costume Quest 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 1143, - "name": "Buff Knight Advanced", - "site": "gamer_info_com", - "genres": [ - "RPG", - "runner", - "инди" - ] - }, - { - "id": 1144, - "name": "Borderlands: The Pre-Sequel!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1145, - "name": "Call of Duty: Black Ops II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1146, - "name": "Borderlands: The Pre-Sequel!", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1147, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1148, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1149, - "name": "Brink", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1150, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1151, - "name": "BloodRayne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1152, - "name": "BloodRayne 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 1153, - "name": "BlackSite: Area 51", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1154, - "name": "Brutal Legend", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 1155, - "name": "Bulb Boy", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "квесты" - ] - }, - { - "id": 1156, - "name": "Call of Duty: Black Ops III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1157, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1158, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1159, - "name": "Cross of the Dutchman", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1160, - "name": "Broforce", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1161, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1162, - "name": "Borderlands: The Pre-Sequel!", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1163, - "name": "Buff Knight Advanced", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1164, - "name": "Bulletstorm", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1165, - "name": "BloodRayne 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1166, - "name": "Call of Duty: Ghosts", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1167, - "name": "Cry Of Fear", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1168, - "name": "BloodRayne: Betrayal", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1169, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1170, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1171, - "name": "Broken Age", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 1172, - "name": "Crysis", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1173, - "name": "Blades of Time", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1174, - "name": "Call of Duty: Infinite Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1175, - "name": "Bulb Boy", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1176, - "name": "Call of Cthulhu (2018)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1177, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1178, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1179, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1180, - "name": "Crysis 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1181, - "name": "BloodRayne: Betrayal", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1182, - "name": "Call of Duty: Modern Warfare 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1183, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 1184, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 1185, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1186, - "name": "Bulletstorm", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1187, - "name": "Blades of Time: Dismal Swamp", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1188, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1189, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1190, - "name": "Crysis Warhead", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1191, - "name": "Brink", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1192, - "name": "Call of Duty", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1193, - "name": "Call of Duty: Modern Warfare 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1194, - "name": "Call of Cthulhu (2018)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1195, - "name": "Bloodbath kavkaz", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1196, - "name": "Bloodbath kavkaz", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 1197, - "name": "Blood Omen 2: Legacy of Kain", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1198, - "name": "Brutal Legend", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 1199, - "name": "Cube Escape: Arles", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1200, - "name": "Brink", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1201, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1202, - "name": "Call of Duty: United Offensive", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1203, - "name": "Call of Duty 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1204, - "name": "Broforce", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1205, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1206, - "name": "Cube Escape: Case 23", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1207, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 1208, - "name": "Bloodline: Линия крови", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1209, - "name": "Bloodline: Линия крови", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1210, - "name": "Buff Knight Advanced", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1211, - "name": "BloodRayne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1212, - "name": "Call of Duty: WWII", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1213, - "name": "Borderlands: The Pre-Sequel!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1214, - "name": "Broforce", - "site": "playground_ru", - "genres": [ - "Инди", - "Аркада", - "Ретро", - "Шутер", - "Платформер" - ] - }, - { - "id": 1215, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1216, - "name": "Cube Escape: Harvey's Box", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1217, - "name": "Broken Age", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1218, - "name": "Brink", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1219, - "name": "Blue Estate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1220, - "name": "Call of Duty", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1221, - "name": "Call of Duty: World at War", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1222, - "name": "Bulb Boy", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 1223, - "name": "Cube Escape: Seasons", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1224, - "name": "Blue Estate", - "site": "gamespot_com", - "genres": [ - "Action", - "Light-Gun", - "Shooter" - ] - }, - { - "id": 1225, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1226, - "name": "Call of Duty: Advanced Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1227, - "name": "Broken Age", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Квест" - ] - }, - { - "id": 1228, - "name": "Brothers: A Tale Of Two Sons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1229, - "name": "BloodRayne 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1230, - "name": "Cube Escape: The Lake", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1231, - "name": "Blues and Bullets: Episode 1", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1232, - "name": "Canabalt", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 1233, - "name": "Bulletstorm", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1234, - "name": "Broforce", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 1235, - "name": "Blues and Bullets: Episode 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1236, - "name": "Call of Duty 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1237, - "name": "Call of Duty: Black Ops", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1238, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1239, - "name": "Brothers: A Tale Of Two Sons", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Инди" - ] - }, - { - "id": 1240, - "name": "Brutal Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1241, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1242, - "name": "BloodRayne: Betrayal", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1243, - "name": "Call of Cthulhu (2018)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1244, - "name": "Castlevania: Lords of Shadow", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1245, - "name": "Broken Age", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Семейные", - "Головоломки", - "Казуальные" - ] - }, - { - "id": 1246, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1247, - "name": "Call of Duty: Black Ops II", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1248, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1249, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1250, - "name": "Buff Knight Advanced", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1251, - "name": "Brutal Legend", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Ролевая", - "Слэшер" - ] - }, - { - "id": 1252, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1253, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 1254, - "name": "Call of Duty: Black Ops III", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1255, - "name": "Bloodbath kavkaz", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1256, - "name": "Brothers: A Tale Of Two Sons", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Головоломки", - "Приключения" - ] - }, - { - "id": 1257, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1258, - "name": "Buff Knight Advanced", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1259, - "name": "Brink", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1260, - "name": "Bulb Boy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1261, - "name": "Boogeyman", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 1262, - "name": "Call of Duty", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1263, - "name": "Call of Duty: Ghosts", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1264, - "name": "Call of Duty: Advanced Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1265, - "name": "Bloodline: Линия крови", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1266, - "name": "Bulb Boy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1267, - "name": "Castlevania: Lords of Shadow 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1268, - "name": "Boogeyman", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1269, - "name": "Brutal Legend", - "site": "ag_ru", - "genres": [ - "Экшены", - "Стратегии" - ] - }, - { - "id": 1270, - "name": "Broforce", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1271, - "name": "Bulletstorm", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1272, - "name": "Borderlands", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1273, - "name": "Call of Duty: Infinite Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1274, - "name": "Cat Mario", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1275, - "name": "Call of Duty 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1276, - "name": "Call of Duty: Black Ops", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Битвы машин" - ] - }, - { - "id": 1277, - "name": "Blue Estate", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1278, - "name": "Bulletstorm", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1279, - "name": "Broken Age", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1280, - "name": "Call of Cthulhu (2018)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1281, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1282, - "name": "Cat Quest", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1283, - "name": "Buff Knight Advanced", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Аркадные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 1284, - "name": "Call of Duty: Black Ops II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1285, - "name": "Call of Cthulhu (2018)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1286, - "name": "Blues and Bullets: Episode 1", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1287, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1288, - "name": "Borderlands 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1289, - "name": "Child Of Light", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Ролевая игра" - ] - }, - { - "id": 1290, - "name": "Call of Duty: Black Ops III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1291, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1292, - "name": "Brothers: A Tale Of Two Sons", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 1293, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1294, - "name": "Bulb Boy", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 1295, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1296, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 1297, - "name": "Borderlands", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1298, - "name": "Call of Duty: Advanced Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1299, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1300, - "name": "Children of Morta", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 1301, - "name": "Call of Duty: United Offensive", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1302, - "name": "Bulletstorm", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1303, - "name": "Call of Duty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1304, - "name": "Brutal Legend", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1305, - "name": "Choice Chamber", - "site": "igromania_ru", - "genres": [ - "MMOG", - "Боевик" - ] - }, - { - "id": 1306, - "name": "Call of Duty", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1307, - "name": "Call of Duty: Black Ops", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1308, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1309, - "name": "Boogeyman", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1310, - "name": "Call of Duty: WWII", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1311, - "name": "Call of Cthulhu (2018)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1312, - "name": "Buff Knight Advanced", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1313, - "name": "Borderlands 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1314, - "name": "Call of Duty 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1315, - "name": "Claire", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1316, - "name": "Call of Duty 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1317, - "name": "Call of Duty: World at War", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1318, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1319, - "name": "Call of Duty: Black Ops II", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1320, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1321, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1322, - "name": "Borderlands", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1323, - "name": "Clustertruck", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1324, - "name": "Bulb Boy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1325, - "name": "Call of Duty 4: Modern Warfare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1326, - "name": "Canabalt", - "site": "gamer_info_com", - "genres": [ - "инди", - "runner", - "аркадные" - ] - }, - { - "id": 1327, - "name": "Call of Duty: Ghosts", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1328, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1329, - "name": "Call of Duty 4: Modern Warfare", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1330, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1331, - "name": "Cold Fear", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1332, - "name": "Call of Duty", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены", - "Симуляторы" - ] - }, - { - "id": 1333, - "name": "Call of Duty: Black Ops III", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1334, - "name": "Bulletstorm", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1335, - "name": "Borderlands 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving", - "Role-Playing (RPG)" - ] - }, - { - "id": 1336, - "name": "Call of Duty: Advanced Warfare", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1337, - "name": "Castlevania: Lords of Shadow", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1338, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1339, - "name": "Call of Duty: Infinite Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1340, - "name": "Costume Quest", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1341, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1342, - "name": "Call of Duty: Advanced Warfare", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Кооператив", - "Научная фантастика", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 1343, - "name": "Call of Duty: Ghosts", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1344, - "name": "Call of Cthulhu (2018)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1345, - "name": "Call of Duty 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1346, - "name": "Call of Duty: Black Ops", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1347, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1348, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1349, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1350, - "name": "Costume Quest 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1351, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1352, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1353, - "name": "Call of Duty: Black Ops", - "site": "playground_ru", - "genres": [ - "Экшен", - "Зомби", - "Шутер" - ] - }, - { - "id": 1354, - "name": "Call of Duty: Infinite Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 1355, - "name": "Call of Duty 4: Modern Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1356, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1357, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 1358, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1359, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1360, - "name": "Call of Duty: Black Ops II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1361, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1362, - "name": "Call of Duty: Black Ops II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1363, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1364, - "name": "Call of Duty: Advanced Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1365, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1366, - "name": "Cross of the Dutchman", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1367, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1368, - "name": "Call of Duty", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1369, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1370, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1371, - "name": "Call of Duty: Black Ops III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1372, - "name": "Call of Duty: Black Ops III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1373, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1374, - "name": "Cry Of Fear", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1375, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1376, - "name": "Cat Mario", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1377, - "name": "Call of Duty: Black Ops", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1378, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1379, - "name": "Call of Duty: Ghosts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1380, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1381, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1382, - "name": "Call of Duty: Ghosts", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1383, - "name": "Call of Duty 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1384, - "name": "Crysis", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1385, - "name": "Cat Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 1386, - "name": "Call of Duty: Black Ops II", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1387, - "name": "Call of Duty: Infinite Warfare", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1388, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1389, - "name": "Borderlands: The Pre-Sequel!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 1390, - "name": "Call of Duty 4: Modern Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1391, - "name": "Call of Duty: United Offensive", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1392, - "name": "Crysis 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1393, - "name": "Call of Duty: Infinite Warfare", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1394, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1395, - "name": "Child Of Light", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 1396, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1397, - "name": "Call of Duty: Black Ops III", - "site": "ag_ru", - "genres": [ - "Шутеры", - "ММО", - "Экшены" - ] - }, - { - "id": 1398, - "name": "Call of Duty: United Offensive", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1399, - "name": "Crysis Warhead", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1400, - "name": "Call of Duty: Modern Warfare 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1401, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1402, - "name": "Call of Duty: WWII", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1403, - "name": "Call of Duty: Advanced Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1404, - "name": "Call of Duty: Modern Warfare 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1405, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1406, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1407, - "name": "Children of Morta", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди", - "action" - ] - }, - { - "id": 1408, - "name": "Call of Duty: Ghosts", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1409, - "name": "Cube Escape: Arles", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1410, - "name": "Call of Duty: WWII", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер" - ] - }, - { - "id": 1411, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1412, - "name": "Call of Duty: Modern Warfare 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1413, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1414, - "name": "Call of Duty: Black Ops", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1415, - "name": "Call of Duty: World at War", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1416, - "name": "Call of Duty: Modern Warfare 3", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1417, - "name": "Choice Chamber", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1418, - "name": "Cube Escape: Case 23", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1419, - "name": "Call of Duty: Infinite Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1420, - "name": "Call of Duty: World at War", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1421, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1422, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1423, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1424, - "name": "Call of Duty: United Offensive", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1425, - "name": "Call of Duty: United Offensive", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1426, - "name": "Canabalt", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1427, - "name": "Call of Duty: Black Ops II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1428, - "name": "Cube Escape: Harvey's Box", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1429, - "name": "Claire", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 1430, - "name": "Call of Duty: Modern Warfare 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1431, - "name": "Canabalt", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 1432, - "name": "Call of Duty: WWII", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1433, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1434, - "name": "Cube Escape: Seasons", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1435, - "name": "Clustertruck", - "site": "gamer_info_com", - "genres": [ - "runner" - ] - }, - { - "id": 1436, - "name": "Castlevania: Lords of Shadow", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 1437, - "name": "Call of Duty: WWII", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1438, - "name": "Call of Duty: Black Ops III", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1439, - "name": "Call of Duty: Modern Warfare 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1440, - "name": "Castlevania: Lords of Shadow", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1441, - "name": "Brink", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1442, - "name": "Brink", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action" - ] - }, - { - "id": 1443, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1444, - "name": "Call of Duty: World at War", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1445, - "name": "Cold Fear", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1446, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1447, - "name": "Borderlands: The Pre-Sequel!", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 1448, - "name": "Call of Duty: World at War", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1449, - "name": "Cube Escape: The Lake", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1450, - "name": "Call of Duty: United Offensive", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1451, - "name": "Call of Duty: Ghosts", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1452, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1453, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1454, - "name": "Costume Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "приключения" - ] - }, - { - "id": 1455, - "name": "Canabalt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1456, - "name": "Canabalt", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1457, - "name": "Cube Escape: The Mill", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1458, - "name": "Broforce", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 1459, - "name": "Call of Duty: WWII", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1460, - "name": "Broforce", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1461, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1462, - "name": "Call of Duty: Infinite Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1463, - "name": "DOOM (2016)", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1464, - "name": "Costume Quest 2", - "site": "gamer_info_com", - "genres": [ - "RPG", - "приключения" - ] - }, - { - "id": 1465, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 1466, - "name": "Castlevania: Lords of Shadow", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1467, - "name": "Call of Duty: World at War", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1468, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1469, - "name": "Castlevania: Lords of Shadow", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 1470, - "name": "Call of Duty: Modern Warfare 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1471, - "name": "Dark Deception", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1472, - "name": "Broken Age", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1473, - "name": "Cat Mario", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1474, - "name": "Cat Mario", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1475, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1476, - "name": "Broken Age", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 1477, - "name": "Canabalt", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Аркадные" - ] - }, - { - "id": 1478, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1479, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1480, - "name": "Dark Messiah of Might and Magic", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1481, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1482, - "name": "Call of Duty: Modern Warfare 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1483, - "name": "Cat Quest", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1484, - "name": "Cross of the Dutchman", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 1485, - "name": "Cat Quest", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 1486, - "name": "Castlevania: Lords of Shadow", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 1487, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1488, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1489, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1490, - "name": "Dark Sector", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1491, - "name": "Brothers: A Tale Of Two Sons", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1492, - "name": "Cry Of Fear", - "site": "gamer_info_com", - "genres": [ - "инди", - "FPS", - "ужасы", - "action" - ] - }, - { - "id": 1493, - "name": "Child Of Light", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 1494, - "name": "Child Of Light", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 1495, - "name": "Call of Duty: United Offensive", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 1496, - "name": "Castlevania: Lords of Shadow 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 1497, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1498, - "name": "Castlevania: Lords of Shadow 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1499, - "name": "Dark Souls II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1500, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1501, - "name": "Brutal Legend", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1502, - "name": "Crysis", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1503, - "name": "Children of Morta", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 1504, - "name": "Children of Morta", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1505, - "name": "Cat Mario", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1506, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1507, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1508, - "name": "Call of Duty: WWII", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1509, - "name": "Brutal Legend", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Adventure" - ] - }, - { - "id": 1510, - "name": "Cat Mario", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1511, - "name": "Choice Chamber", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1512, - "name": "Crysis 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1513, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1514, - "name": "Brink", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1515, - "name": "Choice Chamber", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1516, - "name": "Cat Quest", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 1517, - "name": "Castlevania: Lords of Shadow 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 1518, - "name": "Cat Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1519, - "name": "Call of Duty: World at War", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1520, - "name": "Claire", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1521, - "name": "Crysis Warhead", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1522, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1523, - "name": "Buff Knight Advanced", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1524, - "name": "Claire", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1525, - "name": "Buff Knight Advanced", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Casual" - ] - }, - { - "id": 1526, - "name": "Child Of Light", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Платформер" - ] - }, - { - "id": 1527, - "name": "Canabalt", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1528, - "name": "Child Of Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1529, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1530, - "name": "Cat Mario", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1531, - "name": "Cube Escape: Arles", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1532, - "name": "Clustertruck", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 1533, - "name": "Bulb Boy", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1534, - "name": "Clustertruck", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 1535, - "name": "Dark Souls III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1536, - "name": "Children of Morta", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Кооператив", - "Ретро", - "Ролевая", - "Экшен" - ] - }, - { - "id": 1537, - "name": "Broforce", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1538, - "name": "Bulb Boy", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 1539, - "name": "Children of Morta", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1540, - "name": "Castlevania: Lords of Shadow", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1541, - "name": "Cube Escape: Case 23", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1542, - "name": "Cat Quest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1543, - "name": "Cold Fear", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1544, - "name": "Cold Fear", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 1545, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1546, - "name": "Choice Chamber", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1547, - "name": "Bulletstorm", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1548, - "name": "Choice Chamber", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1549, - "name": "Cube Escape: Harvey's Box", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1550, - "name": "Broken Age", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1551, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1552, - "name": "Child Of Light", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди" - ] - }, - { - "id": 1553, - "name": "Bulletstorm", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1554, - "name": "Costume Quest", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 1555, - "name": "Costume Quest", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 1556, - "name": "Claire", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1557, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1558, - "name": "Cube Escape: Seasons", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1559, - "name": "Call of Cthulhu (2018)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1560, - "name": "Call of Cthulhu (2018)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1561, - "name": "Claire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1562, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1563, - "name": "Children of Morta", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1564, - "name": "Costume Quest 2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 1565, - "name": "Dark Souls: Prepare to Die Edition", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1566, - "name": "Clustertruck", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "От первого лица" - ] - }, - { - "id": 1567, - "name": "Brothers: A Tale Of Two Sons", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1568, - "name": "Costume Quest 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Japanese-Style", - "Role-Playing" - ] - }, - { - "id": 1569, - "name": "Cube Escape: The Lake", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1570, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1571, - "name": "Clustertruck", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1572, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1573, - "name": "Choice Chamber", - "site": "ag_ru", - "genres": [ - "ММО", - "Инди", - "Экшены" - ] - }, - { - "id": 1574, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 1575, - "name": "Castlevania: Lords of Shadow 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1576, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1577, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1578, - "name": "Cold Fear", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1579, - "name": "Cross of the Dutchman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1580, - "name": "Cube Escape: The Mill", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1581, - "name": "Dark Souls: Remastered", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1582, - "name": "Cold Fear", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1583, - "name": "Claire", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1584, - "name": "Cat Mario", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1585, - "name": "Cry Of Fear", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1586, - "name": "Brutal Legend", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 1587, - "name": "Cross of the Dutchman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1588, - "name": "Costume Quest", - "site": "playground_ru", - "genres": [ - "Юмор", - "Адвенчура", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 1589, - "name": "DOOM (2016)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1590, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1591, - "name": "Call of Duty", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1592, - "name": "Costume Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1593, - "name": "Call of Duty", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1594, - "name": "Clustertruck", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Аркадные" - ] - }, - { - "id": 1595, - "name": "Crysis", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1596, - "name": "Cat Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1597, - "name": "Cry Of Fear", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1598, - "name": "Darksiders", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1599, - "name": "Buff Knight Advanced", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1600, - "name": "Costume Quest 2", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 1601, - "name": "Dark Deception", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1602, - "name": "Costume Quest 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1603, - "name": "Cold Fear", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1604, - "name": "Darksiders II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1605, - "name": "Child Of Light", - "site": "stopgame_ru", - "genres": [ - "arcade", - "rpg" - ] - }, - { - "id": 1606, - "name": "Crysis 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1607, - "name": "Crysis", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1608, - "name": "Call of Duty 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1609, - "name": "Dark Messiah of Might and Magic", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1610, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1611, - "name": "Call of Duty 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1612, - "name": "Bulb Boy", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1613, - "name": "Costume Quest", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Семейные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 1614, - "name": "Darksiders III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1615, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1616, - "name": "Cross of the Dutchman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1617, - "name": "Crysis Warhead", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1618, - "name": "Dark Sector", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 1619, - "name": "Children of Morta", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1620, - "name": "Crysis 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1621, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1622, - "name": "Daylight", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1623, - "name": "Cube Escape: Arles", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1624, - "name": "Costume Quest 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Ролевые", - "Приключения" - ] - }, - { - "id": 1625, - "name": "Cross of the Dutchman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1626, - "name": "Cry Of Fear", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1627, - "name": "Bulletstorm", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1628, - "name": "Dark Souls II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1629, - "name": "Call of Duty 4: Modern Warfare", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1630, - "name": "Crysis Warhead", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1631, - "name": "Choice Chamber", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1632, - "name": "Cube Escape: Case 23", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1633, - "name": "Dead Island", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1634, - "name": "Call of Duty: Advanced Warfare", - "site": "gamespot_com", - "genres": [ - "Tactical", - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 1635, - "name": "Cry Of Fear", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1636, - "name": "Call of Cthulhu (2018)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1637, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1638, - "name": "Crysis", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1639, - "name": "Cube Escape: Arles", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1640, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1641, - "name": "Cube Escape: Harvey's Box", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1642, - "name": "Call of Duty: Advanced Warfare", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1643, - "name": "Claire", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1644, - "name": "Dead Island: Riptide", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1645, - "name": "Cube Escape: Seasons", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1646, - "name": "Cube Escape: Case 23", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1647, - "name": "Crysis", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1648, - "name": "Crysis 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1649, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1650, - "name": "Call of Duty: Black Ops", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1651, - "name": "Dead Island: Ryder White", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1652, - "name": "Cross of the Dutchman", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1653, - "name": "Clustertruck", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1654, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1655, - "name": "Cube Escape: The Lake", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1656, - "name": "Cube Escape: Harvey's Box", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1657, - "name": "Crysis 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1658, - "name": "Crysis Warhead", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1659, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1660, - "name": "Call of Duty: Black Ops", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1661, - "name": "Cube Escape: The Mill", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1662, - "name": "Dead Rising", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1663, - "name": "Cry Of Fear", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1664, - "name": "Cube Escape: Seasons", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1665, - "name": "Cold Fear", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1666, - "name": "Call of Duty: Black Ops II", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1667, - "name": "Cube Escape: Arles", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1668, - "name": "DOOM (2016)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1669, - "name": "Crysis Warhead", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1670, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1671, - "name": "Call of Duty", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1672, - "name": "Cube Escape: The Lake", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1673, - "name": "Dead Space", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1674, - "name": "Crysis", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1675, - "name": "Costume Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1676, - "name": "Call of Duty: Black Ops III", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1677, - "name": "Dark Deception", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1678, - "name": "Cube Escape: Case 23", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1679, - "name": "Cube Escape: The Mill", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1680, - "name": "Call of Duty: Black Ops II", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1681, - "name": "Dark Souls III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1682, - "name": "Dead Space 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1683, - "name": "Cube Escape: Arles", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1684, - "name": "Dark Messiah of Might and Magic", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1685, - "name": "Crysis 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1686, - "name": "Call of Duty 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1687, - "name": "Costume Quest 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1688, - "name": "DOOM (2016)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1689, - "name": "Cube Escape: Harvey's Box", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1690, - "name": "Dead Space 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1691, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1692, - "name": "Call of Duty: Ghosts", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1693, - "name": "Cube Escape: Case 23", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1694, - "name": "Dark Deception", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1695, - "name": "Cube Escape: Seasons", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1696, - "name": "Dark Sector", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1697, - "name": "Dead Space 3: Awakened (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1698, - "name": "Crysis Warhead", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1699, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1700, - "name": "Call of Duty: Black Ops III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1701, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1702, - "name": "Call of Duty 4: Modern Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1703, - "name": "Dark Messiah of Might and Magic", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1704, - "name": "Dark Souls II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1705, - "name": "Cube Escape: Harvey's Box", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1706, - "name": "Cube Escape: The Lake", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1707, - "name": "Call of Duty: Infinite Warfare", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 1708, - "name": "Dead by Daylight", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1709, - "name": "Cube Escape: Arles", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1710, - "name": "Dark Sector", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1711, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1712, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1713, - "name": "Cross of the Dutchman", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1714, - "name": "Cube Escape: The Mill", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1715, - "name": "Cube Escape: Seasons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1716, - "name": "Call of Duty: Advanced Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1717, - "name": "Deadlight", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 1718, - "name": "Dark Souls II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1719, - "name": "Cube Escape: Case 23", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1720, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1721, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1722, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1723, - "name": "Call of Duty: Ghosts", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1724, - "name": "Cry Of Fear", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 1725, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1726, - "name": "Cube Escape: The Lake", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1727, - "name": "Deadpool", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1728, - "name": "DOOM (2016)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1729, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1730, - "name": "Cube Escape: Harvey's Box", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1731, - "name": "Dark Souls: Remastered", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1732, - "name": "Call of Duty: Black Ops", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1733, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1734, - "name": "Crysis", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1735, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1736, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1737, - "name": "DeathSpank", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 1738, - "name": "Dark Deception", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1739, - "name": "Cube Escape: The Mill", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1740, - "name": "Cube Escape: Seasons", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1741, - "name": "Call of Duty: Infinite Warfare", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1742, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1743, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1744, - "name": "Dark Souls III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1745, - "name": "Call of Duty: United Offensive", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1746, - "name": "Dark Messiah of Might and Magic", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1747, - "name": "Crysis 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1748, - "name": "DeathSpank: Thongs of Virtue", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1749, - "name": "DOOM (2016)", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 1750, - "name": "Cube Escape: The Lake", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1751, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1752, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1753, - "name": "Darksiders", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1754, - "name": "Call of Duty: Black Ops II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1755, - "name": "Deathtrap", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 1756, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1757, - "name": "Crysis Warhead", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1758, - "name": "Dark Sector", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер", - "Киберпанк" - ] - }, - { - "id": 1759, - "name": "Dark Souls III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1760, - "name": "Call of Duty: WWII", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1761, - "name": "Call of Duty: Modern Warfare 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1762, - "name": "Cube Escape: The Mill", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1763, - "name": "Dark Deception", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1764, - "name": "Darksiders II", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1765, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1766, - "name": "Demigod", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия", - "Ролевая игра" - ] - }, - { - "id": 1767, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1768, - "name": "Dark Souls II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1769, - "name": "Cube Escape: Arles", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1770, - "name": "Call of Duty: Black Ops III", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1771, - "name": "Dark Messiah of Might and Magic", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1772, - "name": "Call of Duty: World at War", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1773, - "name": "DOOM (2016)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1774, - "name": "Darksiders III", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1775, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1776, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1777, - "name": "Descenders", - "site": "igromania_ru", - "genres": [ - "Мототехника", - "Симулятор" - ] - }, - { - "id": 1778, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1779, - "name": "Call of Duty: Modern Warfare 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1780, - "name": "Cube Escape: Case 23", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1781, - "name": "Dark Sector", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1782, - "name": "Daylight", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 1783, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1784, - "name": "Dark Souls: Remastered", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1785, - "name": "Detention", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1786, - "name": "Call of Duty: Ghosts", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1787, - "name": "Dark Deception", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1788, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1789, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1790, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1791, - "name": "Canabalt", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1792, - "name": "Cube Escape: Harvey's Box", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1793, - "name": "Dead Island", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1794, - "name": "Dark Souls II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1795, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1796, - "name": "Dark Messiah of Might and Magic", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1797, - "name": "Call of Duty: United Offensive", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1798, - "name": "Dark Souls: Remastered", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1799, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1800, - "name": "Darksiders", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1801, - "name": "Call of Duty: Infinite Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1802, - "name": "Cube Escape: Seasons", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1803, - "name": "Dead Island: Riptide", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1804, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1805, - "name": "Dark Sector", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1806, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1807, - "name": "Devil May Cry 4", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1808, - "name": "Darksiders II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1809, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1810, - "name": "Dead Island: Ryder White", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1811, - "name": "Cube Escape: The Lake", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1812, - "name": "Castlevania: Lords of Shadow", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1813, - "name": "Darksiders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1814, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1815, - "name": "Call of Duty: WWII", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1816, - "name": "Devil May Cry 5", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1817, - "name": "Darksiders III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1818, - "name": "Call of Duty: Modern Warfare 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1819, - "name": "Dark Souls III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1820, - "name": "Dark Souls II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1821, - "name": "Darksiders II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1822, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1823, - "name": "Dead Rising", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1824, - "name": "Diablo II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1825, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1826, - "name": "Cube Escape: The Mill", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1827, - "name": "Call of Duty: Modern Warfare 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1828, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1829, - "name": "Daylight", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1830, - "name": "Darksiders III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1831, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1832, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1833, - "name": "Call of Duty: World at War", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1834, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1835, - "name": "Dead Space", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1836, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1837, - "name": "DOOM (2016)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1838, - "name": "Daylight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1839, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1840, - "name": "Diablo III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1841, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1842, - "name": "Call of Duty: United Offensive", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1843, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1844, - "name": "Dead Space 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1845, - "name": "Dead Island", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 1846, - "name": "Dead Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1847, - "name": "Dark Souls III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1848, - "name": "Dark Deception", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1849, - "name": "Dark Souls: Prepare to Die Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1850, - "name": "Canabalt", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 1851, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1852, - "name": "Cat Mario", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1853, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1854, - "name": "Dead Island: Riptide", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1855, - "name": "Dead Space 3", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1856, - "name": "Dig or Die", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1857, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1858, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1859, - "name": "Castlevania: Lords of Shadow", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1860, - "name": "Dark Messiah of Might and Magic", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1861, - "name": "Call of Duty: WWII", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1862, - "name": "Dead Island: Ryder White", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1863, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1864, - "name": "Cat Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1865, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1866, - "name": "Disciples II: Dark Prophecy", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1867, - "name": "Dark Souls: Remastered", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1868, - "name": "Dead Island: Riptide", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1869, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1870, - "name": "Dead Rising", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1871, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1872, - "name": "Dark Sector", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1873, - "name": "Disciples III: Renaissance", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1874, - "name": "Dark Souls III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1875, - "name": "Dead by Daylight", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1876, - "name": "Dead Island: Ryder White", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1877, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1878, - "name": "Child Of Light", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 1879, - "name": "Dead Space", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1880, - "name": "Dark Souls: Prepare to Die Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1881, - "name": "Call of Duty: World at War", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1882, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1883, - "name": "Dishonored", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1884, - "name": "Dark Souls II", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1885, - "name": "Deadlight", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 1886, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1887, - "name": "Dead Space 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1888, - "name": "Dead Rising", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1889, - "name": "Darksiders", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер", - "Фэнтези" - ] - }, - { - "id": 1890, - "name": "Children of Morta", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1891, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1892, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1893, - "name": "Dead Space 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1894, - "name": "Deadpool", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1895, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1896, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1897, - "name": "Canabalt", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1898, - "name": "Darksiders II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1899, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1900, - "name": "Dead Space", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 1901, - "name": "Choice Chamber", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1902, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1903, - "name": "Castlevania: Lords of Shadow 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1904, - "name": "Dark Souls: Remastered", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "РПГ", - "Экшен" - ] - }, - { - "id": 1905, - "name": "DeathSpank", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1906, - "name": "Darksiders III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1907, - "name": "Dark Souls: Prepare to Die Edition", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1908, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1909, - "name": "Castlevania: Lords of Shadow", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1910, - "name": "Dead by Daylight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1911, - "name": "Distraint", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1912, - "name": "Cat Mario", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1913, - "name": "Dead Space 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 1914, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1915, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1916, - "name": "Deadlight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1917, - "name": "Disturbed", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 1918, - "name": "Daylight", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 1919, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1920, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1921, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1922, - "name": "Claire", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1923, - "name": "Deathtrap", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1924, - "name": "Darksiders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1925, - "name": "Deadpool", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1926, - "name": "DmC: Devil May Cry", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1927, - "name": "Dark Souls: Remastered", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1928, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1929, - "name": "Dead Island", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 1930, - "name": "Cat Quest", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 1931, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1932, - "name": "Demigod", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1933, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1934, - "name": "DeathSpank", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1935, - "name": "Darksiders II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1936, - "name": "Clustertruck", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 1937, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1938, - "name": "Dark Souls III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1939, - "name": "Dead Space 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1940, - "name": "Dead Island: Riptide", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1941, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1942, - "name": "Doki Doki Literature Club", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1943, - "name": "Descenders", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "спорт" - ] - }, - { - "id": 1944, - "name": "Castlevania: Lords of Shadow 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1945, - "name": "Darksiders III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1946, - "name": "Child Of Light", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1947, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1948, - "name": "Deathtrap", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1949, - "name": "Dead Island: Ryder White", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1950, - "name": "Don't Knock Twice", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1951, - "name": "Cold Fear", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 1952, - "name": "Detention", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1953, - "name": "Cat Mario", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1954, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1955, - "name": "Darksiders", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1956, - "name": "Daylight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1957, - "name": "Demigod", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1958, - "name": "Dead by Daylight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1959, - "name": "Doom 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1960, - "name": "Dead Rising", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Адвенчура", - "Песочница", - "Выживание", - "Зомби", - "Экшен", - "Открытый мир" - ] - }, - { - "id": 1961, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1962, - "name": "Children of Morta", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 1963, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1964, - "name": "Descenders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1965, - "name": "Darksiders II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1966, - "name": "Costume Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1967, - "name": "Dead Island", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1968, - "name": "Double Dragon Neon", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1969, - "name": "Cat Quest", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 1970, - "name": "Deadlight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1971, - "name": "Detention", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1972, - "name": "Devil May Cry 4", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1973, - "name": "Dead Space", - "site": "playground_ru", - "genres": [ - "Экшен", - "Космос", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1974, - "name": "Dark Souls: Prepare to Die Edition", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1975, - "name": "Darksiders III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 1976, - "name": "Dead Island: Riptide", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1977, - "name": "Costume Quest 2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 1978, - "name": "Down Stairs", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1979, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1980, - "name": "Choice Chamber", - "site": "store_steampowered_com", - "genres": [ - "Massively Multiplayer", - "Action", - "Indie" - ] - }, - { - "id": 1981, - "name": "Devil May Cry 5", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1982, - "name": "Dead Space 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Космос", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1983, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1984, - "name": "Devil May Cry 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1985, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1986, - "name": "Downfall", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1987, - "name": "Dead Island: Ryder White", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1988, - "name": "Child Of Light", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1989, - "name": "Daylight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 1990, - "name": "Diablo II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 1991, - "name": "Devil May Cry 5", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1992, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1993, - "name": "Dead Space 3", - "site": "playground_ru", - "genres": [ - "Космос", - "От третьего лица", - "Ужасы", - "Кооператив", - "Экшен" - ] - }, - { - "id": 1994, - "name": "Dead Island", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1995, - "name": "Cross of the Dutchman", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1996, - "name": "Dark Souls: Remastered", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1997, - "name": "Dead Rising", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1998, - "name": "Claire", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1999, - "name": "Children of Morta", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2000, - "name": "Deadpool", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2001, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2002, - "name": "Diablo II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2003, - "name": "Dragon Age II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2004, - "name": "Dead Space", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2005, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2006, - "name": "Cry Of Fear", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2007, - "name": "Dead Space 3: Awakened (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2008, - "name": "Dead Island: Riptide", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 2009, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2010, - "name": "DeathSpank", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2011, - "name": "Diablo III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 2012, - "name": "Dragon Age: Inquisition", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2013, - "name": "Clustertruck", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 2014, - "name": "Choice Chamber", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2015, - "name": "Diablo III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2016, - "name": "Dead Space 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2017, - "name": "Dead Island: Ryder White", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2018, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2019, - "name": "Darksiders", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2020, - "name": "Dead by Daylight", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "От третьего лица", - "Ужасы", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2021, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2022, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Приключения", - "Головоломки/Пазл", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2023, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2024, - "name": "Claire", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2025, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2026, - "name": "Dead Space 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2027, - "name": "Cold Fear", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2028, - "name": "Crysis", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2029, - "name": "Dig or Die", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 2030, - "name": "Deathtrap", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2031, - "name": "Darksiders II", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2032, - "name": "Deadlight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Зомби" - ] - }, - { - "id": 2033, - "name": "Dig or Die", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2034, - "name": "Dead Rising", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2035, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2036, - "name": "Disciples II: Dark Prophecy", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2037, - "name": "Dead Space 3: Awakened (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2038, - "name": "Disciples II: Dark Prophecy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2039, - "name": "Demigod", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2040, - "name": "Darksiders III", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 2041, - "name": "Deadpool", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Юмор" - ] - }, - { - "id": 2042, - "name": "Crysis 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2043, - "name": "Clustertruck", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 2044, - "name": "Costume Quest", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "RPG", - "Adventure" - ] - }, - { - "id": 2045, - "name": "Dead Space", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2046, - "name": "Dragon Age: Origins", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2047, - "name": "Disciples III: Renaissance", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2048, - "name": "Disciples III: Renaissance", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2049, - "name": "Descenders", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Симулятор", - "Спортивные игры" - ] - }, - { - "id": 2050, - "name": "Dead by Daylight", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 2051, - "name": "DeathSpank", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая" - ] - }, - { - "id": 2052, - "name": "Daylight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2053, - "name": "Dragon Age: Origins - Awakening", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2054, - "name": "Dishonored", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2055, - "name": "Dead Space 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2056, - "name": "Cold Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2057, - "name": "Dishonored", - "site": "gamer_info_com", - "genres": [ - "stealth" - ] - }, - { - "id": 2058, - "name": "Detention", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 2059, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2060, - "name": "Deadlight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2061, - "name": "DeathSpank: Thongs of Virtue", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая" - ] - }, - { - "id": 2062, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2063, - "name": "Dead Island", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2064, - "name": "Costume Quest 2", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "RPG", - "Adventure" - ] - }, - { - "id": 2065, - "name": "Crysis Warhead", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2066, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2067, - "name": "Dead Space 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2068, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2069, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2070, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 2071, - "name": "Deadpool", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2072, - "name": "Costume Quest", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2073, - "name": "Cube Escape: Arles", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2074, - "name": "Deathtrap", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 2075, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2076, - "name": "Dead Island: Riptide", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2077, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2078, - "name": "Distraint", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2079, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2080, - "name": "Cube Escape: Case 23", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2081, - "name": "Dead Space 3: Awakened (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2082, - "name": "Devil May Cry 4", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2083, - "name": "DeathSpank", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2084, - "name": "Demigod", - "site": "playground_ru", - "genres": [ - "Экшен", - "MOBA", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 2085, - "name": "Disturbed", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2086, - "name": "Costume Quest 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2087, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2088, - "name": "Dead Island: Ryder White", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2089, - "name": "Distraint", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения" - ] - }, - { - "id": 2090, - "name": "Cube Escape: Harvey's Box", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2091, - "name": "Devil May Cry 5", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2092, - "name": "Cross of the Dutchman", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2093, - "name": "Dead by Daylight", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2094, - "name": "DeathSpank: Thongs of Virtue", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2095, - "name": "DmC: Devil May Cry", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2096, - "name": "Descenders", - "site": "playground_ru", - "genres": [ - "Аркада", - "Гонки", - "Симулятор", - "Спорт" - ] - }, - { - "id": 2097, - "name": "Dragon's Dogma: Dark Arisen", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2098, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2099, - "name": "Cube Escape: Seasons", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2100, - "name": "Disturbed", - "site": "gamer_info_com", - "genres": [ - "инди", - "визуальные новеллы", - "ужасы", - "приключения" - ] - }, - { - "id": 2101, - "name": "Dead Rising", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2102, - "name": "Diablo II", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2103, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2104, - "name": "Deadlight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 2105, - "name": "Deathtrap", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2106, - "name": "Draw a Stickman: Episode 1", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2107, - "name": "Detention", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Выживание" - ] - }, - { - "id": 2108, - "name": "Cube Escape: The Lake", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2109, - "name": "DmC: Devil May Cry", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2110, - "name": "Cry Of Fear", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2111, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2112, - "name": "Doki Doki Literature Club", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2113, - "name": "Dead Space", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2114, - "name": "Draw a Stickman: Episode 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2115, - "name": "Cube Escape: The Mill", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2116, - "name": "Cross of the Dutchman", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2117, - "name": "Demigod", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2118, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2119, - "name": "Don't Knock Twice", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2120, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2121, - "name": "DreadOut", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 2122, - "name": "DOOM (2016)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2123, - "name": "Dead Space 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2124, - "name": "Crysis", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2125, - "name": "Doom 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2126, - "name": "Descenders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2127, - "name": "Deadpool", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2128, - "name": "Devil May Cry 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2129, - "name": "Doki Doki Literature Club", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2130, - "name": "Dropsy", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2131, - "name": "Cry Of Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2132, - "name": "Diablo III", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2133, - "name": "Double Dragon Neon", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2134, - "name": "Dead Space 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2135, - "name": "DeathSpank", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения", - "Файтинги" - ] - }, - { - "id": 2136, - "name": "Crysis 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2137, - "name": "Detention", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2138, - "name": "Don't Knock Twice", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2139, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2140, - "name": "Devil May Cry 5", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2141, - "name": "Dude, Stop", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 2142, - "name": "Down Stairs", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2143, - "name": "Dark Deception", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 2144, - "name": "Dead Space 3: Awakened (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2145, - "name": "Crysis", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2146, - "name": "Dig or Die", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2147, - "name": "Diablo II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2148, - "name": "Doom 3", - "site": "gamer_info_com", - "genres": [ - "FPS", - "ужасы", - "action" - ] - }, - { - "id": 2149, - "name": "DeathSpank: Thongs of Virtue", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 2150, - "name": "Downfall", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2151, - "name": "Duke Nukem Forever", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2152, - "name": "Crysis Warhead", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2153, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2154, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2155, - "name": "Dark Messiah of Might and Magic", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2156, - "name": "Dead by Daylight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2157, - "name": "Double Dragon Neon", - "site": "gamer_info_com", - "genres": [ - "beat 'em up" - ] - }, - { - "id": 2158, - "name": "Disciples II: Dark Prophecy", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 2159, - "name": "Deathtrap", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Стратегии" - ] - }, - { - "id": 2160, - "name": "Dungeon Nightmares", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2161, - "name": "Cube Escape: Arles", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2162, - "name": "Crysis 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2163, - "name": "Diablo III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2164, - "name": "Dragon Age II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2165, - "name": "Disciples III: Renaissance", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 2166, - "name": "Down Stairs", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2167, - "name": "Dying Light", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2168, - "name": "Demigod", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2169, - "name": "Deadlight", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 2170, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2171, - "name": "Dark Sector", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2172, - "name": "Dragon Age: Inquisition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2173, - "name": "Cube Escape: Case 23", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2174, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2175, - "name": "Dying Light: The Following (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2176, - "name": "Downfall", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2177, - "name": "Crysis Warhead", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2178, - "name": "Deadpool", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2179, - "name": "Devil May Cry 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2180, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2181, - "name": "Dig or Die", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2182, - "name": "Descenders", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Спортивные" - ] - }, - { - "id": 2183, - "name": "Cube Escape: Harvey's Box", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2184, - "name": "Dark Souls II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2185, - "name": "Emily Wants To Play", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2186, - "name": "Cube Escape: Arles", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2187, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2188, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 2189, - "name": "DeathSpank", - "site": "stopgame_ru", - "genres": [ - "adventure", - "rpg" - ] - }, - { - "id": 2190, - "name": "Devil May Cry 5", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2191, - "name": "Disciples II: Dark Prophecy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2192, - "name": "Dishonored", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2193, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2194, - "name": "Escape Dead Island", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2195, - "name": "Detention", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 2196, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2197, - "name": "Cube Escape: Seasons", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2198, - "name": "Cube Escape: Case 23", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2199, - "name": "Dragon Age II", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2200, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2201, - "name": "Disciples III: Renaissance", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2202, - "name": "DeathSpank: Thongs of Virtue", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2203, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2204, - "name": "Diablo II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2205, - "name": "Dragon Age: Origins", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2206, - "name": "Evil Defenders", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2207, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2208, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2209, - "name": "Cube Escape: The Lake", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2210, - "name": "Dragon Age: Inquisition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2211, - "name": "Dragon Age: Origins - Awakening", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2212, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2213, - "name": "Cube Escape: Harvey's Box", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2214, - "name": "Dishonored", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Стимпанк", - "Стелс" - ] - }, - { - "id": 2215, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2216, - "name": "Evoland", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2217, - "name": "Deathtrap", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 2218, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2219, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2220, - "name": "Distraint", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2221, - "name": "Devil May Cry 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2222, - "name": "Cube Escape: The Mill", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2223, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2224, - "name": "Cube Escape: Seasons", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2225, - "name": "Evoland 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2226, - "name": "Diablo III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2227, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2228, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2229, - "name": "Disturbed", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2230, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2231, - "name": "Demigod", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 2232, - "name": "Devil May Cry 5", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2233, - "name": "DOOM (2016)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2234, - "name": "Eyes The Horror Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2235, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2236, - "name": "Cube Escape: The Lake", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2237, - "name": "Dark Souls III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2238, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2239, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2240, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2241, - "name": "Descenders", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2242, - "name": "F.E.A.R.", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2243, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2244, - "name": "Diablo II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2245, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2246, - "name": "Dig or Die", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2247, - "name": "Cube Escape: The Mill", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2248, - "name": "Dragon Age: Origins", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2249, - "name": "Dark Deception", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2250, - "name": "Distraint", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "Инди" - ] - }, - { - "id": 2251, - "name": "F.E.A.R. 2: Project Origin", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2252, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2253, - "name": "Detention", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2254, - "name": "DmC: Devil May Cry", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2255, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2256, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2257, - "name": "DOOM (2016)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2258, - "name": "Dragon Age: Origins - Awakening", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2259, - "name": "Disciples II: Dark Prophecy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2260, - "name": "F.E.A.R. 2: Reborn", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2261, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2262, - "name": "Dark Messiah of Might and Magic", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2263, - "name": "Draw a Stickman: Episode 1", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2264, - "name": "Disturbed", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 2265, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2266, - "name": "Diablo III", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2267, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2268, - "name": "Draw a Stickman: Episode 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2269, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2270, - "name": "F.E.A.R. 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2271, - "name": "Disciples III: Renaissance", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2272, - "name": "Doki Doki Literature Club", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2273, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2274, - "name": "DmC: Devil May Cry", - "site": "playground_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 2275, - "name": "Dark Deception", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2276, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2277, - "name": "DreadOut", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2278, - "name": "Dark Sector", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2279, - "name": "Devil May Cry 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2280, - "name": "F.E.A.R. Extraction Point", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2281, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2282, - "name": "Dark Souls: Remastered", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2283, - "name": "Don't Knock Twice", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2284, - "name": "Dishonored", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 2285, - "name": "Dropsy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2286, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2287, - "name": "Dig or Die", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 2288, - "name": "Dark Messiah of Might and Magic", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2289, - "name": "F.E.A.R. Perseus Mandate", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2290, - "name": "Devil May Cry 5", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2291, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2292, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2293, - "name": "Dude, Stop", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2294, - "name": "Doom 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2295, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2296, - "name": "Dark Souls II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 2297, - "name": "Doki Doki Literature Club", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2298, - "name": "Disciples II: Dark Prophecy", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2299, - "name": "Fable (2004)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2300, - "name": "Diablo II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2301, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2302, - "name": "Duke Nukem Forever", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2303, - "name": "Double Dragon Neon", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2304, - "name": "Fable III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2305, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2306, - "name": "Don't Knock Twice", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Виртуальная реальность", - "От первого лица" - ] - }, - { - "id": 2307, - "name": "Dark Sector", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2308, - "name": "Disciples III: Renaissance", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2309, - "name": "Dungeon Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2310, - "name": "Darksiders", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2311, - "name": "Down Stairs", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2312, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2313, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2314, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2315, - "name": "Fahrenheit", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2316, - "name": "Distraint", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2317, - "name": "Dying Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2318, - "name": "Downfall", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2319, - "name": "Doom 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 2320, - "name": "Draw a Stickman: Episode 1", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2321, - "name": "Dishonored", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 2322, - "name": "Diablo III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2323, - "name": "Failman", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2324, - "name": "Dark Souls II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2325, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2326, - "name": "Dying Light: The Following (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2327, - "name": "Darksiders II", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2328, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2329, - "name": "Disturbed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2330, - "name": "Double Dragon Neon", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2331, - "name": "Draw a Stickman: Episode 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2332, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2333, - "name": "Fallout 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2334, - "name": "Emily Wants To Play", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2335, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2336, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2337, - "name": "Dragon Age II", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2338, - "name": "DmC: Devil May Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2339, - "name": "Darksiders III", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2340, - "name": "Down Stairs", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2341, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2342, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2343, - "name": "Escape Dead Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2344, - "name": "DreadOut", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2345, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2346, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2347, - "name": "Downfall", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2348, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2349, - "name": "Evil Defenders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2350, - "name": "Dig or Die", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 2351, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2352, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2353, - "name": "Dropsy", - "site": "gamer_info_com", - "genres": [ - "инди", - "квесты" - ] - }, - { - "id": 2354, - "name": "Distraint", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 2355, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2356, - "name": "Evoland", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2357, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2358, - "name": "Dragon Age: Inquisition", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2359, - "name": "Daylight", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2360, - "name": "Disciples II: Dark Prophecy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2361, - "name": "Doki Doki Literature Club", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2362, - "name": "Dude, Stop", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2363, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2364, - "name": "Disturbed", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 2365, - "name": "Dark Souls III", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2366, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2367, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2368, - "name": "Evoland 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2369, - "name": "Dragon Age II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2370, - "name": "Duke Nukem Forever", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "FPS" - ] - }, - { - "id": 2371, - "name": "Don't Knock Twice", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2372, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2373, - "name": "Disciples III: Renaissance", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2374, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2375, - "name": "Eyes The Horror Game", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2376, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2377, - "name": "Dead Island", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2378, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2379, - "name": "Dragon Age: Inquisition", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 2380, - "name": "DmC: Devil May Cry", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2381, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2382, - "name": "Dungeon Nightmares", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2383, - "name": "F.E.A.R.", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2384, - "name": "Doom 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2385, - "name": "Dishonored", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2386, - "name": "Fallout 4", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2387, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2388, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2389, - "name": "Dark Souls III", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2390, - "name": "Dead Island: Riptide", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2391, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2392, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2393, - "name": "Dragon Age: Origins", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2394, - "name": "Dying Light", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2395, - "name": "Fallout: New Vegas", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2396, - "name": "Double Dragon Neon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2397, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2398, - "name": "F.E.A.R. 2: Reborn", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2399, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2400, - "name": "Dead Island: Ryder White", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2401, - "name": "Family Trouble", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2402, - "name": "Doki Doki Literature Club", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2403, - "name": "Dying Light: The Following (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2404, - "name": "Dragon Age: Origins - Awakening", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2405, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2406, - "name": "Down Stairs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2407, - "name": "F.E.A.R. 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2408, - "name": "Dark Souls: Prepare to Die Edition", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 2409, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2410, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2411, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2412, - "name": "Dead Rising", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2413, - "name": "Far Cry", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2414, - "name": "Emily Wants To Play", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "survival" - ] - }, - { - "id": 2415, - "name": "F.E.A.R. Extraction Point", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2416, - "name": "Downfall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2417, - "name": "Don't Knock Twice", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 2418, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2419, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2420, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2421, - "name": "Dragon Age: Origins", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Тактика", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 2422, - "name": "Distraint", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2423, - "name": "Far Cry 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2424, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2425, - "name": "Escape Dead Island", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2426, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2427, - "name": "Dead Space", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2428, - "name": "Doom 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2429, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2430, - "name": "Fable (2004)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2431, - "name": "Far Cry 3: Blood Dragon", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2432, - "name": "Disturbed", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2433, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2434, - "name": "Dragon Age: Origins - Awakening", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2435, - "name": "Evil Defenders", - "site": "gamer_info_com", - "genres": [ - "инди", - "tower defense" - ] - }, - { - "id": 2436, - "name": "Dark Souls: Remastered", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2437, - "name": "Fable III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2438, - "name": "FarSky", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2439, - "name": "Dragon Age II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2440, - "name": "Double Dragon Neon", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 2441, - "name": "Dead Space 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2442, - "name": "Dark Souls: Prepare to Die Edition", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2443, - "name": "DmC: Devil May Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2444, - "name": "Evoland", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "приключения", - "action" - ] - }, - { - "id": 2445, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2446, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2447, - "name": "Fahrenheit", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2448, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2449, - "name": "Final Fantasy III (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2450, - "name": "Dragon Age: Inquisition", - "site": "iwantgames_ru", - "genres": [ - "РПГ" - ] - }, - { - "id": 2451, - "name": "Draw a Stickman: Episode 1", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2452, - "name": "Evoland 2", - "site": "gamer_info_com", - "genres": [ - "RTS", - "инди", - "приключения" - ] - }, - { - "id": 2453, - "name": "Failman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2454, - "name": "Down Stairs", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2455, - "name": "Dead Space 3", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2456, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2457, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2458, - "name": "Final Fantasy IV (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2459, - "name": "Draw a Stickman: Episode 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2460, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2461, - "name": "Fallout 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2462, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2463, - "name": "Darksiders", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2464, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2465, - "name": "Eyes The Horror Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2466, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2467, - "name": "Doki Doki Literature Club", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2468, - "name": "Downfall", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2469, - "name": "DreadOut", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2470, - "name": "Dark Souls: Remastered", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2471, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2472, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2473, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2474, - "name": "F.E.A.R.", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2475, - "name": "Final Fantasy X HD Remaster", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2476, - "name": "Darksiders II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2477, - "name": "Dropsy", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2478, - "name": "Don't Knock Twice", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2479, - "name": "Dead by Daylight", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2480, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2481, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 2482, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2483, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2484, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2485, - "name": "Dude, Stop", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2486, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2487, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2488, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2489, - "name": "Doom 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2490, - "name": "Darksiders III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 2491, - "name": "Final Fantasy XIII", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2492, - "name": "Dragon Age II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2493, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2494, - "name": "Dragon Age: Origins", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2495, - "name": "F.E.A.R. 2: Reborn", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2496, - "name": "Duke Nukem Forever", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2497, - "name": "Darksiders", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2498, - "name": "Deadlight", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 2499, - "name": "Dragon's Dogma: Dark Arisen", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2500, - "name": "Double Dragon Neon", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 2501, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2502, - "name": "Final Fantasy XIII-2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2503, - "name": "Dragon Age: Inquisition", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2504, - "name": "Dungeon Nightmares", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2505, - "name": "Dragon Age: Origins - Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2506, - "name": "F.E.A.R. 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2507, - "name": "Draw a Stickman: Episode 1", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2508, - "name": "Daylight", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2509, - "name": "Fallout 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2510, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2511, - "name": "Down Stairs", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2512, - "name": "F.E.A.R. Extraction Point", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2513, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2514, - "name": "Darksiders II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2515, - "name": "Draw a Stickman: Episode 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2516, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 2517, - "name": "Fallout: New Vegas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2518, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2519, - "name": "Downfall", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 2520, - "name": "Dead Island", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2521, - "name": "Deadpool", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2522, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2523, - "name": "Dying Light", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2524, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2525, - "name": "Family Trouble", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2526, - "name": "DreadOut", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От третьего лица", - "Инди" - ] - }, - { - "id": 2527, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2528, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2529, - "name": "Darksiders III", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2530, - "name": "Dead Island: Riptide", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2531, - "name": "Dying Light: The Following (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2532, - "name": "Far Cry", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2533, - "name": "DeathSpank", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2534, - "name": "Fable (2004)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2535, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2536, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2537, - "name": "Dropsy", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 2538, - "name": "Final Fantasy XV", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2539, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 2540, - "name": "Far Cry 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2541, - "name": "Dead Island: Ryder White", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2542, - "name": "Emily Wants To Play", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2543, - "name": "Daylight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2544, - "name": "Fable III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2545, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2546, - "name": "Dude, Stop", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2547, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2548, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2549, - "name": "Far Cry 3: Blood Dragon", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2550, - "name": "Dragon Age II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2551, - "name": "Dragon Age: Origins", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2552, - "name": "Escape Dead Island", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2553, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2554, - "name": "Fahrenheit", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2555, - "name": "FarSky", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2556, - "name": "Duke Nukem Forever", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "От первого лица", - "Шутер" - ] - }, - { - "id": 2557, - "name": "Dragon's Dogma: Dark Arisen", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2558, - "name": "Dead Island", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2559, - "name": "Dead Rising", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2560, - "name": "Dragon Age: Inquisition", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2561, - "name": "Dragon Age: Origins - Awakening", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2562, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2563, - "name": "Failman", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2564, - "name": "Evil Defenders", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 2565, - "name": "Final Fantasy III (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2566, - "name": "Deathtrap", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2567, - "name": "Dungeon Nightmares", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2568, - "name": "Draw a Stickman: Episode 1", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2569, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2570, - "name": "Final Fantasy IV (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2571, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2572, - "name": "Fallout 3", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2573, - "name": "Evoland", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2574, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2575, - "name": "Dead Space", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2576, - "name": "Dying Light", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Кооператив", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2577, - "name": "Dead Island: Riptide", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving", - "Role-Playing (RPG)" - ] - }, - { - "id": 2578, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2579, - "name": "Draw a Stickman: Episode 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2580, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2581, - "name": "Evoland 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2582, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2583, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2584, - "name": "Demigod", - "site": "gamespot_com", - "genres": [ - "MOBA", - "Strategy" - ] - }, - { - "id": 2585, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2586, - "name": "Final Fantasy X HD Remaster", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2587, - "name": "Eyes The Horror Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2588, - "name": "Firewatch", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2589, - "name": "Dead Space 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2590, - "name": "DreadOut", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2591, - "name": "Dying Light: The Following (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2592, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2593, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2594, - "name": "F.E.A.R.", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2595, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2596, - "name": "Dead Island: Ryder White", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2597, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2598, - "name": "First Winter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2599, - "name": "Emily Wants To Play", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2600, - "name": "Dropsy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2601, - "name": "Dead Space 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2602, - "name": "Descenders", - "site": "gamespot_com", - "genres": [ - "Arcade", - "Driving/Racing" - ] - }, - { - "id": 2603, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2604, - "name": "Final Fantasy XIII", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2605, - "name": "Dragon Age: Origins", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2606, - "name": "Five Minutes", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2607, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2608, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2609, - "name": "Escape Dead Island", - "site": "playground_ru", - "genres": [ - "Экшен", - "Выживание", - "Вид сверху" - ] - }, - { - "id": 2610, - "name": "Final Fantasy XIII-2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2611, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2612, - "name": "Dude, Stop", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2613, - "name": "Dead Space 3: Awakened (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2614, - "name": "Dead Rising", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2615, - "name": "F.E.A.R. 2: Reborn", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2616, - "name": "Dragon Age: Origins - Awakening", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2617, - "name": "Five Nights At Freddy's", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2618, - "name": "Dragon's Dogma: Dark Arisen", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2619, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2620, - "name": "Evil Defenders", - "site": "playground_ru", - "genres": [ - "Аркада", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 2621, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2622, - "name": "Duke Nukem Forever", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2623, - "name": "Detention", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2624, - "name": "Five Nights At Freddy's 2", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2625, - "name": "F.E.A.R. 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2626, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2627, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2628, - "name": "Dead by Daylight", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2629, - "name": "Draw a Stickman: Episode 1", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2630, - "name": "Dead Space", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2631, - "name": "Fallout 4", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2632, - "name": "Evoland", - "site": "playground_ru", - "genres": [ - "Инди", - "Вид сверху", - "Адвенчура", - "Ретро", - "Экшен" - ] - }, - { - "id": 2633, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2634, - "name": "Five Nights At Freddy's 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик" - ] - }, - { - "id": 2635, - "name": "Dungeon Nightmares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2636, - "name": "F.E.A.R. Extraction Point", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2637, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2638, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2639, - "name": "Evoland 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2640, - "name": "Draw a Stickman: Episode 2", - "site": "ag_ru", - "genres": [ - "Приключения", - "Семейные" - ] - }, - { - "id": 2641, - "name": "Fallout: New Vegas", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2642, - "name": "Final Fantasy XV", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2643, - "name": "Five Nights At Freddy's 4", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2644, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2645, - "name": "Dying Light", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 2646, - "name": "Deadlight", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2647, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2648, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2649, - "name": "Dead Space 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2650, - "name": "Eyes The Horror Game", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2651, - "name": "Five Nights at Freddy's 3D", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2652, - "name": "Family Trouble", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2653, - "name": "DreadOut", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2654, - "name": "Fable (2004)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2655, - "name": "Devil May Cry 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2656, - "name": "Dying Light: The Following (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2657, - "name": "Deadpool", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2658, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2659, - "name": "Far Cry", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2660, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2661, - "name": "Five Nights in Anime v3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2662, - "name": "Dropsy", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 2663, - "name": "F.E.A.R.", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Ужасы" - ] - }, - { - "id": 2664, - "name": "Fable III", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2665, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2666, - "name": "Devil May Cry 5", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2667, - "name": "Far Cry 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2668, - "name": "Fran Bow", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2669, - "name": "Emily Wants To Play", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2670, - "name": "Dragon's Dogma: Dark Arisen", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 2671, - "name": "Dude, Stop", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 2672, - "name": "Dead Space 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2673, - "name": "Fahrenheit", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2674, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2675, - "name": "DeathSpank", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 2676, - "name": "F.E.A.R. 2: Project Origin", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Ужасы" - ] - }, - { - "id": 2677, - "name": "Front Mission Evolved", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 2678, - "name": "Diablo II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2679, - "name": "Far Cry 3: Blood Dragon", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2680, - "name": "Escape Dead Island", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 2681, - "name": "Failman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2682, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2683, - "name": "Duke Nukem Forever", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2684, - "name": "Draw a Stickman: Episode 1", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2685, - "name": "F.E.A.R. 2: Reborn", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2686, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2687, - "name": "Firewatch", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2688, - "name": "FarSky", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 2689, - "name": "Dead Space 3: Awakened (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2690, - "name": "GUN", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2691, - "name": "DeathSpank: Thongs of Virtue", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 2692, - "name": "Evil Defenders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2693, - "name": "Fallout 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2694, - "name": "Dungeon Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2695, - "name": "Draw a Stickman: Episode 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2696, - "name": "First Winter", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2697, - "name": "F.E.A.R. 3", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Кооператив", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2698, - "name": "Final Fantasy III (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2699, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2700, - "name": "Gemini Rue", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2701, - "name": "Diablo III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2702, - "name": "Evoland", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2703, - "name": "Five Minutes", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2704, - "name": "Dying Light", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2705, - "name": "Dead by Daylight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2706, - "name": "F.E.A.R. Extraction Point", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2707, - "name": "DreadOut", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 2708, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2709, - "name": "Deathtrap", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2710, - "name": "Geometry Dash", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2711, - "name": "Final Fantasy IV (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2712, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2713, - "name": "Five Nights At Freddy's", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2714, - "name": "F.E.A.R. Perseus Mandate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2715, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2716, - "name": "Evoland 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2717, - "name": "Dying Light: The Following (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2718, - "name": "Ghost of a Tale", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2719, - "name": "Dropsy", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2720, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2721, - "name": "Deadlight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2722, - "name": "Five Nights At Freddy's 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2723, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2724, - "name": "Fable (2004)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2725, - "name": "Eyes The Horror Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2726, - "name": "Demigod", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 2727, - "name": "Goat Simulator", - "site": "igromania_ru", - "genres": [ - "Прочее", - "Симулятор" - ] - }, - { - "id": 2728, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2729, - "name": "Five Nights At Freddy's 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2730, - "name": "Final Fantasy X HD Remaster", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2731, - "name": "Dude, Stop", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 2732, - "name": "Dig or Die", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2733, - "name": "Emily Wants To Play", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Экшены", - "Казуальные", - "Головоломки", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 2734, - "name": "Fable III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2735, - "name": "Deadpool", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2736, - "name": "F.E.A.R.", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2737, - "name": "Grand Theft Auto III", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2738, - "name": "Five Nights At Freddy's 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2739, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2740, - "name": "Escape Dead Island", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2741, - "name": "Fallout 4", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2742, - "name": "Duke Nukem Forever", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2743, - "name": "Descenders", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Action", - "Racing" - ] - }, - { - "id": 2744, - "name": "Fahrenheit", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 2745, - "name": "Disciples II: Dark Prophecy", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 2746, - "name": "Grand Theft Auto: San Andreas", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2747, - "name": "Five Nights at Freddy's 3D", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2748, - "name": "F.E.A.R. 2: Project Origin", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2749, - "name": "Final Fantasy XIII", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 2750, - "name": "DeathSpank", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2751, - "name": "Failman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2752, - "name": "Dungeon Nightmares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2753, - "name": "Grand Theft Auto: Vice City", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2754, - "name": "Five Nights in Anime v3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2755, - "name": "Fallout: New Vegas", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2756, - "name": "Disciples III: Renaissance", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 2757, - "name": "Evil Defenders", - "site": "ag_ru", - "genres": [ - "Приключения", - "Казуальные", - "Инди", - "Стратегии" - ] - }, - { - "id": 2758, - "name": "F.E.A.R. 2: Reborn", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2759, - "name": "Detention", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 2760, - "name": "Final Fantasy XIII-2", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 2761, - "name": "Family Trouble", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2762, - "name": "Fallout 3", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Постапокалипсис", - "Ролевая" - ] - }, - { - "id": 2763, - "name": "Fran Bow", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2764, - "name": "Grandpa", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2765, - "name": "DeathSpank: Thongs of Virtue", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2766, - "name": "Dying Light", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2767, - "name": "Evoland", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 2768, - "name": "F.E.A.R. 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2769, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2770, - "name": "Front Mission Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2771, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2772, - "name": "Far Cry", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 2773, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2774, - "name": "Grey", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2775, - "name": "Dishonored", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2776, - "name": "Dying Light: The Following (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2777, - "name": "GUN", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2778, - "name": "Evoland 2", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Аркадные", - "Инди", - "Экшены", - "Файтинги" - ] - }, - { - "id": 2779, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2780, - "name": "F.E.A.R. Extraction Point", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2781, - "name": "Deathtrap", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2782, - "name": "Grim Fandango Remastered", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2783, - "name": "Far Cry 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 2784, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2785, - "name": "Gemini Rue", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2786, - "name": "Devil May Cry 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2787, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2788, - "name": "Emily Wants To Play", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2789, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2790, - "name": "Eyes The Horror Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2791, - "name": "F.E.A.R. Perseus Mandate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2792, - "name": "Grow Home", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2793, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2794, - "name": "Geometry Dash", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2795, - "name": "Demigod", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2796, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2797, - "name": "Final Fantasy XV", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "action" - ] - }, - { - "id": 2798, - "name": "Ghost of a Tale", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2799, - "name": "Escape Dead Island", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2800, - "name": "Grow Up", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2801, - "name": "Fable (2004)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2802, - "name": "Far Cry 3: Blood Dragon", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2803, - "name": "Devil May Cry 5", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2804, - "name": "F.E.A.R.", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2805, - "name": "Goat Simulator", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2806, - "name": "Descenders", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 2807, - "name": "Evil Defenders", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 2808, - "name": "Guns, Gore & Cannoli", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 2809, - "name": "FarSky", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2810, - "name": "Distraint", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2811, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2812, - "name": "Fable III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2813, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2814, - "name": "F.E.A.R. 2: Project Origin", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2815, - "name": "Diablo II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2816, - "name": "Grand Theft Auto III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2817, - "name": "Final Fantasy III (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2818, - "name": "Guns, Gore & Cannoli 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2819, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2820, - "name": "Fahrenheit", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2821, - "name": "Evoland", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2822, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2823, - "name": "Grand Theft Auto: San Andreas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2824, - "name": "Final Fantasy IV (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2825, - "name": "Detention", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2826, - "name": "F.E.A.R. 2: Reborn", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2827, - "name": "Half-Life", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2828, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2829, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2830, - "name": "Grand Theft Auto: Vice City", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2831, - "name": "Failman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2832, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2833, - "name": "Evoland 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2834, - "name": "Fallout 4", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Постапокалипсис", - "Ролевая", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 2835, - "name": "Half-Life 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2836, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2837, - "name": "Disturbed", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2838, - "name": "Diablo III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2839, - "name": "F.E.A.R. 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2840, - "name": "Final Fantasy X HD Remaster", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2841, - "name": "Grandpa", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2842, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2843, - "name": "Fallout 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2844, - "name": "Eyes The Horror Game", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2845, - "name": "Half-Life 2: Episode One", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2846, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2847, - "name": "Grey", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2848, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2849, - "name": "Fallout: New Vegas", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Постапокалипсис", - "Ролевая" - ] - }, - { - "id": 2850, - "name": "DmC: Devil May Cry", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2851, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2852, - "name": "Devil May Cry 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2853, - "name": "Half-Life 2: Episode Two", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2854, - "name": "F.E.A.R. Extraction Point", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2855, - "name": "Grim Fandango Remastered", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2856, - "name": "F.E.A.R.", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2857, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2858, - "name": "Final Fantasy XIII", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2859, - "name": "Family Trouble", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2860, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2861, - "name": "Firewatch", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 2862, - "name": "Grow Home", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2863, - "name": "Half-Life: Blue Shift", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2864, - "name": "F.E.A.R. Perseus Mandate", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2865, - "name": "Dig or Die", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 2866, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2867, - "name": "F.E.A.R. 2: Project Origin", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2868, - "name": "Final Fantasy XIII-2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2869, - "name": "Devil May Cry 5", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2870, - "name": "Far Cry", - "site": "playground_ru", - "genres": [ - "Песочница", - "Шутер", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 2871, - "name": "Grow Up", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2872, - "name": "First Winter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2873, - "name": "Half-Life: Opposing Force", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2874, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2875, - "name": "Fable (2004)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2876, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2877, - "name": "F.E.A.R. 2: Reborn", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2878, - "name": "Disciples II: Dark Prophecy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2879, - "name": "Doki Doki Literature Club", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2880, - "name": "Guns, Gore & Cannoli", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2881, - "name": "Five Minutes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2882, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2883, - "name": "Half-Life: Paranoia", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2884, - "name": "Diablo II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2885, - "name": "Far Cry 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Песочница", - "От первого лица" - ] - }, - { - "id": 2886, - "name": "Guns, Gore & Cannoli 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2887, - "name": "Fable III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2888, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2889, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2890, - "name": "Disciples III: Renaissance", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2891, - "name": "F.E.A.R. 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2892, - "name": "Don't Knock Twice", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 2893, - "name": "Five Nights At Freddy's", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2894, - "name": "Half-Life: The Xeno Project", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2895, - "name": "Half-Life", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2896, - "name": "Far Cry 3: Blood Dragon", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Песочница" - ] - }, - { - "id": 2897, - "name": "Fahrenheit", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2898, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2899, - "name": "F.E.A.R. Extraction Point", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2900, - "name": "Five Nights At Freddy's 2", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2901, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2902, - "name": "Final Fantasy XV", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2903, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2904, - "name": "Half-Life 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2905, - "name": "Doom 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2906, - "name": "FarSky", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2907, - "name": "Dishonored", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2908, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2909, - "name": "Halo: Combat Evolved", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2910, - "name": "Half-Life 2: Episode One", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2911, - "name": "Five Nights At Freddy's 3", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2912, - "name": "Failman", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2913, - "name": "F.E.A.R. Perseus Mandate", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2914, - "name": "Final Fantasy III (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2915, - "name": "Fallout 4", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 2916, - "name": "Double Dragon Neon", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 2917, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2918, - "name": "Half-Life 2: Episode Two", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2919, - "name": "Diablo III", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2920, - "name": "Hand of Fate", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2921, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2922, - "name": "Five Nights At Freddy's 4", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2923, - "name": "Final Fantasy IV (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2924, - "name": "Fallout 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые" - ] - }, - { - "id": 2925, - "name": "Fable (2004)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2926, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2927, - "name": "Fallout: New Vegas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2928, - "name": "Down Stairs", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2929, - "name": "Half-Life: Blue Shift", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2930, - "name": "Hand of Fate 2", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2931, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2932, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2933, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2934, - "name": "Five Nights at Freddy's 3D", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2935, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2936, - "name": "Half-Life: Opposing Force", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2937, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2938, - "name": "Fable III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2939, - "name": "Family Trouble", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2940, - "name": "Happy Room", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 2941, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2942, - "name": "Final Fantasy X HD Remaster", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2943, - "name": "Half-Life: Paranoia", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2944, - "name": "Five Nights in Anime v3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2945, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2946, - "name": "Distraint", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2947, - "name": "Happy Wheels", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2948, - "name": "Fahrenheit", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2949, - "name": "Dig or Die", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2950, - "name": "Far Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2951, - "name": "Half-Life: The Xeno Project", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2952, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2953, - "name": "Fran Bow", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "квесты" - ] - }, - { - "id": 2954, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 2955, - "name": "Hard Reset", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2956, - "name": "Downfall", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2957, - "name": "Firewatch", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2958, - "name": "Failman", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2959, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2960, - "name": "Disciples II: Dark Prophecy", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2961, - "name": "Final Fantasy XIII", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2962, - "name": "Far Cry 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2963, - "name": "Front Mission Evolved", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2964, - "name": "Disturbed", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Free to Play", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 2965, - "name": "First Winter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2966, - "name": "Hard Reset: Exile (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2967, - "name": "Halo: Combat Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2968, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2969, - "name": "Final Fantasy XIII-2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2970, - "name": "Fallout 3", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 2971, - "name": "Disciples III: Renaissance", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2972, - "name": "Five Minutes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2973, - "name": "Far Cry 3: Blood Dragon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2974, - "name": "Harts", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2975, - "name": "Hand of Fate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2976, - "name": "GUN", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2977, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2978, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2979, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2980, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 2981, - "name": "Five Nights At Freddy's", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Спортивные игры" - ] - }, - { - "id": 2982, - "name": "DmC: Devil May Cry", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2983, - "name": "Hand of Fate 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2984, - "name": "Hatred", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2985, - "name": "Dishonored", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2986, - "name": "FarSky", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2987, - "name": "Gemini Rue", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2988, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2989, - "name": "Fallout 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2990, - "name": "Happy Room", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2991, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2992, - "name": "Hellblade: Senua's Sacrifice", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 2993, - "name": "Five Nights At Freddy's 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Приключения" - ] - }, - { - "id": 2994, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2995, - "name": "Geometry Dash", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2996, - "name": "Dragon Age II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2997, - "name": "Final Fantasy III (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2998, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2999, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3000, - "name": "Happy Wheels", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3001, - "name": "Cube Escape: The Mill", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3002, - "name": "Hellgate: London", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3003, - "name": "Fallout: New Vegas", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 3004, - "name": "Five Nights At Freddy's 3", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3005, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3006, - "name": "Ghost of a Tale", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3007, - "name": "Final Fantasy XV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3008, - "name": "Hard Reset", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3009, - "name": "DOOM (2016)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3010, - "name": "Final Fantasy IV (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3011, - "name": "Doki Doki Literature Club", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Casual", - "Indie" - ] - }, - { - "id": 3012, - "name": "Her Story", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3013, - "name": "Dragon Age: Inquisition", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Action", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 3014, - "name": "Family Trouble", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3015, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3016, - "name": "Five Nights At Freddy's 4", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3017, - "name": "Dark Deception", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3018, - "name": "Hard Reset: Exile (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3019, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3020, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3021, - "name": "Goat Simulator", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 3022, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3023, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3024, - "name": "Hero Academy", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия" - ] - }, - { - "id": 3025, - "name": "Far Cry", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3026, - "name": "Dark Messiah of Might and Magic", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3027, - "name": "Five Nights at Freddy's 3D", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3028, - "name": "Harts", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3029, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3030, - "name": "Don't Knock Twice", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 3031, - "name": "Grand Theft Auto III", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3032, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3033, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3034, - "name": "Distraint", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3035, - "name": "Dark Sector", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3036, - "name": "Heroes of Might and Magic III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3037, - "name": "Five Nights in Anime v3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3038, - "name": "Hatred", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3039, - "name": "Final Fantasy X HD Remaster", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3040, - "name": "Far Cry 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3041, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3042, - "name": "Grand Theft Auto: San Andreas", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "приключения", - "action" - ] - }, - { - "id": 3043, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3044, - "name": "Fran Bow", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3045, - "name": "Dark Souls II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3046, - "name": "Heroes of Might and Magic IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3047, - "name": "Fallout 4", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3048, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3049, - "name": "Disturbed", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3050, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3051, - "name": "Far Cry 3: Blood Dragon", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 3052, - "name": "Doom 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3053, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3054, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3055, - "name": "Hellgate: London", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3056, - "name": "Heroes of Might and Magic V", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3057, - "name": "Front Mission Evolved", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер", - "Битвы машин" - ] - }, - { - "id": 3058, - "name": "Grand Theft Auto: Vice City", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "приключения", - "action" - ] - }, - { - "id": 3059, - "name": "Dragon Age: Origins", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3060, - "name": "Fallout: New Vegas", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3061, - "name": "FarSky", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 3062, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3063, - "name": "Final Fantasy XIII", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3064, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3065, - "name": "DmC: Devil May Cry", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3066, - "name": "Her Story", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3067, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3068, - "name": "GUN", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3069, - "name": "Grandpa", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3070, - "name": "Dragon Age: Origins - Awakening", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3071, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3072, - "name": "Double Dragon Neon", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 3073, - "name": "Family Trouble", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3074, - "name": "Final Fantasy III (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3075, - "name": "Hero Academy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3076, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3077, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3078, - "name": "Firewatch", - "site": "playground_ru", - "genres": [ - "Логические", - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 3079, - "name": "Final Fantasy XIII-2", - "site": "iwantgames_ru", - "genres": [ - "РПГ" - ] - }, - { - "id": 3080, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3081, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3082, - "name": "Gemini Rue", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3083, - "name": "Grey", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3084, - "name": "Heroes of Might and Magic III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3085, - "name": "Down Stairs", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3086, - "name": "Homefront", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3087, - "name": "Final Fantasy IV (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3088, - "name": "First Winter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3089, - "name": "Far Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3090, - "name": "Dark Souls III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3091, - "name": "Geometry Dash", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3092, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3093, - "name": "Doki Doki Literature Club", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3094, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3095, - "name": "Heroes of Might and Magic IV", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3096, - "name": "Grim Fandango Remastered", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 3097, - "name": "Hotel Remorse", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3098, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3099, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3100, - "name": "Five Minutes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3101, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3102, - "name": "Far Cry 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3103, - "name": "Heroes of Might and Magic V", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3104, - "name": "Ghost of a Tale", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3105, - "name": "Grow Home", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "платформеры" - ] - }, - { - "id": 3106, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3107, - "name": "Downfall", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3108, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3109, - "name": "Hotline Miami", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 3110, - "name": "Final Fantasy X HD Remaster", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3111, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3112, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3113, - "name": "Five Nights At Freddy's", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Симулятор", - "От первого лица" - ] - }, - { - "id": 3114, - "name": "Don't Knock Twice", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3115, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 3116, - "name": "Grow Up", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "платформеры" - ] - }, - { - "id": 3117, - "name": "Hotline Miami 2: Wrong Number", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3118, - "name": "Far Cry 3: Blood Dragon", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3119, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3120, - "name": "Goat Simulator", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3121, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3122, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3123, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3124, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3125, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Indie" - ] - }, - { - "id": 3126, - "name": "Five Nights At Freddy's 2", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "От первого лица", - "Инди" - ] - }, - { - "id": 3127, - "name": "Guns, Gore & Cannoli", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "платформеры" - ] - }, - { - "id": 3128, - "name": "House Flipper", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3129, - "name": "Homefront", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3130, - "name": "Draw a Stickman: Episode 1", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3131, - "name": "Dark Souls: Remastered", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Ремастер" - ] - }, - { - "id": 3132, - "name": "Grand Theft Auto III", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3133, - "name": "Final Fantasy XV", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 3134, - "name": "FarSky", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3135, - "name": "Doom 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3136, - "name": "Final Fantasy XIII", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3137, - "name": "Dragon Age II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3138, - "name": "Hotel Remorse", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3139, - "name": "Five Nights At Freddy's 3", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "От первого лица", - "Инди" - ] - }, - { - "id": 3140, - "name": "Guns, Gore & Cannoli 2", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "платформеры" - ] - }, - { - "id": 3141, - "name": "I Am Alive", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3142, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3143, - "name": "Draw a Stickman: Episode 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3144, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3145, - "name": "Grand Theft Auto: San Andreas", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3146, - "name": "Final Fantasy III (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3147, - "name": "Final Fantasy XIII-2", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3148, - "name": "Hotline Miami", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3149, - "name": "Darksiders", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3150, - "name": "I am Setsuna", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3151, - "name": "Dragon Age: Inquisition", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3152, - "name": "Half-Life", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3153, - "name": "Five Nights At Freddy's 4", - "site": "playground_ru", - "genres": [ - "Ужасы", - "От первого лица", - "Инди" - ] - }, - { - "id": 3154, - "name": "Double Dragon Neon", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3155, - "name": "Grand Theft Auto: Vice City", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3156, - "name": "DreadOut", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3157, - "name": "Darksiders II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3158, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3159, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3160, - "name": "Final Fantasy IV (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3161, - "name": "I hate this game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3162, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3163, - "name": "Half-Life 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3164, - "name": "Grandpa", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3165, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3166, - "name": "Five Nights at Freddy's 3D", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3167, - "name": "Darksiders III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3168, - "name": "Down Stairs", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3169, - "name": "House Flipper", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3170, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3171, - "name": "Grey", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3172, - "name": "INSIDE", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 3173, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3174, - "name": "Daylight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 3175, - "name": "Half-Life 2: Episode One", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3176, - "name": "Five Nights in Anime v3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3177, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3178, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3179, - "name": "I Am Alive", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3180, - "name": "Dropsy", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3181, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3182, - "name": "Downfall", - "site": "mobygames_com", - "genres": [ - "Puzzle", - "Strategy / tactics" - ] - }, - { - "id": 3183, - "name": "Indivisible", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3184, - "name": "Grim Fandango Remastered", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3185, - "name": "I am Setsuna", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3186, - "name": "Dead Island", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3187, - "name": "Half-Life 2: Episode Two", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3188, - "name": "Fran Bow", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 3189, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3190, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3191, - "name": "Final Fantasy X HD Remaster", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3192, - "name": "Injustice: Gods Among Us", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3193, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3194, - "name": "Dead Island: Riptide", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3195, - "name": "I hate this game", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3196, - "name": "Half-Life: Blue Shift", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3197, - "name": "Grow Home", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Приключения" - ] - }, - { - "id": 3198, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3199, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3200, - "name": "Dude, Stop", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 3201, - "name": "Front Mission Evolved", - "site": "playground_ru", - "genres": [ - "Экшен", - "Симулятор" - ] - }, - { - "id": 3202, - "name": "Final Fantasy XV", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3203, - "name": "Dead Island: Ryder White", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3204, - "name": "INSIDE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3205, - "name": "Into the Breach", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Ролевая игра" - ] - }, - { - "id": 3206, - "name": "Firewatch", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3207, - "name": "Half-Life: Opposing Force", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3208, - "name": "Dragon Age: Origins", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3209, - "name": "Grow Up", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Платформер" - ] - }, - { - "id": 3210, - "name": "Dead Rising", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3211, - "name": "Indivisible", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3212, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3213, - "name": "Final Fantasy XIII", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3214, - "name": "Jade Empire", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3215, - "name": "GUN", - "site": "playground_ru", - "genres": [ - "Экшен", - "Вестерн", - "От третьего лица", - "От первого лица" - ] - }, - { - "id": 3216, - "name": "Dragon Age II", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3217, - "name": "Guns, Gore & Cannoli", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3218, - "name": "Duke Nukem Forever", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3219, - "name": "First Winter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3220, - "name": "Half-Life: Paranoia", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3221, - "name": "Dead Space", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3222, - "name": "Injustice: Gods Among Us", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3223, - "name": "Guns, Gore & Cannoli 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3224, - "name": "Jotun", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3225, - "name": "Dragon Age: Origins - Awakening", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3226, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3227, - "name": "Final Fantasy XIII-2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3228, - "name": "Gemini Rue", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Квест" - ] - }, - { - "id": 3229, - "name": "Dead Space 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3230, - "name": "Into the Breach", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3231, - "name": "Five Minutes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3232, - "name": "Half-Life: The Xeno Project", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3233, - "name": "Just Cause", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3234, - "name": "Dragon Age: Inquisition", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3235, - "name": "Half-Life", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 3236, - "name": "Dead Space 3", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3237, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 3238, - "name": "Jade Empire", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3239, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3240, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3241, - "name": "Geometry Dash", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 3242, - "name": "Dungeon Nightmares", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3243, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3244, - "name": "Five Nights At Freddy's", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3245, - "name": "Just Cause 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3246, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3247, - "name": "Half-Life 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 3248, - "name": "Jotun", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3249, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3250, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 3251, - "name": "Halo: Combat Evolved", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3252, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3253, - "name": "Ghost of a Tale", - "site": "playground_ru", - "genres": [ - "Инди", - "От третьего лица", - "Адвенчура", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 3254, - "name": "Kane & Lynch 2: Dog Days", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3255, - "name": "Five Nights At Freddy's 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3256, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3257, - "name": "Dying Light", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3258, - "name": "Dead by Daylight", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3259, - "name": "Just Cause", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3260, - "name": "Half-Life 2: Episode One", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3261, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 3262, - "name": "Hand of Fate", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "карточные", - "action" - ] - }, - { - "id": 3263, - "name": "Deadlight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 3264, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3265, - "name": "Dying Light: The Following (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3266, - "name": "Five Nights At Freddy's 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3267, - "name": "Killer is Dead: Nightmare Edition", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3268, - "name": "Goat Simulator", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Песочница", - "Экшен", - "Открытый мир" - ] - }, - { - "id": 3269, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3270, - "name": "Just Cause 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3271, - "name": "Half-Life 2: Episode Two", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3272, - "name": "Deadpool", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3273, - "name": "Firewatch", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы" - ] - }, - { - "id": 3274, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3275, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3276, - "name": "Hand of Fate 2", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "карточные", - "action" - ] - }, - { - "id": 3277, - "name": "King's Bounty: Armored Princess", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3278, - "name": "Grand Theft Auto III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3279, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3280, - "name": "Five Nights At Freddy's 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3281, - "name": "Emily Wants To Play", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3282, - "name": "Final Fantasy XV", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3283, - "name": "DeathSpank", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3284, - "name": "Half-Life: Blue Shift", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3285, - "name": "First Winter", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3286, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3287, - "name": "King's Bounty: Crossworlds", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3288, - "name": "Happy Room", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 3289, - "name": "DeathSpank: Thongs of Virtue", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение" - ] - }, - { - "id": 3290, - "name": "Grand Theft Auto: San Andreas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3291, - "name": "Five Nights at Freddy's 3D", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3292, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3293, - "name": "King's Bounty: Armored Princess", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3294, - "name": "Escape Dead Island", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3295, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3296, - "name": "Half-Life: Opposing Force", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3297, - "name": "King's Bounty: Dark Side", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3298, - "name": "Dragon's Dogma: Dark Arisen", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 3299, - "name": "Deathtrap", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 3300, - "name": "Five Minutes", - "site": "ag_ru", - "genres": [ - "Головоломки" - ] - }, - { - "id": 3301, - "name": "Happy Wheels", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3302, - "name": "King's Bounty: Crossworlds", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3303, - "name": "Five Nights in Anime v3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3304, - "name": "Grand Theft Auto: Vice City", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3305, - "name": "Half-Life: Paranoia", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3306, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3307, - "name": "Demigod", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Стратегия" - ] - }, - { - "id": 3308, - "name": "King's Bounty: The Legend", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3309, - "name": "Five Nights At Freddy's", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 3310, - "name": "Hard Reset", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3311, - "name": "King's Bounty: Dark Side", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3312, - "name": "Draw a Stickman: Episode 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3313, - "name": "Half-Life: The Xeno Project", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3314, - "name": "Descenders", - "site": "gameguru_ru", - "genres": [ - "Спорт", - "Велоспорт" - ] - }, - { - "id": 3315, - "name": "Grandpa", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3316, - "name": "King's Bounty: Warriors of the North", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3317, - "name": "Fran Bow", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3318, - "name": "Evil Defenders", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 3319, - "name": "Dragon Age: Origins", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3320, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3321, - "name": "King's Bounty: The Legend", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3322, - "name": "Detention", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 3323, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3324, - "name": "Hard Reset: Exile (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3325, - "name": "Grey", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3326, - "name": "Five Nights At Freddy's 2", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 3327, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3328, - "name": "Draw a Stickman: Episode 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3329, - "name": "Front Mission Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3330, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3331, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3332, - "name": "King's Bounty: Warriors of the North", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3333, - "name": "Grim Fandango Remastered", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3334, - "name": "Harts", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3335, - "name": "Halo: Combat Evolved", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3336, - "name": "Dragon Age: Origins - Awakening", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3337, - "name": "Kingdom Rush", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия" - ] - }, - { - "id": 3338, - "name": "Five Nights At Freddy's 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3339, - "name": "Devil May Cry 4", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3340, - "name": "Evoland", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3341, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3342, - "name": "GUN", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3343, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3344, - "name": "Hatred", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3345, - "name": "Hand of Fate", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3346, - "name": "DreadOut", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3347, - "name": "Devil May Cry 5", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 3348, - "name": "Grow Home", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 3349, - "name": "Kingdom: Classic", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Боевик" - ] - }, - { - "id": 3350, - "name": "Kingdom Rush", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3351, - "name": "Five Nights At Freddy's 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 3352, - "name": "Evoland 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3353, - "name": "Gemini Rue", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3354, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3355, - "name": "Firewatch", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3356, - "name": "Diablo II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3357, - "name": "Hand of Fate 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Карточные игры", - "Ролевые игры" - ] - }, - { - "id": 3358, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3359, - "name": "Kingdom: Classic", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3360, - "name": "Kingdoms of Amalur: Reckoning", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3361, - "name": "Grow Up", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Адвенчура" - ] - }, - { - "id": 3362, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3363, - "name": "Happy Room", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3364, - "name": "Dropsy", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3365, - "name": "Geometry Dash", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3366, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3367, - "name": "Five Nights at Freddy's 3D", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3368, - "name": "Knock-Knock", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3369, - "name": "First Winter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3370, - "name": "Hellgate: London", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 3371, - "name": "Diablo III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3372, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3373, - "name": "Happy Wheels", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3374, - "name": "Guns, Gore & Cannoli", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Шутер", - "Платформер" - ] - }, - { - "id": 3375, - "name": "Eyes The Horror Game", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3376, - "name": "Knock-Knock", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3377, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3378, - "name": "Five Nights in Anime v3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3379, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3380, - "name": "Ghost of a Tale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3381, - "name": "Five Minutes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3382, - "name": "Her Story", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 3383, - "name": "Dude, Stop", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 3384, - "name": "Hard Reset", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3385, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3386, - "name": "Guns, Gore & Cannoli 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Инди", - "Кооператив" - ] - }, - { - "id": 3387, - "name": "Dig or Die", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер", - "Инди" - ] - }, - { - "id": 3388, - "name": "Lara Croft and the Guardian of Light", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3389, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3390, - "name": "Goat Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3391, - "name": "Hero Academy", - "site": "gamer_info_com", - "genres": [ - "тактика", - "TBS" - ] - }, - { - "id": 3392, - "name": "Fran Bow", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 3393, - "name": "Five Nights At Freddy's", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3394, - "name": "F.E.A.R.", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3395, - "name": "Hard Reset: Exile (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3396, - "name": "Lara Croft and the Guardian of Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3397, - "name": "Disciples II: Dark Prophecy", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 3398, - "name": "Lara Croft and the Temple of Osiris", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Приключение" - ] - }, - { - "id": 3399, - "name": "Half-Life", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 3400, - "name": "Duke Nukem Forever", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3401, - "name": "Harts", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3402, - "name": "Heroes of Might and Magic III", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3403, - "name": "Disciples III: Renaissance", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3404, - "name": "Grand Theft Auto III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3405, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3406, - "name": "Five Nights At Freddy's 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3407, - "name": "Front Mission Evolved", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3408, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3409, - "name": "Late Shift", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3410, - "name": "Half-Life 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 3411, - "name": "Dishonored", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3412, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3413, - "name": "Late Shift", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3414, - "name": "Dungeon Nightmares", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3415, - "name": "Heroes of Might and Magic IV", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3416, - "name": "Hatred", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3417, - "name": "Grand Theft Auto: San Andreas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3418, - "name": "GUN", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 3419, - "name": "Layers of Fear", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3420, - "name": "Five Nights At Freddy's 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3421, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3422, - "name": "F.E.A.R. 2: Reborn", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3423, - "name": "Dragon's Dogma: Dark Arisen", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 3424, - "name": "Layers of Fear", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3425, - "name": "Heroes of Might and Magic V", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3426, - "name": "Half-Life 2: Episode One", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3427, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 3428, - "name": "Grand Theft Auto: Vice City", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3429, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3430, - "name": "Gemini Rue", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3431, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3432, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3433, - "name": "Five Nights At Freddy's 4", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3434, - "name": "Dying Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3435, - "name": "Distraint", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3436, - "name": "F.E.A.R. 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3437, - "name": "Draw a Stickman: Episode 1", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3438, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3439, - "name": "Hellgate: London", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "ММОРПГ/Многопользоватеские онлайновые", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3440, - "name": "Half-Life 2: Episode Two", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3441, - "name": "Left 4 Dead", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3442, - "name": "Left 4 Dead", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3443, - "name": "Geometry Dash", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Аркадные" - ] - }, - { - "id": 3444, - "name": "Grandpa", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3445, - "name": "Disturbed", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3446, - "name": "Dying Light: The Following (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3447, - "name": "Five Nights at Freddy's 3D", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3448, - "name": "F.E.A.R. Extraction Point", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3449, - "name": "Left 4 Dead 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3450, - "name": "Draw a Stickman: Episode 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3451, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3452, - "name": "Left 4 Dead 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3453, - "name": "Her Story", - "site": "gamebomb_ru", - "genres": [ - "Симулятор", - "Головоломки/Пазл" - ] - }, - { - "id": 3454, - "name": "Ghost of a Tale", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 3455, - "name": "Half-Life: Blue Shift", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3456, - "name": "DmC: Devil May Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3457, - "name": "Grey", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3458, - "name": "Legend Hand of God", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3459, - "name": "Homefront", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3460, - "name": "Five Nights in Anime v3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3461, - "name": "Legend Hand of God", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3462, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3463, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 3464, - "name": "Hero Academy", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3465, - "name": "DreadOut", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3466, - "name": "Goat Simulator", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3467, - "name": "Grim Fandango Remastered", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 3468, - "name": "Emily Wants To Play", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 3469, - "name": "Legend of Grimrock", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3470, - "name": "Half-Life: Opposing Force", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3471, - "name": "Doki Doki Literature Club", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3472, - "name": "Fable (2004)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3473, - "name": "Legend of Grimrock", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3474, - "name": "Hotel Remorse", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3475, - "name": "Heroes of Might and Magic III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3476, - "name": "Fran Bow", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3477, - "name": "Legendary", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3478, - "name": "Don't Knock Twice", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3479, - "name": "Half-Life: Paranoia", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3480, - "name": "Grow Home", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3481, - "name": "Dropsy", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3482, - "name": "Grand Theft Auto III", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3483, - "name": "Life After Us: Shipwrecked", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3484, - "name": "Hotline Miami", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 3485, - "name": "Heroes of Might and Magic IV", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3486, - "name": "Legendary", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3487, - "name": "Front Mission Evolved", - "site": "stopgame_ru", - "genres": [ - "simulator", - "action" - ] - }, - { - "id": 3488, - "name": "Escape Dead Island", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3489, - "name": "Fable III", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3490, - "name": "Doom 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3491, - "name": "Half-Life: The Xeno Project", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3492, - "name": "Grow Up", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3493, - "name": "Life Goes On: Done to Death", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3494, - "name": "Dude, Stop", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3495, - "name": "Grand Theft Auto: San Andreas", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3496, - "name": "Double Dragon Neon", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 3497, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 3498, - "name": "Life After Us: Shipwrecked", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3499, - "name": "Heroes of Might and Magic V", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3500, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3501, - "name": "GUN", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3502, - "name": "Life Is Strange", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3503, - "name": "Down Stairs", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3504, - "name": "Life Goes On: Done to Death", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 3505, - "name": "Guns, Gore & Cannoli", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3506, - "name": "Evil Defenders", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Strategy", - "Indie" - ] - }, - { - "id": 3507, - "name": "Fahrenheit", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3508, - "name": "House Flipper", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3509, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3510, - "name": "Grand Theft Auto: Vice City", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3511, - "name": "Limbo", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3512, - "name": "Downfall", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 3513, - "name": "Duke Nukem Forever", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3514, - "name": "Halo: Combat Evolved", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 3515, - "name": "Life Is Strange", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3516, - "name": "Gemini Rue", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3517, - "name": "I Am Alive", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3518, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3519, - "name": "Guns, Gore & Cannoli 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3520, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 3521, - "name": "Little Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3522, - "name": "Grandpa", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3523, - "name": "Dungeon Nightmares", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3524, - "name": "Failman", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3525, - "name": "Evoland", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 3526, - "name": "Limbo", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 3527, - "name": "Hand of Fate", - "site": "playground_ru", - "genres": [ - "Экшен", - "Карточная", - "Мультиплеер", - "Ролевая" - ] - }, - { - "id": 3528, - "name": "Geometry Dash", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3529, - "name": "I am Setsuna", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 3530, - "name": "Homefront", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3531, - "name": "Dragon Age II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3532, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3533, - "name": "Half-Life", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3534, - "name": "Grey", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3535, - "name": "Little Nightmares", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3536, - "name": "Dying Light", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3537, - "name": "Hotel Remorse", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3538, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3539, - "name": "Dragon Age: Inquisition", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 3540, - "name": "Fallout 3", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3541, - "name": "Hand of Fate 2", - "site": "playground_ru", - "genres": [ - "Инди", - "Карточная", - "Адвенчура", - "Стратегия" - ] - }, - { - "id": 3542, - "name": "Ghost of a Tale", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3543, - "name": "I hate this game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3544, - "name": "Half-Life 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3545, - "name": "Evoland 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 3546, - "name": "Grim Fandango Remastered", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3547, - "name": "Little Nightmares - The Depths (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3548, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 3549, - "name": "Loki: Heroes of Mythology", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3550, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3551, - "name": "Hotline Miami", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 3552, - "name": "Happy Room", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3553, - "name": "INSIDE", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 3554, - "name": "Goat Simulator", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3555, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3556, - "name": "Half-Life 2: Episode One", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3557, - "name": "Dying Light: The Following (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3558, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3559, - "name": "Lost Planet: Colonies", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3560, - "name": "Eyes The Horror Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3561, - "name": "Grow Home", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 3562, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3563, - "name": "Happy Wheels", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3564, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 3565, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Аддон" - ] - }, - { - "id": 3566, - "name": "Indivisible", - "site": "gamer_info_com", - "genres": [ - "RPG", - "платформеры" - ] - }, - { - "id": 3567, - "name": "Lost: Via Domus", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3568, - "name": "Grand Theft Auto III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3569, - "name": "Loki: Heroes of Mythology", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3570, - "name": "Half-Life 2: Episode Two", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3571, - "name": "Grow Up", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 3572, - "name": "Emily Wants To Play", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3573, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3574, - "name": "Dragon Age: Origins", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3575, - "name": "Hard Reset", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "Киберпанк" - ] - }, - { - "id": 3576, - "name": "Love Is Strange (DEMO)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3577, - "name": "House Flipper", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3578, - "name": "Injustice: Gods Among Us", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 3579, - "name": "F.E.A.R.", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3580, - "name": "Lost Planet: Colonies", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3581, - "name": "Dragon Age: Origins - Awakening", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3582, - "name": "Grand Theft Auto: San Andreas", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 3583, - "name": "Half-Life: Blue Shift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3584, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3585, - "name": "Guns, Gore & Cannoli", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 3586, - "name": "METAL SLUG", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3587, - "name": "Hard Reset: Exile (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3588, - "name": "Escape Dead Island", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3589, - "name": "Into the Breach", - "site": "gamer_info_com", - "genres": [ - "инди", - "TBS" - ] - }, - { - "id": 3590, - "name": "Lost: Via Domus", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3591, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3592, - "name": "F.E.A.R. 2: Project Origin", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3593, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3594, - "name": "METAL SLUG 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3595, - "name": "Half-Life: Opposing Force", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3596, - "name": "Guns, Gore & Cannoli 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3597, - "name": "Grand Theft Auto: Vice City", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 3598, - "name": "Harts", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3599, - "name": "I Am Alive", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 3600, - "name": "Love Is Strange (DEMO)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3601, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3602, - "name": "Jade Empire", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3603, - "name": "Evil Defenders", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 3604, - "name": "MINERVA: Metastasis", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3605, - "name": "F.E.A.R. 2: Reborn", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3606, - "name": "Half-Life", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3607, - "name": "Half-Life: Paranoia", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3608, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3609, - "name": "Grandpa", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3610, - "name": "Fallout 4", - "site": "gamespot_com", - "genres": [ - "VR", - "Role-Playing" - ] - }, - { - "id": 3611, - "name": "I am Setsuna", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3612, - "name": "Hatred", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Вид сверху" - ] - }, - { - "id": 3613, - "name": "METAL SLUG", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 3614, - "name": "MOTHERGUNSHIP", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3615, - "name": "Jotun", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 3616, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3617, - "name": "I hate this game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3618, - "name": "Half-Life 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3619, - "name": "Machinarium", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3620, - "name": "Half-Life: The Xeno Project", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3621, - "name": "METAL SLUG 3", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 3622, - "name": "Evoland", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3623, - "name": "Grey", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3624, - "name": "F.E.A.R. 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3625, - "name": "Just Cause", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3626, - "name": "Hellblade: Senua's Sacrifice", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Фэнтези" - ] - }, - { - "id": 3627, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 3628, - "name": "Fallout: New Vegas", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3629, - "name": "Magibot", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3630, - "name": "INSIDE", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 3631, - "name": "MINERVA: Metastasis", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3632, - "name": "Half-Life 2: Episode One", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3633, - "name": "Evoland 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3634, - "name": "Draw a Stickman: Episode 1", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3635, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3636, - "name": "Just Cause 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3637, - "name": "F.E.A.R. Extraction Point", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3638, - "name": "Family Trouble", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3639, - "name": "Grim Fandango Remastered", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3640, - "name": "Hellgate: London", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "От первого лица", - "Фэнтези" - ] - }, - { - "id": 3641, - "name": "Magicka", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3642, - "name": "MOTHERGUNSHIP", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 3643, - "name": "Draw a Stickman: Episode 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3644, - "name": "Indivisible", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3645, - "name": "Half-Life 2: Episode Two", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3646, - "name": "Halo: Combat Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3647, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3648, - "name": "Magicka 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3649, - "name": "F.E.A.R. Perseus Mandate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3650, - "name": "DreadOut", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 3651, - "name": "Far Cry", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3652, - "name": "Grow Home", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3653, - "name": "Eyes The Horror Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3654, - "name": "Machinarium", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3655, - "name": "Her Story", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Адвенчура", - "Инди" - ] - }, - { - "id": 3656, - "name": "Dropsy", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3657, - "name": "Manual Samuel", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3658, - "name": "Half-Life: Blue Shift", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3659, - "name": "Hand of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3660, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3661, - "name": "Fable (2004)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3662, - "name": "Injustice: Gods Among Us", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 3663, - "name": "Grow Up", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3664, - "name": "Dude, Stop", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3665, - "name": "Magibot", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3666, - "name": "F.E.A.R.", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3667, - "name": "Mario.EXE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3668, - "name": "Hero Academy", - "site": "playground_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3669, - "name": "Far Cry 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3670, - "name": "Half-Life: Opposing Force", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3671, - "name": "King's Bounty: Armored Princess", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3672, - "name": "Hand of Fate 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3673, - "name": "Guns, Gore & Cannoli", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3674, - "name": "Duke Nukem Forever", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3675, - "name": "Into the Breach", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Ролевые игры" - ] - }, - { - "id": 3676, - "name": "Fable III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3677, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3678, - "name": "Magicka", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 3679, - "name": "Heroes of Might and Magic III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3680, - "name": "King's Bounty: Crossworlds", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3681, - "name": "Half-Life: Paranoia", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3682, - "name": "Happy Room", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3683, - "name": "Far Cry 3: Blood Dragon", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3684, - "name": "Guns, Gore & Cannoli 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3685, - "name": "Dungeon Nightmares", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3686, - "name": "Masochisia", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3687, - "name": "Jade Empire", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3688, - "name": "Fahrenheit", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3689, - "name": "Magicka 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3690, - "name": "Heroes of Might and Magic IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3691, - "name": "Dying Light", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер" - ] - }, - { - "id": 3692, - "name": "King's Bounty: Dark Side", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3693, - "name": "Half-Life: The Xeno Project", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3694, - "name": "F.E.A.R. 2: Project Origin", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3695, - "name": "Masters of Anima", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3696, - "name": "Happy Wheels", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3697, - "name": "Half-Life", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3698, - "name": "Manual Samuel", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3699, - "name": "Jotun", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 3700, - "name": "Heroes of Might and Magic V", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3701, - "name": "Dying Light: The Following (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Аддон" - ] - }, - { - "id": 3702, - "name": "Failman", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3703, - "name": "King's Bounty: The Legend", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3704, - "name": "Max Payne", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3705, - "name": "FarSky", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3706, - "name": "Hard Reset", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3707, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3708, - "name": "Mario.EXE", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3709, - "name": "Emily Wants To Play", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 3710, - "name": "Half-Life 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3711, - "name": "Just Cause", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 3712, - "name": "F.E.A.R. 2: Reborn", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3713, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3714, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3715, - "name": "King's Bounty: Warriors of the North", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3716, - "name": "Final Fantasy III (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3717, - "name": "Escape Dead Island", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3718, - "name": "Marvel vs. Capcom: Infinite", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3719, - "name": "Hard Reset: Exile (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3720, - "name": "Fallout 3", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3721, - "name": "Halo: Combat Evolved", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 3722, - "name": "Just Cause 2", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Битвы машин", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3723, - "name": "Half-Life 2: Episode One", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3724, - "name": "F.E.A.R. 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3725, - "name": "Max Payne 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3726, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3727, - "name": "Evil Defenders", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 3728, - "name": "Final Fantasy IV (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3729, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3730, - "name": "Masochisia", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3731, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3732, - "name": "Harts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3733, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3734, - "name": "Max: The Curse Of Brotherhood", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3735, - "name": "Evoland", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3736, - "name": "Hand of Fate", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 3737, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3738, - "name": "Half-Life 2: Episode Two", - "site": "stopgame_ru", - "genres": [ - "logic", - "action" - ] - }, - { - "id": 3739, - "name": "Homefront", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 3740, - "name": "Kingdom Rush", - "site": "gamer_info_com", - "genres": [ - "инди", - "tower defense" - ] - }, - { - "id": 3741, - "name": "Masters of Anima", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Приключение" - ] - }, - { - "id": 3742, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3743, - "name": "Evoland 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3744, - "name": "McPixel", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3745, - "name": "F.E.A.R. Extraction Point", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3746, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3747, - "name": "Hatred", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3748, - "name": "Hand of Fate 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 3749, - "name": "Final Fantasy X HD Remaster", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3750, - "name": "Hotel Remorse", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3751, - "name": "Max Payne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3752, - "name": "Eyes The Horror Game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3753, - "name": "Kingdom: Classic", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3754, - "name": "Half-Life: Blue Shift", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3755, - "name": "Metal Gear Rising: Revengeance", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3756, - "name": "King's Bounty: Armored Princess", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3757, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3758, - "name": "Happy Room", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3759, - "name": "Hellblade: Senua's Sacrifice", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3760, - "name": "F.E.A.R.", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3761, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3762, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3763, - "name": "Metal Wolf Chaos XD", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3764, - "name": "F.E.A.R. Perseus Mandate", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3765, - "name": "Hotline Miami", - "site": "playground_ru", - "genres": [ - "Вид сверху", - "Аркада", - "Ретро", - "Шутер", - "Экшен" - ] - }, - { - "id": 3766, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3767, - "name": "King's Bounty: Crossworlds", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3768, - "name": "Half-Life: Opposing Force", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3769, - "name": "F.E.A.R. 2: Project Origin", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3770, - "name": "Metro 2033", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3771, - "name": "Max Payne 3", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3772, - "name": "Happy Wheels", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки" - ] - }, - { - "id": 3773, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3774, - "name": "Hellgate: London", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3775, - "name": "Fable (2004)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3776, - "name": "Final Fantasy XIII", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3777, - "name": "Knock-Knock", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3778, - "name": "Hotline Miami 2: Wrong Number", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Ретро", - "Вид сверху" - ] - }, - { - "id": 3779, - "name": "King's Bounty: Dark Side", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3780, - "name": "F.E.A.R. 2: Reborn", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3781, - "name": "Metro 2033 Redux", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3782, - "name": "Half-Life: Paranoia", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3783, - "name": "Max: The Curse Of Brotherhood", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 3784, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3785, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3786, - "name": "Her Story", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3787, - "name": "F.E.A.R. 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3788, - "name": "Hard Reset", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3789, - "name": "House Flipper", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Строительство", - "От первого лица", - "Симулятор" - ] - }, - { - "id": 3790, - "name": "Metro Exodus", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3791, - "name": "King's Bounty: The Legend", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3792, - "name": "Final Fantasy XIII-2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3793, - "name": "McPixel", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 3794, - "name": "Fable III", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3795, - "name": "King's Bounty: Warriors of the North", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3796, - "name": "Half-Life: The Xeno Project", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3797, - "name": "Metro Last Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3798, - "name": "Lara Croft and the Guardian of Light", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3799, - "name": "Hero Academy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3800, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3801, - "name": "F.E.A.R. Extraction Point", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3802, - "name": "I Am Alive", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Постапокалипсис", - "Выживание", - "Шутер", - "Экшен", - "Открытый мир", - "Платформер" - ] - }, - { - "id": 3803, - "name": "Metal Gear Rising: Revengeance", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3804, - "name": "Fahrenheit", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3805, - "name": "Hard Reset: Exile (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3806, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3807, - "name": "Metro Last Light Redux", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3808, - "name": "Fallout 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3809, - "name": "F.E.A.R. Perseus Mandate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3810, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3811, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3812, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3813, - "name": "I am Setsuna", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3814, - "name": "Heroes of Might and Magic III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3815, - "name": "Metal Wolf Chaos XD", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3816, - "name": "Kingdom Rush", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3817, - "name": "Failman", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3818, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3819, - "name": "Harts", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3820, - "name": "Fable (2004)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3821, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3822, - "name": "Late Shift", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3823, - "name": "I hate this game", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3824, - "name": "Halo: Combat Evolved", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3825, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3826, - "name": "Metro 2033", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3827, - "name": "Fallout: New Vegas", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3828, - "name": "Kingdom: Classic", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Приключения" - ] - }, - { - "id": 3829, - "name": "Fable III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3830, - "name": "Heroes of Might and Magic IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3831, - "name": "Hatred", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены" - ] - }, - { - "id": 3832, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3833, - "name": "Layers of Fear", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 3834, - "name": "Metro 2033 Redux", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3835, - "name": "Fahrenheit", - "site": "gameguru_ru", - "genres": [ - "Интерактивный фильм", - "Квест" - ] - }, - { - "id": 3836, - "name": "Fallout 3", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3837, - "name": "INSIDE", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди", - "Платформер" - ] - }, - { - "id": 3838, - "name": "Final Fantasy XV", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 3839, - "name": "Hand of Fate", - "site": "stopgame_ru", - "genres": [ - "logic", - "rpg" - ] - }, - { - "id": 3840, - "name": "Family Trouble", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3841, - "name": "Heroes of Might and Magic V", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3842, - "name": "Hellblade: Senua's Sacrifice", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 3843, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3844, - "name": "Failman", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3845, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3846, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3847, - "name": "Metro Exodus", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3848, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3849, - "name": "Indivisible", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Платформер" - ] - }, - { - "id": 3850, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3851, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3852, - "name": "Fallout 3", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3853, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3854, - "name": "Hand of Fate 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3855, - "name": "Hellgate: London", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 3856, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3857, - "name": "Metro Last Light", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3858, - "name": "Left 4 Dead", - "site": "gamer_info_com", - "genres": [ - "FPS", - "action" - ] - }, - { - "id": 3859, - "name": "Far Cry", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3860, - "name": "Knock-Knock", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3861, - "name": "Might & Magic Heroes VI", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3862, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3863, - "name": "Injustice: Gods Among Us", - "site": "playground_ru", - "genres": [ - "Аркада", - "Условно-бесплатная", - "Файтинг" - ] - }, - { - "id": 3864, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3865, - "name": "Metro Last Light Redux", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3866, - "name": "Happy Room", - "site": "stopgame_ru", - "genres": [ - "simulator", - "action" - ] - }, - { - "id": 3867, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3868, - "name": "Her Story", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 3869, - "name": "Minecraft Story Mode", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3870, - "name": "Left 4 Dead 2", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 3871, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3872, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3873, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3874, - "name": "Far Cry 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3875, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3876, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3877, - "name": "Into the Breach", - "site": "playground_ru", - "genres": [ - "Инди", - "Тактика", - "Научная фантастика", - "Пошаговая", - "Стратегия" - ] - }, - { - "id": 3878, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3879, - "name": "Hero Academy", - "site": "ag_ru", - "genres": [ - "Инди", - "Настольные", - "Стратегии" - ] - }, - { - "id": 3880, - "name": "Mini Ninjas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3881, - "name": "Homefront", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3882, - "name": "Happy Wheels", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3883, - "name": "Legend Hand of God", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3884, - "name": "Lara Croft and the Guardian of Light", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Пристрели их всех", - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 3885, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3886, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3887, - "name": "Mirror's Edge", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3888, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3889, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3890, - "name": "Jade Empire", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 3891, - "name": "Heroes of Might and Magic III", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3892, - "name": "Legend of Grimrock", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3893, - "name": "Far Cry 3: Blood Dragon", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 3894, - "name": "Hotel Remorse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3895, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Боевик-приключения" - ] - }, - { - "id": 3896, - "name": "Hard Reset", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3897, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3898, - "name": "Monst", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3899, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3900, - "name": "Fallout 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 3901, - "name": "Heroes of Might and Magic IV", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3902, - "name": "Legendary", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3903, - "name": "Jotun", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 3904, - "name": "Late Shift", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3905, - "name": "Mortal Kombat IX", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3906, - "name": "Hotline Miami", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3907, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3908, - "name": "Firewatch", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3909, - "name": "Hard Reset: Exile (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3910, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3911, - "name": "Fallout: New Vegas", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3912, - "name": "FarSky", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3913, - "name": "Mortal Kombat X", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3914, - "name": "Life After Us: Shipwrecked", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3915, - "name": "Middle-earth: Shadow of Mordor", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3916, - "name": "Heroes of Might and Magic V", - "site": "ag_ru", - "genres": [ - "Стратегии" - ] - }, - { - "id": 3917, - "name": "Layers of Fear", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3918, - "name": "Just Cause", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3919, - "name": "Hotline Miami 2: Wrong Number", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3920, - "name": "Family Trouble", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3921, - "name": "Harts", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3922, - "name": "Mother Russia Bleeds", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3923, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3924, - "name": "Might & Magic Heroes VI", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3925, - "name": "Final Fantasy III (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3926, - "name": "Life Goes On: Done to Death", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3927, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3928, - "name": "Far Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3929, - "name": "First Winter", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3930, - "name": "House Flipper", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3931, - "name": "Hatred", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3932, - "name": "Just Cause 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер", - "Песочница" - ] - }, - { - "id": 3933, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3934, - "name": "Mount Your Friends", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3935, - "name": "Minecraft Story Mode", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3936, - "name": "Far Cry 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3937, - "name": "Life Is Strange", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3938, - "name": "Left 4 Dead", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3939, - "name": "Final Fantasy IV (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3940, - "name": "Five Minutes", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3941, - "name": "Fallout 4", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3942, - "name": "Kane & Lynch 2: Dog Days", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3943, - "name": "Mr. Robot", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3944, - "name": "I Am Alive", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3945, - "name": "Hellblade: Senua's Sacrifice", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3946, - "name": "Far Cry 3: Blood Dragon", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон", - "Самостоятельный" - ] - }, - { - "id": 3947, - "name": "Mini Ninjas", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 3948, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3949, - "name": "Limbo", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 3950, - "name": "Mr.President!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3951, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3952, - "name": "FarSky", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3953, - "name": "Killer is Dead: Nightmare Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3954, - "name": "Five Nights At Freddy's", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3955, - "name": "Mirror's Edge", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3956, - "name": "Left 4 Dead 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3957, - "name": "I am Setsuna", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3958, - "name": "Hellgate: London", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3959, - "name": "Fallout: New Vegas", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3960, - "name": "Homefront", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3961, - "name": "Murdered: Soul Suspect", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3962, - "name": "Little Nightmares", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "платформеры" - ] - }, - { - "id": 3963, - "name": "Final Fantasy III (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3964, - "name": "Final Fantasy X HD Remaster", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3965, - "name": "King's Bounty: Armored Princess", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3966, - "name": "Five Nights At Freddy's 2", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3967, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3968, - "name": "Legend Hand of God", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3969, - "name": "I hate this game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3970, - "name": "Final Fantasy IV (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3971, - "name": "Monst", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3972, - "name": "Her Story", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3973, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3974, - "name": "Hotel Remorse", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 3975, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3976, - "name": "Family Trouble", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3977, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3978, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3979, - "name": "King's Bounty: Crossworlds", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3980, - "name": "Legend of Grimrock", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3981, - "name": "Five Nights At Freddy's 3", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 3982, - "name": "Mortal Kombat IX", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3983, - "name": "INSIDE", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3984, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3985, - "name": "Hero Academy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3986, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3987, - "name": "Hotline Miami", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 3988, - "name": "Final Fantasy X HD Remaster", - "site": "gameguru_ru", - "genres": [ - "Ремастер", - "RPG", - "jRPG" - ] - }, - { - "id": 3989, - "name": "Mortal Kombat X", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3990, - "name": "King's Bounty: Dark Side", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Стратегия" - ] - }, - { - "id": 3991, - "name": "Legendary", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3992, - "name": "Indivisible", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3993, - "name": "Need for Speed: Underground", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3994, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3995, - "name": "Loki: Heroes of Mythology", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 3996, - "name": "Final Fantasy XIII", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3997, - "name": "Five Nights At Freddy's 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3998, - "name": "Heroes of Might and Magic III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3999, - "name": "Hotline Miami 2: Wrong Number", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 4000, - "name": "Far Cry", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4001, - "name": "Life After Us: Shipwrecked", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4002, - "name": "Mother Russia Bleeds", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик" - ] - }, - { - "id": 4003, - "name": "Never Alone", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4004, - "name": "Final Fantasy XIII", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4005, - "name": "King's Bounty: The Legend", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 4006, - "name": "Injustice: Gods Among Us", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4007, - "name": "Five Nights at Freddy's 3D", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4008, - "name": "Lost Planet: Colonies", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4009, - "name": "Life Goes On: Done to Death", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4010, - "name": "Final Fantasy XIII-2", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4011, - "name": "Heroes of Might and Magic IV", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4012, - "name": "Neverending Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4013, - "name": "Mount Your Friends", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4014, - "name": "House Flipper", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 4015, - "name": "Final Fantasy XIII-2", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4016, - "name": "Far Cry 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4017, - "name": "Five Nights in Anime v3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4018, - "name": "King's Bounty: Warriors of the North", - "site": "playground_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 4019, - "name": "Into the Breach", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4020, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4021, - "name": "Lost: Via Domus", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4022, - "name": "Nevermind", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4023, - "name": "Mr. Robot", - "site": "igromania_ru", - "genres": [ - "Тактика", - "Платформер", - "Ролевая игра" - ] - }, - { - "id": 4024, - "name": "Life Is Strange", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4025, - "name": "I Am Alive", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4026, - "name": "Heroes of Might and Magic V", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4027, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4028, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4029, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4030, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4031, - "name": "Love Is Strange (DEMO)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4032, - "name": "Fran Bow", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4033, - "name": "Jade Empire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4034, - "name": "Mr.President!", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4035, - "name": "Far Cry 3: Blood Dragon", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4036, - "name": "Limbo", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4037, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4038, - "name": "I am Setsuna", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 4039, - "name": "Neverwinter Nights", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4040, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4041, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4042, - "name": "Kingdom Rush", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Tower defence" - ] - }, - { - "id": 4043, - "name": "METAL SLUG", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4044, - "name": "Murdered: Soul Suspect", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4045, - "name": "Final Fantasy XV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4046, - "name": "Jotun", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4047, - "name": "Front Mission Evolved", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4048, - "name": "Little Nightmares", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Приключения" - ] - }, - { - "id": 4049, - "name": "Neverwinter Nights 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4050, - "name": "FarSky", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4051, - "name": "I hate this game", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4052, - "name": "Kingdom: Classic", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4053, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4054, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4055, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4056, - "name": "METAL SLUG 3", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 4057, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4058, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4059, - "name": "NieR: Automata", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4060, - "name": "Just Cause", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4061, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4062, - "name": "GUN", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4063, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4064, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Файтинг" - ] - }, - { - "id": 4065, - "name": "INSIDE", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 4066, - "name": "Final Fantasy III (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4067, - "name": "Kingdoms of Amalur: Reckoning", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 4068, - "name": "Nightmare House 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4069, - "name": "MINERVA: Metastasis", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4070, - "name": "Final Fantasy XV", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4071, - "name": "Homefront", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4072, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4073, - "name": "Just Cause 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4074, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4075, - "name": "Loki: Heroes of Mythology", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4076, - "name": "Gemini Rue", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4077, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4078, - "name": "Final Fantasy IV (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4079, - "name": "Knock-Knock", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4080, - "name": "MOTHERGUNSHIP", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 4081, - "name": "Indivisible", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 4082, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4083, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4084, - "name": "Hotel Remorse", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4085, - "name": "Nox Timore", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4086, - "name": "Lost Planet: Colonies", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4087, - "name": "Kane & Lynch 2: Dog Days", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4088, - "name": "Need for Speed: Underground", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Симулятор" - ] - }, - { - "id": 4089, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4090, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4091, - "name": "Machinarium", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4092, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4093, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4094, - "name": "Hotline Miami", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4095, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4096, - "name": "Injustice: Gods Among Us", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4097, - "name": "Firewatch", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4098, - "name": "Lost: Via Domus", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 4099, - "name": "Never Alone", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4100, - "name": "Magibot", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 4101, - "name": "Killer is Dead: Nightmare Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4102, - "name": "Geometry Dash", - "site": "gamespot_com", - "genres": [ - "Music/Rhythm" - ] - }, - { - "id": 4103, - "name": "First Winter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4104, - "name": "Final Fantasy X HD Remaster", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4105, - "name": "Omensight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4106, - "name": "Lara Croft and the Guardian of Light", - "site": "playground_ru", - "genres": [ - "Аркада", - "Вид сверху" - ] - }, - { - "id": 4107, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4108, - "name": "Love Is Strange (DEMO)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4109, - "name": "Into the Breach", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 4110, - "name": "Neverending Nightmares", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4111, - "name": "Hotline Miami 2: Wrong Number", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4112, - "name": "Five Minutes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4113, - "name": "Magicka", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4114, - "name": "King's Bounty: Armored Princess", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4115, - "name": "One Hand Clapping (DEMO)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4116, - "name": "Ghost of a Tale", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4117, - "name": "METAL SLUG", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 4118, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4119, - "name": "Nevermind", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4120, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4121, - "name": "Lara Croft and the Temple of Osiris", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4122, - "name": "Five Nights At Freddy's", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4123, - "name": "Jade Empire", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Файтинги" - ] - }, - { - "id": 4124, - "name": "House Flipper", - "site": "stopgame_ru", - "genres": [ - "simulator" - ] - }, - { - "id": 4125, - "name": "One Night at Flumpty's", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4126, - "name": "Magicka 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4127, - "name": "King's Bounty: Crossworlds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4128, - "name": "Five Nights At Freddy's 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 4129, - "name": "METAL SLUG 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4130, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4131, - "name": "Late Shift", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4132, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4133, - "name": "One Night at Flumpty's 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4134, - "name": "Goat Simulator", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 4135, - "name": "Manual Samuel", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4136, - "name": "Final Fantasy XIII", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 4137, - "name": "Jotun", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4138, - "name": "Five Nights At Freddy's 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4139, - "name": "MINERVA: Metastasis", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4140, - "name": "I Am Alive", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 4141, - "name": "King's Bounty: Dark Side", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4142, - "name": "Neverwinter Nights", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4143, - "name": "One Piece: Burning Blood", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4144, - "name": "Five Nights At Freddy's 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4145, - "name": "Mario.EXE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4146, - "name": "Layers of Fear", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 4147, - "name": "Just Cause", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4148, - "name": "MOTHERGUNSHIP", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 4149, - "name": "Neverwinter Nights 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4150, - "name": "Orcs Must Die!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4151, - "name": "I am Setsuna", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4152, - "name": "King's Bounty: The Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4153, - "name": "Firewatch", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4154, - "name": "Five Nights at Freddy's 3D", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4155, - "name": "Grand Theft Auto III", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4156, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4157, - "name": "Final Fantasy XIII-2", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Role-Playing (RPG)" - ] - }, - { - "id": 4158, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4159, - "name": "Just Cause 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4160, - "name": "NieR: Automata", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4161, - "name": "Machinarium", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 4162, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4163, - "name": "Five Nights in Anime v3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4164, - "name": "King's Bounty: Warriors of the North", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4165, - "name": "I hate this game", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4166, - "name": "Ori and the Blind Forest", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4167, - "name": "Magibot", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4168, - "name": "Masochisia", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4169, - "name": "Nightmare House 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4170, - "name": "Fran Bow", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4171, - "name": "Grand Theft Auto: San Andreas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4172, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4173, - "name": "First Winter", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4174, - "name": "Left 4 Dead", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Шутер", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4175, - "name": "Kane & Lynch 2: Dog Days", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4176, - "name": "Outlast", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4177, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4178, - "name": "Front Mission Evolved", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор", - "Шутер" - ] - }, - { - "id": 4179, - "name": "Nosferatu: The Wrath of Malachi", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4180, - "name": "INSIDE", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4181, - "name": "Masters of Anima", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4182, - "name": "Magicka", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4183, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4184, - "name": "Outlast: Whistleblower", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4185, - "name": "Five Minutes", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4186, - "name": "Grand Theft Auto: Vice City", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4187, - "name": "Left 4 Dead 2", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Шутер", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4188, - "name": "GUN", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 4189, - "name": "Killer is Dead: Nightmare Edition", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4190, - "name": "Nox Timore", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4191, - "name": "Kingdom Rush", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4192, - "name": "Max Payne", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4193, - "name": "Magicka 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4194, - "name": "Indivisible", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4195, - "name": "Overlord", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4196, - "name": "Gemini Rue", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4197, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4198, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4199, - "name": "Legend Hand of God", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 4200, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4201, - "name": "Geometry Dash", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4202, - "name": "King's Bounty: Armored Princess", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4203, - "name": "Overlord: Raising Hell", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4204, - "name": "Manual Samuel", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4205, - "name": "Kingdom: Classic", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4206, - "name": "Injustice: Gods Among Us", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4207, - "name": "Five Nights At Freddy's", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie" - ] - }, - { - "id": 4208, - "name": "Omensight", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4209, - "name": "Final Fantasy XV", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4210, - "name": "Legend of Grimrock", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 4211, - "name": "Ghost of a Tale", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4212, - "name": "Mario.EXE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4213, - "name": "Oxenfree", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4214, - "name": "Max Payne 3", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4215, - "name": "King's Bounty: Crossworlds", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4216, - "name": "Grandpa", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 4217, - "name": "One Hand Clapping (DEMO)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4218, - "name": "Kingdoms of Amalur: Reckoning", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4219, - "name": "Into the Breach", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 4220, - "name": "Goat Simulator", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Открытый мир", - "Симулятор" - ] - }, - { - "id": 4221, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4222, - "name": "Painkiller", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4223, - "name": "Five Nights At Freddy's 2", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4224, - "name": "Legendary", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4225, - "name": "Max: The Curse Of Brotherhood", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 4226, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4227, - "name": "Grey", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4228, - "name": "One Night at Flumpty's", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4229, - "name": "Grand Theft Auto III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4230, - "name": "Knock-Knock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4231, - "name": "Painkiller: Battle Out of Hell", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4232, - "name": "Jade Empire", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4233, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4234, - "name": "Life After Us: Shipwrecked", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4235, - "name": "Masochisia", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4236, - "name": "McPixel", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "инди", - "логические", - "приключения" - ] - }, - { - "id": 4237, - "name": "Grand Theft Auto: San Andreas", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4238, - "name": "Grim Fandango Remastered", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4239, - "name": "One Night at Flumpty's 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4240, - "name": "King's Bounty: Dark Side", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4241, - "name": "Painkiller: Overdose", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4242, - "name": "Five Nights At Freddy's 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 4243, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4244, - "name": "Jotun", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4245, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4246, - "name": "Grand Theft Auto: Vice City", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4247, - "name": "Masters of Anima", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4248, - "name": "Life Goes On: Done to Death", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада", - "Платформер" - ] - }, - { - "id": 4249, - "name": "Metal Gear Rising: Revengeance", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4250, - "name": "One Piece: Burning Blood", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4251, - "name": "Paint the Town Red", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4252, - "name": "King's Bounty: The Legend", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4253, - "name": "Grow Home", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4254, - "name": "Grandpa", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4255, - "name": "Lara Croft and the Guardian of Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4256, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4257, - "name": "Just Cause", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 4258, - "name": "Max Payne", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4259, - "name": "Panty Party", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4260, - "name": "Orcs Must Die!", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 4261, - "name": "Metal Wolf Chaos XD", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4262, - "name": "Life Is Strange", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Квест" - ] - }, - { - "id": 4263, - "name": "Five Nights At Freddy's 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 4264, - "name": "Grey", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4265, - "name": "King's Bounty: Warriors of the North", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4266, - "name": "Grow Up", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4267, - "name": "Papers, Please", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4268, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4269, - "name": "Lara Croft and the Temple of Osiris", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Аркады", - "Приключения" - ] - }, - { - "id": 4270, - "name": "Just Cause 2", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 4271, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4272, - "name": "Grim Fandango Remastered", - "site": "gameguru_ru", - "genres": [ - "Ремастер", - "Квест" - ] - }, - { - "id": 4273, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4274, - "name": "Metro 2033", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4275, - "name": "Five Nights at Freddy's 3D", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4276, - "name": "Limbo", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди", - "Платформер" - ] - }, - { - "id": 4277, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4278, - "name": "Peace, Death!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4279, - "name": "Kane & Lynch 2: Dog Days", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4280, - "name": "Ori and the Blind Forest", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4281, - "name": "Grow Home", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 4282, - "name": "Guns, Gore & Cannoli", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4283, - "name": "Late Shift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4284, - "name": "Metro 2033 Redux", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4285, - "name": "Pillars of Eternity", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4286, - "name": "Grow Up", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 4287, - "name": "Outlast", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4288, - "name": "Little Nightmares", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 4289, - "name": "Firewatch", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4290, - "name": "Five Nights in Anime v3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4291, - "name": "Kingdom Rush", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 4292, - "name": "Killer is Dead: Nightmare Edition", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4293, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4294, - "name": "Metro Exodus", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4295, - "name": "Guns, Gore & Cannoli", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4296, - "name": "Layers of Fear", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 4297, - "name": "Outlast: Whistleblower", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4298, - "name": "Little Nightmares - The Depths (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4299, - "name": "Max Payne 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4300, - "name": "Guns, Gore & Cannoli 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4301, - "name": "First Winter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4302, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4303, - "name": "Kingdom: Classic", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 4304, - "name": "Guns, Gore & Cannoli 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 4305, - "name": "Metro Last Light", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4306, - "name": "King's Bounty: Armored Princess", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4307, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4308, - "name": "Fran Bow", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4309, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4310, - "name": "Overlord", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4311, - "name": "Half-Life", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4312, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4313, - "name": "Max: The Curse Of Brotherhood", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4314, - "name": "Five Minutes", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4315, - "name": "Kingdoms of Amalur: Reckoning", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 4316, - "name": "Metro Last Light Redux", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4317, - "name": "Half-Life", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4318, - "name": "King's Bounty: Crossworlds", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4319, - "name": "Half-Life 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4320, - "name": "Left 4 Dead", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4321, - "name": "Overlord: Raising Hell", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4322, - "name": "Pinstripe", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4323, - "name": "McPixel", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4324, - "name": "Loki: Heroes of Mythology", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 4325, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4326, - "name": "Half-Life 2: Episode One", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4327, - "name": "Knock-Knock", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4328, - "name": "Front Mission Evolved", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4329, - "name": "Half-Life 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4330, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4331, - "name": "Lost Planet: Colonies", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4332, - "name": "Five Nights At Freddy's", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4333, - "name": "King's Bounty: Dark Side", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4334, - "name": "Oxenfree", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4335, - "name": "Left 4 Dead 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4336, - "name": "Half-Life 2: Episode Two", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4337, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4338, - "name": "Pizza Delivery 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4339, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4340, - "name": "King's Bounty: The Legend", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4341, - "name": "Half-Life: Blue Shift", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4342, - "name": "Painkiller", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4343, - "name": "Half-Life 2: Episode One", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4344, - "name": "Lost: Via Domus", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 4345, - "name": "Five Nights At Freddy's 2", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4346, - "name": "Legend Hand of God", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4347, - "name": "GUN", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4348, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4349, - "name": "Plague Inc: Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4350, - "name": "Metal Gear Rising: Revengeance", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4351, - "name": "Lara Croft and the Guardian of Light", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4352, - "name": "Half-Life: Opposing Force", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4353, - "name": "Painkiller: Battle Out of Hell", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4354, - "name": "Love Is Strange (DEMO)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4355, - "name": "King's Bounty: Warriors of the North", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 4356, - "name": "Legend of Grimrock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4357, - "name": "Plants vs. Zombies", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4358, - "name": "Half-Life: Paranoia", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4359, - "name": "Metal Wolf Chaos XD", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4360, - "name": "Five Nights At Freddy's 3", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4361, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4362, - "name": "Half-Life 2: Episode Two", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4363, - "name": "Painkiller: Overdose", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4364, - "name": "Lara Croft and the Temple of Osiris", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 4365, - "name": "METAL SLUG", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4366, - "name": "Gemini Rue", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4367, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4368, - "name": "Half-Life: The Xeno Project", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4369, - "name": "Legendary", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4370, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4371, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4372, - "name": "Metro 2033", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4373, - "name": "Late Shift", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4374, - "name": "Paint the Town Red", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 4375, - "name": "Five Nights At Freddy's 4", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4376, - "name": "METAL SLUG 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4377, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4378, - "name": "Pony Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4379, - "name": "Half-Life: Blue Shift", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4380, - "name": "Metro 2033 Redux", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4381, - "name": "Panty Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4382, - "name": "Life After Us: Shipwrecked", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4383, - "name": "Kingdom Rush", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 4384, - "name": "MINERVA: Metastasis", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4385, - "name": "Halo: Combat Evolved", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4386, - "name": "Might & Magic Heroes VI", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4387, - "name": "Portal", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4388, - "name": "Half-Life: Opposing Force", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4389, - "name": "Geometry Dash", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4390, - "name": "Layers of Fear", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 4391, - "name": "Five Nights at Freddy's 3D", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4392, - "name": "Hand of Fate", - "site": "gameguru_ru", - "genres": [ - "Карты", - "На логику" - ] - }, - { - "id": 4393, - "name": "Papers, Please", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4394, - "name": "Portal 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4395, - "name": "Metro Exodus", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4396, - "name": "Life Goes On: Done to Death", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4397, - "name": "Half-Life: Paranoia", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4398, - "name": "Minecraft Story Mode", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4399, - "name": "MOTHERGUNSHIP", - "site": "playground_ru", - "genres": [ - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 4400, - "name": "Kingdom: Classic", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4401, - "name": "Five Nights in Anime v3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4402, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4403, - "name": "Hand of Fate 2", - "site": "gameguru_ru", - "genres": [ - "Карты", - "На логику" - ] - }, - { - "id": 4404, - "name": "Prey (2006)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4405, - "name": "Peace, Death!", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4406, - "name": "Ghost of a Tale", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 4407, - "name": "Half-Life: The Xeno Project", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4408, - "name": "Mini Ninjas", - "site": "gamer_info_com", - "genres": [ - "детские", - "action" - ] - }, - { - "id": 4409, - "name": "Life Is Strange", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4410, - "name": "Happy Room", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4411, - "name": "Machinarium", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Логические", - "Адвенчура", - "Квест" - ] - }, - { - "id": 4412, - "name": "Prince of Persia (2008)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4413, - "name": "Left 4 Dead", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4414, - "name": "Pillars of Eternity", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4415, - "name": "Kingdoms of Amalur: Reckoning", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4416, - "name": "Fran Bow", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4417, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4418, - "name": "Happy Wheels", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 4419, - "name": "Mirror's Edge", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4420, - "name": "Prince of Persia: The Two Thrones", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4421, - "name": "Limbo", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4422, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4423, - "name": "Goat Simulator", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 4424, - "name": "Magibot", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Экшен", - "Логические", - "Платформер", - "Фэнтези" - ] - }, - { - "id": 4425, - "name": "Hard Reset", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4426, - "name": "Knock-Knock", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4427, - "name": "Left 4 Dead 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4428, - "name": "Front Mission Evolved", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4429, - "name": "Prospekt", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4430, - "name": "Monst", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4431, - "name": "Halo: Combat Evolved", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4432, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4433, - "name": "Hard Reset: Exile (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4434, - "name": "Little Nightmares", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4435, - "name": "Magicka", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4436, - "name": "Legend Hand of God", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 4437, - "name": "Prototype", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4438, - "name": "Mortal Kombat IX", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4439, - "name": "Harts", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4440, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4441, - "name": "Grand Theft Auto III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4442, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4443, - "name": "Hand of Fate", - "site": "gamespot_com", - "genres": [ - "Trivia/Board Game" - ] - }, - { - "id": 4444, - "name": "Little Nightmares - The Depths (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4445, - "name": "Prototype 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4446, - "name": "Hatred", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4447, - "name": "Legend of Grimrock", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 4448, - "name": "Magicka 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 4449, - "name": "GUN", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4450, - "name": "Pinstripe", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4451, - "name": "Mortal Kombat X", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4452, - "name": "Lara Croft and the Guardian of Light", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4453, - "name": "Psychonauts", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4454, - "name": "Hellblade: Senua's Sacrifice", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 4455, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4456, - "name": "Metro Last Light", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4457, - "name": "Pirates of the Caribbean: At World’s End", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4458, - "name": "Legendary", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4459, - "name": "Hand of Fate 2", - "site": "gamespot_com", - "genres": [ - "Trivia/Board Game" - ] - }, - { - "id": 4460, - "name": "Grand Theft Auto: San Andreas", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4461, - "name": "Manual Samuel", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Юмор" - ] - }, - { - "id": 4462, - "name": "Mother Russia Bleeds", - "site": "gamer_info_com", - "genres": [ - "инди", - "beat 'em up" - ] - }, - { - "id": 4463, - "name": "Hellgate: London", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4464, - "name": "PuniTy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4465, - "name": "Metro Last Light Redux", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4466, - "name": "Gemini Rue", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4467, - "name": "Pizza Delivery 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4468, - "name": "Lara Croft and the Temple of Osiris", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4469, - "name": "Loki: Heroes of Mythology", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4470, - "name": "Mario.EXE", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4471, - "name": "Her Story", - "site": "gameguru_ru", - "genres": [ - "Инди", - "На логику", - "Квест" - ] - }, - { - "id": 4472, - "name": "Life After Us: Shipwrecked", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4473, - "name": "P·O·L·L·E·N", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4474, - "name": "Mount Your Friends", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4475, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4476, - "name": "Plague Inc: Evolved", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4477, - "name": "Hero Academy", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 4478, - "name": "Grand Theft Auto: Vice City", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4479, - "name": "Geometry Dash", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4480, - "name": "Late Shift", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4481, - "name": "Lost Planet: Colonies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4482, - "name": "Quake 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4483, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4484, - "name": "Happy Room", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4485, - "name": "Marvel vs. Capcom: Infinite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4486, - "name": "Mr. Robot", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4487, - "name": "Life Goes On: Done to Death", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 4488, - "name": "Heroes of Might and Magic III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4489, - "name": "Plants vs. Zombies", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия", - "Аркада" - ] - }, - { - "id": 4490, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4491, - "name": "Quantum Break", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4492, - "name": "Masochisia", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4493, - "name": "Layers of Fear", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4494, - "name": "Lost: Via Domus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4495, - "name": "Mr.President!", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4496, - "name": "Heroes of Might and Magic IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4497, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4498, - "name": "Ghost of a Tale", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4499, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4500, - "name": "Raft", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4501, - "name": "Grandpa", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4502, - "name": "Life Is Strange", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4503, - "name": "Heroes of Might and Magic V", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4504, - "name": "Murdered: Soul Suspect", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4505, - "name": "Masters of Anima", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Фэнтези", - "Тактика" - ] - }, - { - "id": 4506, - "name": "Love Is Strange (DEMO)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4507, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4508, - "name": "Rage", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4509, - "name": "Pony Island", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 4510, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4511, - "name": "Happy Wheels", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 4512, - "name": "Limbo", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 4513, - "name": "Grey", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4514, - "name": "Goat Simulator", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4515, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 4516, - "name": "Max Payne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица" - ] - }, - { - "id": 4517, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4518, - "name": "METAL SLUG", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4519, - "name": "Left 4 Dead", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4520, - "name": "Rampage Knights", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4521, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4522, - "name": "Portal", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 4523, - "name": "Little Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 4524, - "name": "Hard Reset", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4525, - "name": "Might & Magic Heroes VI", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4526, - "name": "Homefront", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4527, - "name": "Rayman Legends", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4528, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamer_info_com", - "genres": [ - "файтинги", - "action" - ] - }, - { - "id": 4529, - "name": "METAL SLUG 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4530, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица" - ] - }, - { - "id": 4531, - "name": "Left 4 Dead 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4532, - "name": "Grim Fandango Remastered", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4533, - "name": "Portal 2", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 4534, - "name": "Hotel Remorse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4535, - "name": "Hard Reset: Exile (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4536, - "name": "Rayman Origins", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4537, - "name": "Minecraft Story Mode", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4538, - "name": "Little Nightmares - The Depths (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 4539, - "name": "Grand Theft Auto III", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4540, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4541, - "name": "MINERVA: Metastasis", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4542, - "name": "Prey (2006)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4543, - "name": "Hotline Miami", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада" - ] - }, - { - "id": 4544, - "name": "Max Payne 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 4545, - "name": "Legend Hand of God", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4546, - "name": "Rayman Raving Rabbids", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4547, - "name": "Mini Ninjas", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4548, - "name": "Grow Home", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 4549, - "name": "Need for Speed: Underground", - "site": "gamer_info_com", - "genres": [ - "гонки" - ] - }, - { - "id": 4550, - "name": "Hotline Miami 2: Wrong Number", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада" - ] - }, - { - "id": 4551, - "name": "Prince of Persia (2008)", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4552, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 4553, - "name": "MOTHERGUNSHIP", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4554, - "name": "Rayman Raving Rabbids 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4555, - "name": "Max: The Curse Of Brotherhood", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 4556, - "name": "Legend of Grimrock", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4557, - "name": "Grand Theft Auto: San Andreas", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 4558, - "name": "House Flipper", - "site": "gameguru_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 4559, - "name": "Mirror's Edge", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 4560, - "name": "Prince of Persia: The Two Thrones", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4561, - "name": "Never Alone", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 4562, - "name": "Real Horror Stories", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4563, - "name": "Machinarium", - "site": "iwantgames_ru", - "genres": [ - "Головоломки" - ] - }, - { - "id": 4564, - "name": "Harts", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 4565, - "name": "Loki: Heroes of Mythology", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 4566, - "name": "I Am Alive", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 4567, - "name": "Legendary", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4568, - "name": "Grow Up", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 4569, - "name": "McPixel", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4570, - "name": "Monst", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4571, - "name": "Prospekt", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4572, - "name": "Neverending Nightmares", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 4573, - "name": "Redeemer", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4574, - "name": "I am Setsuna", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4575, - "name": "Mortal Kombat IX", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4576, - "name": "Magibot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4577, - "name": "Life After Us: Shipwrecked", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4578, - "name": "Lost Planet: Colonies", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4579, - "name": "Grand Theft Auto: Vice City", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4580, - "name": "Nevermind", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 4581, - "name": "Metal Gear Rising: Revengeance", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер", - "Научная фантастика" - ] - }, - { - "id": 4582, - "name": "Prototype", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 4583, - "name": "Reign Of Kings", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4584, - "name": "I hate this game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4585, - "name": "Hatred", - "site": "gamespot_com", - "genres": [ - "Fixed-Screen", - "Action", - "2D", - "Shooter" - ] - }, - { - "id": 4586, - "name": "Guns, Gore & Cannoli", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4587, - "name": "Magicka", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4588, - "name": "INSIDE", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4589, - "name": "Life Goes On: Done to Death", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4590, - "name": "Reigns", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4591, - "name": "Prototype 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4592, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4593, - "name": "Grandpa", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4594, - "name": "Metal Wolf Chaos XD", - "site": "playground_ru", - "genres": [ - "Экшен", - "Научная фантастика" - ] - }, - { - "id": 4595, - "name": "Mortal Kombat X", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4596, - "name": "Indivisible", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4597, - "name": "Lost: Via Domus", - "site": "ag_ru", - "genres": [ - "Приключения", - "Аркадные" - ] - }, - { - "id": 4598, - "name": "Reigns: Her Majesty", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4599, - "name": "Magicka 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4600, - "name": "Guns, Gore & Cannoli 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4601, - "name": "Psychonauts", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4602, - "name": "Life Is Strange", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4603, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamespot_com", - "genres": [ - "Action", - "Open-World", - "3D", - "Adventure" - ] - }, - { - "id": 4604, - "name": "Neverwinter Nights", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4605, - "name": "Injustice: Gods Among Us", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4606, - "name": "Grey", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4607, - "name": "Metro 2033", - "site": "playground_ru", - "genres": [ - "Стелс", - "Научная фантастика", - "Постапокалипсис", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4608, - "name": "Mother Russia Bleeds", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4609, - "name": "Resident Evil", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4610, - "name": "Love Is Strange (DEMO)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4611, - "name": "PuniTy", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4612, - "name": "Manual Samuel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4613, - "name": "Into the Breach", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 4614, - "name": "Limbo", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4615, - "name": "Neverwinter Nights 2", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4616, - "name": "Mount Your Friends", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4617, - "name": "Metro 2033 Redux", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4618, - "name": "Hellgate: London", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 4619, - "name": "Half-Life", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4620, - "name": "P·O·L·L·E·N", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Космос" - ] - }, - { - "id": 4621, - "name": "METAL SLUG", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 4622, - "name": "Jade Empire", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4623, - "name": "Grim Fandango Remastered", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4624, - "name": "Mr. Robot", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4625, - "name": "Mario.EXE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4626, - "name": "Little Nightmares", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4627, - "name": "NieR: Automata", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4628, - "name": "Jotun", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 4629, - "name": "Quake 4", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4630, - "name": "Metro Exodus", - "site": "playground_ru", - "genres": [ - "Стелс", - "Постапокалипсис", - "Выживание", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4631, - "name": "METAL SLUG 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 4632, - "name": "Her Story", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4633, - "name": "Mr.President!", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4634, - "name": "Marvel vs. Capcom: Infinite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4635, - "name": "Just Cause", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Квест" - ] - }, - { - "id": 4636, - "name": "Half-Life 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4637, - "name": "Nightmare House 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4638, - "name": "Little Nightmares - The Depths (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4639, - "name": "Quantum Break", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 4640, - "name": "Grow Home", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4641, - "name": "Metro Last Light", - "site": "playground_ru", - "genres": [ - "Стелс", - "Научная фантастика", - "Постапокалипсис", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4642, - "name": "Just Cause 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Квест" - ] - }, - { - "id": 4643, - "name": "Hero Academy", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4644, - "name": "MINERVA: Metastasis", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 4645, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4646, - "name": "Masochisia", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4647, - "name": "Murdered: Soul Suspect", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4648, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4649, - "name": "Raft", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4650, - "name": "Kane & Lynch 2: Dog Days", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4651, - "name": "Metro Last Light Redux", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4652, - "name": "Half-Life 2: Episode One", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4653, - "name": "Heroes of Might and Magic III", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 4654, - "name": "MOTHERGUNSHIP", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4655, - "name": "Grow Up", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4656, - "name": "Nox Timore", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4657, - "name": "Killer is Dead: Nightmare Edition", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4658, - "name": "Masters of Anima", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4659, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4660, - "name": "Loki: Heroes of Mythology", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4661, - "name": "Rage", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4662, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4663, - "name": "King's Bounty: Armored Princess", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4664, - "name": "Machinarium", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4665, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "action" - ] - }, - { - "id": 4666, - "name": "Max Payne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4667, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4668, - "name": "Guns, Gore & Cannoli", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4669, - "name": "Half-Life 2: Episode Two", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4670, - "name": "Heroes of Might and Magic IV", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4671, - "name": "Lost Planet: Colonies", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4672, - "name": "King's Bounty: Crossworlds", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4673, - "name": "Rampage Knights", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4674, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4675, - "name": "Magibot", - "site": "ag_ru", - "genres": [ - "Приключения", - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 4676, - "name": "Omensight", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 4677, - "name": "King's Bounty: Dark Side", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Стратегия" - ] - }, - { - "id": 4678, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4679, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4680, - "name": "Rayman Legends", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4681, - "name": "Guns, Gore & Cannoli 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4682, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4683, - "name": "Lost: Via Domus", - "site": "stopgame_ru", - "genres": [ - "adventure", - "arcade" - ] - }, - { - "id": 4684, - "name": "Heroes of Might and Magic V", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4685, - "name": "Magicka", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4686, - "name": "King's Bounty: The Legend", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4687, - "name": "One Hand Clapping (DEMO)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4688, - "name": "Half-Life: Blue Shift", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4689, - "name": "Rayman Origins", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4690, - "name": "Max Payne 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4691, - "name": "Need for Speed: Underground", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг" - ] - }, - { - "id": 4692, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4693, - "name": "King's Bounty: Warriors of the North", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4694, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4695, - "name": "Love Is Strange (DEMO)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4696, - "name": "Half-Life", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4697, - "name": "One Night at Flumpty's", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4698, - "name": "Magicka 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 4699, - "name": "Rayman Raving Rabbids", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4700, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4701, - "name": "Half-Life: Opposing Force", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4702, - "name": "Max: The Curse Of Brotherhood", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4703, - "name": "METAL SLUG", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4704, - "name": "Middle-earth: Shadow of Mordor", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Открытый мир" - ] - }, - { - "id": 4705, - "name": "One Night at Flumpty's 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4706, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4707, - "name": "Rayman Raving Rabbids 2", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4708, - "name": "Manual Samuel", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4709, - "name": "Kingdom Rush", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4710, - "name": "Half-Life 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4711, - "name": "Might & Magic Heroes VI", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4712, - "name": "Half-Life: Paranoia", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4713, - "name": "McPixel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4714, - "name": "One Piece: Burning Blood", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4715, - "name": "Kingdom: Classic", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4716, - "name": "METAL SLUG 3", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4717, - "name": "Real Horror Stories", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4718, - "name": "Mario.EXE", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4719, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4720, - "name": "Redeemer", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 4721, - "name": "Half-Life: The Xeno Project", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4722, - "name": "Orcs Must Die!", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4723, - "name": "Minecraft Story Mode", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Фэнтези" - ] - }, - { - "id": 4724, - "name": "Metal Gear Rising: Revengeance", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4725, - "name": "MINERVA: Metastasis", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4726, - "name": "Homefront", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4727, - "name": "Never Alone", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4728, - "name": "Knock-Knock", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4729, - "name": "Half-Life 2: Episode One", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4730, - "name": "Reign Of Kings", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4731, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4732, - "name": "MOTHERGUNSHIP", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4733, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4734, - "name": "Mini Ninjas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 4735, - "name": "Metal Wolf Chaos XD", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4736, - "name": "Neverending Nightmares", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4737, - "name": "Marvel vs. Capcom: Infinite", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 4738, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4739, - "name": "Reigns", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4740, - "name": "Lara Croft and the Guardian of Light", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4741, - "name": "Half-Life 2: Episode Two", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4742, - "name": "Ori and the Blind Forest", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 4743, - "name": "Hotel Remorse", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 4744, - "name": "Machinarium", - "site": "stopgame_ru", - "genres": [ - "adventure", - "logic" - ] - }, - { - "id": 4745, - "name": "Nevermind", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4746, - "name": "Masochisia", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4747, - "name": "Metro 2033", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4748, - "name": "Mirror's Edge", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица" - ] - }, - { - "id": 4749, - "name": "Halo: Combat Evolved", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4750, - "name": "Reigns: Her Majesty", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4751, - "name": "Lara Croft and the Temple of Osiris", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4752, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4753, - "name": "Outlast", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 4754, - "name": "Masters of Anima", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения", - "Стратегии" - ] - }, - { - "id": 4755, - "name": "Monst", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4756, - "name": "Magibot", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4757, - "name": "Metro 2033 Redux", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4758, - "name": "Hotline Miami", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4759, - "name": "Late Shift", - "site": "gameguru_ru", - "genres": [ - "Интерактивный фильм", - "Квест" - ] - }, - { - "id": 4760, - "name": "Resident Evil", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4761, - "name": "Half-Life: Blue Shift", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4762, - "name": "Neverwinter Nights", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 4763, - "name": "Outlast: Whistleblower", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4764, - "name": "Mortal Kombat IX", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4765, - "name": "Layers of Fear", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4766, - "name": "Hand of Fate", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 4767, - "name": "Max Payne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4768, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4769, - "name": "Metro Exodus", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 4770, - "name": "Magicka", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4771, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4772, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Квест" - ] - }, - { - "id": 4773, - "name": "Neverwinter Nights 2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 4774, - "name": "Overlord", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 4775, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4776, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4777, - "name": "Mortal Kombat X", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Аркада", - "Файтинг" - ] - }, - { - "id": 4778, - "name": "Half-Life: Opposing Force", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 4779, - "name": "Left 4 Dead", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4780, - "name": "Metro Last Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4781, - "name": "Magicka 2", - "site": "stopgame_ru", - "genres": [ - "online", - "arcade", - "rpg" - ] - }, - { - "id": 4782, - "name": "NieR: Automata", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4783, - "name": "Hand of Fate 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 4784, - "name": "Overlord: Raising Hell", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4785, - "name": "Resident Evil 4", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4786, - "name": "Left 4 Dead 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4787, - "name": "Max Payne 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4788, - "name": "Nightmare House 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4789, - "name": "House Flipper", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 4790, - "name": "Metro Last Light Redux", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4791, - "name": "Manual Samuel", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4792, - "name": "Mother Russia Bleeds", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер", - "Ретро" - ] - }, - { - "id": 4793, - "name": "Oxenfree", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения", - "платформеры" - ] - }, - { - "id": 4794, - "name": "Resident Evil 5", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4795, - "name": "Legend Hand of God", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4796, - "name": "Max: The Curse Of Brotherhood", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4797, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4798, - "name": "Happy Room", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 4799, - "name": "Mount Your Friends", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4800, - "name": "Mario.EXE", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4801, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4802, - "name": "Legend of Grimrock", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4803, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4804, - "name": "Half-Life: Paranoia", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4805, - "name": "I Am Alive", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 4806, - "name": "Painkiller", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4807, - "name": "Nox Timore", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4808, - "name": "McPixel", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Аркадные", - "Экшены", - "Головоломки" - ] - }, - { - "id": 4809, - "name": "Mr. Robot", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4810, - "name": "Legendary", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4811, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4812, - "name": "Happy Wheels", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4813, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4814, - "name": "Painkiller: Battle Out of Hell", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4815, - "name": "Marvel vs. Capcom: Infinite", - "site": "stopgame_ru", - "genres": [ - "arcade", - "fighting" - ] - }, - { - "id": 4816, - "name": "Half-Life: The Xeno Project", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4817, - "name": "Life After Us: Shipwrecked", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4818, - "name": "I am Setsuna", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 4819, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4820, - "name": "Mr.President!", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4821, - "name": "Metal Gear Rising: Revengeance", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4822, - "name": "Resident Evil 6", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4823, - "name": "Hard Reset", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4824, - "name": "Painkiller: Overdose", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4825, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4826, - "name": "Life Goes On: Done to Death", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4827, - "name": "Masochisia", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4828, - "name": "Omensight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4829, - "name": "Metal Wolf Chaos XD", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4830, - "name": "Resident Evil 7: Biohazard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4831, - "name": "Murdered: Soul Suspect", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 4832, - "name": "Life Is Strange", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4833, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4834, - "name": "Paint the Town Red", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4835, - "name": "Hard Reset: Exile (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4836, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4837, - "name": "One Hand Clapping (DEMO)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4838, - "name": "Masters of Anima", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4839, - "name": "Limbo", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 4840, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4841, - "name": "Metro 2033", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4842, - "name": "I hate this game", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4843, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4844, - "name": "One Night at Flumpty's", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4845, - "name": "Panty Party", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "инди", - "action" - ] - }, - { - "id": 4846, - "name": "Middle-earth: Shadow of Mordor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4847, - "name": "Little Nightmares", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4848, - "name": "Halo: Combat Evolved", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4849, - "name": "One Night at Flumpty's 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4850, - "name": "Max Payne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4851, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4852, - "name": "Harts", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4853, - "name": "Metro 2033 Redux", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4854, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4855, - "name": "INSIDE", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 4856, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "Платформер" - ] - }, - { - "id": 4857, - "name": "Papers, Please", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 4858, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4859, - "name": "One Piece: Burning Blood", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4860, - "name": "Might & Magic Heroes VI", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4861, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "Платформер" - ] - }, - { - "id": 4862, - "name": "Metro Exodus", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4863, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4864, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4865, - "name": "Peace, Death!", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 4866, - "name": "Hatred", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4867, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4868, - "name": "Loki: Heroes of Mythology", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4869, - "name": "Orcs Must Die!", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 4870, - "name": "Minecraft Story Mode", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4871, - "name": "Metro Last Light", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4872, - "name": "Indivisible", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 4873, - "name": "Max Payne 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4874, - "name": "Pillars of Eternity", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4875, - "name": "Need for Speed: Underground", - "site": "playground_ru", - "genres": [ - "Аркада", - "Гонки", - "Симулятор" - ] - }, - { - "id": 4876, - "name": "Resident Evil: Operation Raccoon City", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4877, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4878, - "name": "Lost Planet: Colonies", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4879, - "name": "Mini Ninjas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4880, - "name": "Metro Last Light Redux", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4881, - "name": "Hellblade: Senua's Sacrifice", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 4882, - "name": "Resident Evil: Revelations", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4883, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4884, - "name": "Lost: Via Domus", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 4885, - "name": "Max: The Curse Of Brotherhood", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4886, - "name": "Never Alone", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4887, - "name": "Injustice: Gods Among Us", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 4888, - "name": "Ori and the Blind Forest", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 4889, - "name": "Love Is Strange (DEMO)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4890, - "name": "Hand of Fate", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4891, - "name": "Mirror's Edge", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4892, - "name": "Resident Evil: Revelations 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик", - "Приключение", - "Боевик от третьего лица" - ] - }, - { - "id": 4893, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4894, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4895, - "name": "McPixel", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4896, - "name": "Hellgate: London", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 4897, - "name": "METAL SLUG", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Шутер" - ] - }, - { - "id": 4898, - "name": "Neverending Nightmares", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4899, - "name": "Into the Breach", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy", - "Tactical" - ] - }, - { - "id": 4900, - "name": "Reus", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4901, - "name": "Monst", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4902, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4903, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4904, - "name": "METAL SLUG 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4905, - "name": "Hand of Fate 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4906, - "name": "Nevermind", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4907, - "name": "Metal Gear Rising: Revengeance", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4908, - "name": "Outlast", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 4909, - "name": "Rezrog", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 4910, - "name": "MINERVA: Metastasis", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4911, - "name": "Pinstripe", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 4912, - "name": "Mortal Kombat IX", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4913, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4914, - "name": "Jade Empire", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 4915, - "name": "Her Story", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 4916, - "name": "Outlast: Whistleblower", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4917, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4918, - "name": "MOTHERGUNSHIP", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4919, - "name": "Happy Room", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation" - ] - }, - { - "id": 4920, - "name": "Rise of the Tomb Raider", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4921, - "name": "Metal Wolf Chaos XD", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4922, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4923, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4924, - "name": "Mortal Kombat X", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4925, - "name": "Overlord", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 4926, - "name": "Machinarium", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 4927, - "name": "Hero Academy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4928, - "name": "Happy Wheels", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4929, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4930, - "name": "Neverwinter Nights", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 4931, - "name": "Metro 2033", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4932, - "name": "Pizza Delivery 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4933, - "name": "Magibot", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4934, - "name": "Overlord: Raising Hell", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия" - ] - }, - { - "id": 4935, - "name": "Middle-earth: Shadow of Mordor", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4936, - "name": "Jotun", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4937, - "name": "Mother Russia Bleeds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4938, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4939, - "name": "Heroes of Might and Magic III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4940, - "name": "Magicka", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4941, - "name": "Hard Reset", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4942, - "name": "Metro 2033 Redux", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4943, - "name": "Neverwinter Nights 2", - "site": "playground_ru", - "genres": [ - "Вид сверху", - "От третьего лица", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 4944, - "name": "Plague Inc: Evolved", - "site": "gamer_info_com", - "genres": [ - "инди", - "RTS" - ] - }, - { - "id": 4945, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4946, - "name": "Mount Your Friends", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4947, - "name": "Might & Magic Heroes VI", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4948, - "name": "Magicka 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 4949, - "name": "Heroes of Might and Magic IV", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4950, - "name": "Just Cause", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4951, - "name": "Plants vs. Zombies", - "site": "gamer_info_com", - "genres": [ - "tower defense" - ] - }, - { - "id": 4952, - "name": "Metro Exodus", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4953, - "name": "Hard Reset: Exile (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 4954, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4955, - "name": "NieR: Automata", - "site": "playground_ru", - "genres": [ - "Экшен", - "Постапокалипсис", - "Слэшер", - "Научная фантастика" - ] - }, - { - "id": 4956, - "name": "Manual Samuel", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Квест" - ] - }, - { - "id": 4957, - "name": "Oxenfree", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4958, - "name": "Mr. Robot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4959, - "name": "Minecraft Story Mode", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4960, - "name": "Heroes of Might and Magic V", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4961, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 4962, - "name": "Nightmare House 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4963, - "name": "Mario.EXE", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4964, - "name": "Risen", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4965, - "name": "Harts", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4966, - "name": "Metro Last Light", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4967, - "name": "Just Cause 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4968, - "name": "Painkiller", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4969, - "name": "Mr.President!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4970, - "name": "Marvel vs. Capcom: Infinite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4971, - "name": "Rock of Ages", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 4972, - "name": "Mini Ninjas", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Семейные" - ] - }, - { - "id": 4973, - "name": "Pony Island", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4974, - "name": "Nosferatu: The Wrath of Malachi", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4975, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4976, - "name": "Metro Last Light Redux", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4977, - "name": "Masochisia", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 4978, - "name": "Hatred", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4979, - "name": "Painkiller: Battle Out of Hell", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4980, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4981, - "name": "Murdered: Soul Suspect", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4982, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 4983, - "name": "Mirror's Edge", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 4984, - "name": "Nox Timore", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4985, - "name": "Portal", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "action" - ] - }, - { - "id": 4986, - "name": "Masters of Anima", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 4987, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4988, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4989, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4990, - "name": "Painkiller: Overdose", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4991, - "name": "Root Of Evil: The Tailor", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 4992, - "name": "Max Payne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4993, - "name": "Hellblade: Senua's Sacrifice", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4994, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4995, - "name": "Monst", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4996, - "name": "Portal 2", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "action" - ] - }, - { - "id": 4997, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 4998, - "name": "Paint the Town Red", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4999, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5000, - "name": "Runic Rampage", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5001, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5002, - "name": "King's Bounty: Armored Princess", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5003, - "name": "Homefront", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5004, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5005, - "name": "Mortal Kombat IX", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5006, - "name": "Panty Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5007, - "name": "Prey (2006)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5008, - "name": "Omensight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Инди" - ] - }, - { - "id": 5009, - "name": "Max Payne 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5010, - "name": "Hellgate: London", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5011, - "name": "Rust", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5012, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5013, - "name": "Max: The Curse Of Brotherhood", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5014, - "name": "Prince of Persia (2008)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5015, - "name": "Papers, Please", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5016, - "name": "One Hand Clapping (DEMO)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5017, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5018, - "name": "King's Bounty: Crossworlds", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5019, - "name": "Mortal Kombat X", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 5020, - "name": "Hotel Remorse", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5021, - "name": "Ryse: Son of Rome", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5022, - "name": "McPixel", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5023, - "name": "Peace, Death!", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 5024, - "name": "Prince of Persia: The Two Thrones", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5025, - "name": "One Night at Flumpty's", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5026, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5027, - "name": "Her Story", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5028, - "name": "Need for Speed: Underground", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5029, - "name": "King's Bounty: Dark Side", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5030, - "name": "Metal Gear Rising: Revengeance", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 5031, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5032, - "name": "Mother Russia Bleeds", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 5033, - "name": "One Night at Flumpty's 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5034, - "name": "Prospekt", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5035, - "name": "Hotline Miami", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5036, - "name": "Metal Wolf Chaos XD", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Роботы" - ] - }, - { - "id": 5037, - "name": "Pillars of Eternity", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 5038, - "name": "Never Alone", - "site": "iwantgames_ru", - "genres": [ - "Аркады" - ] - }, - { - "id": 5039, - "name": "Middle-earth: Shadow of Mordor", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 5040, - "name": "Hero Academy", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5041, - "name": "SCP-087", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5042, - "name": "Mount Your Friends", - "site": "ag_ru", - "genres": [ - "Спортивные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 5043, - "name": "King's Bounty: The Legend", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5044, - "name": "Metro 2033", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5045, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5046, - "name": "Might & Magic Heroes VI", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5047, - "name": "One Piece: Burning Blood", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5048, - "name": "Prototype", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5049, - "name": "SCP-087-B", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5050, - "name": "Neverending Nightmares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5051, - "name": "Heroes of Might and Magic III", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5052, - "name": "Metro 2033 Redux", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5053, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5054, - "name": "Mr. Robot", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5055, - "name": "King's Bounty: Warriors of the North", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5056, - "name": "Hotline Miami 2: Wrong Number", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5057, - "name": "SINNER: Sacrifice for Redemption", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5058, - "name": "Prototype 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5059, - "name": "Minecraft Story Mode", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5060, - "name": "Orcs Must Die!", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 5061, - "name": "Metro Exodus", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер" - ] - }, - { - "id": 5062, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5063, - "name": "Nevermind", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5064, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5065, - "name": "Mr.President!", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 5066, - "name": "SWAT 4", - "site": "igromania_ru", - "genres": [ - "Тактика", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5067, - "name": "Metro Last Light", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5068, - "name": "Psychonauts", - "site": "gamer_info_com", - "genres": [ - "приключения", - "платформеры" - ] - }, - { - "id": 5069, - "name": "Heroes of Might and Magic IV", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5070, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5071, - "name": "Mini Ninjas", - "site": "stopgame_ru", - "genres": [ - "for kids", - "action" - ] - }, - { - "id": 5072, - "name": "Pinstripe", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 5073, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5074, - "name": "House Flipper", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie" - ] - }, - { - "id": 5075, - "name": "Metro Last Light Redux", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5076, - "name": "Murdered: Soul Suspect", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 5077, - "name": "Kingdom Rush", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 5078, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5079, - "name": "PuniTy", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5080, - "name": "Ori and the Blind Forest", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5081, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5082, - "name": "Mirror's Edge", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5083, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5084, - "name": "Neverwinter Nights", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5085, - "name": "Kingdom: Classic", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5086, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5087, - "name": "Heroes of Might and Magic V", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5088, - "name": "P·O·L·L·E·N", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5089, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Файтинги" - ] - }, - { - "id": 5090, - "name": "Pizza Delivery 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5091, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5092, - "name": "I Am Alive", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5093, - "name": "Outlast", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ужасы" - ] - }, - { - "id": 5094, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5095, - "name": "Monst", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5096, - "name": "Neverwinter Nights 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5097, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5098, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "ag_ru", - "genres": [ - "Файтинги" - ] - }, - { - "id": 5099, - "name": "Quake 4", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5100, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 5101, - "name": "Plague Inc: Evolved", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 5102, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5103, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5104, - "name": "Outlast: Whistleblower", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5105, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5106, - "name": "NieR: Automata", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 5107, - "name": "Mortal Kombat IX", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5108, - "name": "Quantum Break", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5109, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5110, - "name": "I am Setsuna", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 5111, - "name": "Plants vs. Zombies", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Головоломки/Пазл" - ] - }, - { - "id": 5112, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5113, - "name": "Middle-earth: Shadow of Mordor", - "site": "gameguru_ru", - "genres": [ - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 5114, - "name": "Overlord", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5115, - "name": "Nightmare House 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5116, - "name": "Knock-Knock", - "site": "gamespot_com", - "genres": [ - "Miscellaneous" - ] - }, - { - "id": 5117, - "name": "Need for Speed: Underground", - "site": "ag_ru", - "genres": [ - "Гонки", - "Аркадные" - ] - }, - { - "id": 5118, - "name": "Mortal Kombat X", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5119, - "name": "Raft", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5120, - "name": "Might & Magic Heroes VI", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5121, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamebomb_ru", - "genres": [ - "Многопользоватеский шутер" - ] - }, - { - "id": 5122, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5123, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5124, - "name": "Overlord: Raising Hell", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5125, - "name": "I hate this game", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5126, - "name": "Nosferatu: The Wrath of Malachi", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5127, - "name": "Minecraft Story Mode", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5128, - "name": "Rage", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5129, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5130, - "name": "Never Alone", - "site": "ag_ru", - "genres": [ - "Аркадные" - ] - }, - { - "id": 5131, - "name": "Mother Russia Bleeds", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5132, - "name": "Pony Island", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5133, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5134, - "name": "Homefront", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5135, - "name": "Mini Ninjas", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 5136, - "name": "Oxenfree", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди" - ] - }, - { - "id": 5137, - "name": "Nox Timore", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5138, - "name": "Rampage Knights", - "site": "gamer_info_com", - "genres": [ - "инди", - "beat 'em up" - ] - }, - { - "id": 5139, - "name": "Sacred", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5140, - "name": "Neverending Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 5141, - "name": "Mount Your Friends", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5142, - "name": "Portal", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 5143, - "name": "INSIDE", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 5144, - "name": "Mirror's Edge", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор", - "Шутер" - ] - }, - { - "id": 5145, - "name": "Hotel Remorse", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5146, - "name": "Lara Croft and the Guardian of Light", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5147, - "name": "Rayman Legends", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5148, - "name": "Sacred 3", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5149, - "name": "Painkiller", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 5150, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5151, - "name": "Monst", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5152, - "name": "Nevermind", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 5153, - "name": "Mr. Robot", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5154, - "name": "Portal 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 5155, - "name": "Rayman Origins", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5156, - "name": "Mortal Kombat IX", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5157, - "name": "Hotline Miami", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5158, - "name": "Sacred Citadel", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Боевик" - ] - }, - { - "id": 5159, - "name": "Painkiller: Battle Out of Hell", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5160, - "name": "Indivisible", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 5161, - "name": "Omensight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5162, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5163, - "name": "Mortal Kombat X", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5164, - "name": "Mr.President!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5165, - "name": "Prey (2006)", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5166, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5167, - "name": "Rayman Raving Rabbids", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5168, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5169, - "name": "Painkiller: Overdose", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5170, - "name": "Injustice: Gods Among Us", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5171, - "name": "Hotline Miami 2: Wrong Number", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5172, - "name": "One Hand Clapping (DEMO)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5173, - "name": "Mother Russia Bleeds", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Beat-em-up" - ] - }, - { - "id": 5174, - "name": "Prince of Persia (2008)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5175, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5176, - "name": "Late Shift", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5177, - "name": "Rayman Raving Rabbids 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5178, - "name": "Murdered: Soul Suspect", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5179, - "name": "Mount Your Friends", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5180, - "name": "Neverwinter Nights", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5181, - "name": "Paint the Town Red", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Выживание" - ] - }, - { - "id": 5182, - "name": "Prince of Persia: The Two Thrones", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5183, - "name": "One Night at Flumpty's", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5184, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5185, - "name": "House Flipper", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 5186, - "name": "Mr. Robot", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5187, - "name": "Real Horror Stories", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5188, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5189, - "name": "Layers of Fear", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 5190, - "name": "Into the Breach", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 5191, - "name": "Neverwinter Nights 2", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5192, - "name": "Prospekt", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5193, - "name": "Saints Row IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5194, - "name": "Mr.President!", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5195, - "name": "One Night at Flumpty's 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5196, - "name": "Panty Party", - "site": "playground_ru", - "genres": [ - "Экшен", - "Юмор", - "Инди" - ] - }, - { - "id": 5197, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5198, - "name": "Redeemer", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 5199, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5200, - "name": "I Am Alive", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5201, - "name": "Jade Empire", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5202, - "name": "Murdered: Soul Suspect", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5203, - "name": "Prototype", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5204, - "name": "Saints Row: Gat out of Hell", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5205, - "name": "One Piece: Burning Blood", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5206, - "name": "Papers, Please", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Ретро", - "Адвенчура", - "Инди" - ] - }, - { - "id": 5207, - "name": "NieR: Automata", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 5208, - "name": "Reign Of Kings", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5209, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5210, - "name": "Left 4 Dead", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5211, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5212, - "name": "Salt and Sanctuary", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 5213, - "name": "Jotun", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5214, - "name": "I am Setsuna", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5215, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5216, - "name": "Nightmare House 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5217, - "name": "Orcs Must Die!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5218, - "name": "Reigns", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 5219, - "name": "Peace, Death!", - "site": "playground_ru", - "genres": [ - "Ретро", - "Юмор", - "Инди" - ] - }, - { - "id": 5220, - "name": "Samsara Room", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 5221, - "name": "Need for Speed: Underground", - "site": "stopgame_ru", - "genres": [ - "arcade", - "racing" - ] - }, - { - "id": 5222, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5223, - "name": "I hate this game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5224, - "name": "Left 4 Dead 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 5225, - "name": "Nosferatu: The Wrath of Malachi", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5226, - "name": "Reigns: Her Majesty", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 5227, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5228, - "name": "Pillars of Eternity", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 5229, - "name": "Just Cause", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5230, - "name": "Need for Speed: Underground", - "site": "gameguru_ru", - "genres": [ - "Гонки/вождение" - ] - }, - { - "id": 5231, - "name": "Saw: The Video Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5232, - "name": "Never Alone", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5233, - "name": "Prototype 2", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 5234, - "name": "Never Alone", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5235, - "name": "INSIDE", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5236, - "name": "Legend Hand of God", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5237, - "name": "Nox Timore", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5238, - "name": "Resident Evil", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5239, - "name": "Scratches", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 5240, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5241, - "name": "Ori and the Blind Forest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5242, - "name": "Neverending Nightmares", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5243, - "name": "Psychonauts", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер" - ] - }, - { - "id": 5244, - "name": "Neverending Nightmares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5245, - "name": "Just Cause 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5246, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5247, - "name": "Seasons after Fall", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5248, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5249, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5250, - "name": "Legend of Grimrock", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5251, - "name": "Indivisible", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5252, - "name": "Nevermind", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5253, - "name": "Outlast", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 5254, - "name": "PuniTy", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5255, - "name": "Serena", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5256, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5257, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5258, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5259, - "name": "Nevermind", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5260, - "name": "Omensight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5261, - "name": "Injustice: Gods Among Us", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5262, - "name": "Outlast: Whistleblower", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5263, - "name": "P·O·L·L·E·N", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5264, - "name": "Neverwinter Nights", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5265, - "name": "Serious Sam 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5266, - "name": "Pinstripe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5267, - "name": "Legendary", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5268, - "name": "Kane & Lynch 2: Dog Days", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5269, - "name": "Resident Evil 4", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "action" - ] - }, - { - "id": 5270, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5271, - "name": "One Hand Clapping (DEMO)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5272, - "name": "Quake 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 5273, - "name": "Neverwinter Nights 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5274, - "name": "Overlord", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5275, - "name": "Serious Sam 3: BFE", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5276, - "name": "Life After Us: Shipwrecked", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5277, - "name": "Pirates of the Caribbean: At World’s End", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5278, - "name": "Into the Breach", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5279, - "name": "Resident Evil 5", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 5280, - "name": "NieR: Automata", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер", - "Роботы" - ] - }, - { - "id": 5281, - "name": "Neverwinter Nights", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5282, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5283, - "name": "One Night at Flumpty's", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5284, - "name": "Overlord: Raising Hell", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5285, - "name": "Life Goes On: Done to Death", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5286, - "name": "Killer is Dead: Nightmare Edition", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5287, - "name": "Quantum Break", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5288, - "name": "Pizza Delivery 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5289, - "name": "Nightmare House 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5290, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5291, - "name": "Jade Empire", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5292, - "name": "Serious Sam Classic: The First Encounter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5293, - "name": "Neverwinter Nights 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5294, - "name": "Plague Inc: Evolved", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5295, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5296, - "name": "Oxenfree", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5297, - "name": "Raft", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5298, - "name": "Life Is Strange", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5299, - "name": "One Night at Flumpty's 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5300, - "name": "Serious Sam Classic: The Second Encounter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5301, - "name": "Jotun", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5302, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5303, - "name": "Nox Timore", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5304, - "name": "NieR: Automata", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5305, - "name": "Rage", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5306, - "name": "King's Bounty: Armored Princess", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5307, - "name": "Painkiller", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5308, - "name": "Plants vs. Zombies", - "site": "playground_ru", - "genres": [ - "Зомби", - "Аркада", - "Tower defence" - ] - }, - { - "id": 5309, - "name": "Shadow Warrior", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 5310, - "name": "One Piece: Burning Blood", - "site": "ag_ru", - "genres": [ - "Файтинги", - "Аркадные" - ] - }, - { - "id": 5311, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 5312, - "name": "Resident Evil 6", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "action" - ] - }, - { - "id": 5313, - "name": "Rampage Knights", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5314, - "name": "Nightmare House 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5315, - "name": "Just Cause", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 5316, - "name": "Omensight", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 5317, - "name": "Painkiller: Battle Out of Hell", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5318, - "name": "Shadow of the Tomb Raider", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 5319, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 5320, - "name": "Resident Evil 7: Biohazard", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 5321, - "name": "One Hand Clapping (DEMO)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5322, - "name": "Limbo", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 5323, - "name": "Orcs Must Die!", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5324, - "name": "King's Bounty: Crossworlds", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5325, - "name": "Rayman Legends", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 5326, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5327, - "name": "Painkiller: Overdose", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5328, - "name": "Nosferatu: The Wrath of Malachi", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5329, - "name": "One Night at Flumpty's", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 5330, - "name": "Just Cause 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5331, - "name": "Pony Island", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 5332, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5333, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5334, - "name": "Shantae: Half-Genie Hero", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 5335, - "name": "Rayman Origins", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5336, - "name": "Little Nightmares", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5337, - "name": "Paint the Town Red", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5338, - "name": "One Night at Flumpty's 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5339, - "name": "Nox Timore", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5340, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5341, - "name": "Portal", - "site": "playground_ru", - "genres": [ - "Экшен", - "Логические", - "Научная фантастика" - ] - }, - { - "id": 5342, - "name": "Ori and the Blind Forest", - "site": "ag_ru", - "genres": [ - "Платформеры" - ] - }, - { - "id": 5343, - "name": "King's Bounty: Dark Side", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5344, - "name": "Shoot Your Nightmare Halloween Special", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5345, - "name": "One Piece: Burning Blood", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5346, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5347, - "name": "Rayman Raving Rabbids", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 5348, - "name": "Panty Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5349, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5350, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5351, - "name": "Orcs Must Die!", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 5352, - "name": "Outlast", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 5353, - "name": "Shovel Knight: Shovel of Hope", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5354, - "name": "Kane & Lynch 2: Dog Days", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5355, - "name": "Portal 2", - "site": "playground_ru", - "genres": [ - "Логические", - "От первого лица", - "Научная фантастика" - ] - }, - { - "id": 5356, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5357, - "name": "Rayman Raving Rabbids 2", - "site": "gamebomb_ru", - "genres": [ - "Музыка/Ритм", - "Миниигры" - ] - }, - { - "id": 5358, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5359, - "name": "Papers, Please", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5360, - "name": "Omensight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5361, - "name": "Shrek 2: The Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5362, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5363, - "name": "Loki: Heroes of Mythology", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5364, - "name": "Prey (2006)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5365, - "name": "King's Bounty: The Legend", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5366, - "name": "Real Horror Stories", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5367, - "name": "Killer is Dead: Nightmare Edition", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5368, - "name": "Outlast: Whistleblower", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5369, - "name": "Ori and the Blind Forest", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5370, - "name": "Silence on The Line", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5371, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamer_info_com", - "genres": [ - "командные шутеры" - ] - }, - { - "id": 5372, - "name": "Peace, Death!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5373, - "name": "Lost Planet: Colonies", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5374, - "name": "One Hand Clapping (DEMO)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5375, - "name": "Outlast", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5376, - "name": "Redeemer", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5377, - "name": "Prince of Persia (2008)", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Аркада", - "Экшен", - "Открытый мир", - "Платформер", - "Фэнтези" - ] - }, - { - "id": 5378, - "name": "Overlord", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 5379, - "name": "Silent Hill: Alchemilla", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5380, - "name": "Resident Evil: Revelations", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5381, - "name": "Outlast: Whistleblower", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5382, - "name": "King's Bounty: Armored Princess", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5383, - "name": "Pillars of Eternity", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5384, - "name": "King's Bounty: Warriors of the North", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5385, - "name": "Reign Of Kings", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5386, - "name": "One Night at Flumpty's", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5387, - "name": "Overlord: Raising Hell", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5388, - "name": "Prince of Persia: The Two Thrones", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Платформер" - ] - }, - { - "id": 5389, - "name": "Silverfall", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5390, - "name": "Overlord", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5391, - "name": "Lost: Via Domus", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5392, - "name": "Resident Evil: Revelations 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5393, - "name": "Reigns", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5394, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5395, - "name": "Prospekt", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5396, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5397, - "name": "King's Bounty: Crossworlds", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5398, - "name": "Overlord: Raising Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5399, - "name": "Silverfall: Earth Awakening", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5400, - "name": "One Night at Flumpty's 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5401, - "name": "Love Is Strange (DEMO)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5402, - "name": "Reigns: Her Majesty", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5403, - "name": "Oxenfree", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5404, - "name": "Reus", - "site": "gamer_info_com", - "genres": [ - "симуляторы бога" - ] - }, - { - "id": 5405, - "name": "Oxenfree", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 5406, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5407, - "name": "Singularity", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5408, - "name": "Painkiller", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5409, - "name": "Prototype", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5410, - "name": "Resident Evil", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5411, - "name": "One Piece: Burning Blood", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5412, - "name": "Painkiller", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5413, - "name": "Rezrog", - "site": "gamer_info_com", - "genres": [ - "инди", - "тактика", - "TBS" - ] - }, - { - "id": 5414, - "name": "King's Bounty: Dark Side", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Role-Playing (RPG)" - ] - }, - { - "id": 5415, - "name": "Kingdom Rush", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 5416, - "name": "Slap-Man", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5417, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5418, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5419, - "name": "METAL SLUG", - "site": "gamespot_com", - "genres": [ - "Action", - "Shoot-'Em-Up", - "2D" - ] - }, - { - "id": 5420, - "name": "Painkiller: Battle Out of Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5421, - "name": "Prototype 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 5422, - "name": "Orcs Must Die!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5423, - "name": "Painkiller: Battle Out of Hell", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5424, - "name": "Rise of the Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5425, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5426, - "name": "Slay the Spire", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Пошаговая стратегия" - ] - }, - { - "id": 5427, - "name": "Pinstripe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5428, - "name": "Painkiller: Overdose", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5429, - "name": "King's Bounty: The Legend", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5430, - "name": "Painkiller: Overdose", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5431, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5432, - "name": "Psychonauts", - "site": "playground_ru", - "genres": [ - "Юмор", - "От третьего лица", - "Аркада", - "Экшен", - "Платформер" - ] - }, - { - "id": 5433, - "name": "Kingdom: Classic", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 5434, - "name": "Sleeping Dogs", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5435, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5436, - "name": "Paint the Town Red", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5437, - "name": "Resident Evil 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5438, - "name": "Pirates of the Caribbean: At World’s End", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5439, - "name": "METAL SLUG 3", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Shoot-'Em-Up" - ] - }, - { - "id": 5440, - "name": "Paint the Town Red", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 5441, - "name": "Panty Party", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 5442, - "name": "PuniTy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5443, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5444, - "name": "Slender: The Arrival", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5445, - "name": "Ori and the Blind Forest", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5446, - "name": "Resident Evil 5", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 5447, - "name": "King's Bounty: Warriors of the North", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Role-Playing (RPG)" - ] - }, - { - "id": 5448, - "name": "Papers, Please", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5449, - "name": "Pizza Delivery 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5450, - "name": "Kingdoms of Amalur: Reckoning", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 5451, - "name": "Slendytubbies", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5452, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5453, - "name": "Panty Party", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Экшены", - "Казуальные", - "Головоломки", - "Симуляторы" - ] - }, - { - "id": 5454, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5455, - "name": "P·O·L·L·E·N", - "site": "playground_ru", - "genres": [ - "Виртуальная реальность", - "От первого лица", - "Космос" - ] - }, - { - "id": 5456, - "name": "MINERVA: Metastasis", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5457, - "name": "Outlast", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5458, - "name": "Peace, Death!", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Инди" - ] - }, - { - "id": 5459, - "name": "Slime Rancher", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Логика", - "Боевик" - ] - }, - { - "id": 5460, - "name": "Plague Inc: Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5461, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5462, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5463, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5464, - "name": "Papers, Please", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Ролевые", - "Инди", - "Симуляторы" - ] - }, - { - "id": 5465, - "name": "Pillars of Eternity", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5466, - "name": "Knock-Knock", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5467, - "name": "Outlast: Whistleblower", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 5468, - "name": "Quake 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 5469, - "name": "Sniper Elite", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5470, - "name": "Plants vs. Zombies", - "site": "iwantgames_ru", - "genres": [ - "Стратегии" - ] - }, - { - "id": 5471, - "name": "Risen", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 5472, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5473, - "name": "Peace, Death!", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Аркадные" - ] - }, - { - "id": 5474, - "name": "MOTHERGUNSHIP", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 5475, - "name": "Kingdom Rush", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5476, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5477, - "name": "Quantum Break", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 5478, - "name": "Sniper Elite 3", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5479, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5480, - "name": "Overlord", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5481, - "name": "Resident Evil 6", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 5482, - "name": "Rock of Ages", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "action", - "TBS" - ] - }, - { - "id": 5483, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5484, - "name": "Pillars of Eternity", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5485, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5486, - "name": "Sniper Elite V2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5487, - "name": "Kingdom: Classic", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5488, - "name": "Overlord: Raising Hell", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5489, - "name": "Raft", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Выживание", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 5490, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5491, - "name": "Pony Island", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5492, - "name": "Resident Evil 7: Biohazard", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5493, - "name": "Pinstripe", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 5494, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5495, - "name": "Machinarium", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5496, - "name": "Sophie's Curse", - "site": "igromania_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 5497, - "name": "Lara Croft and the Guardian of Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5498, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5499, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5500, - "name": "Oxenfree", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5501, - "name": "Root Of Evil: The Tailor", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 5502, - "name": "Portal", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5503, - "name": "Rage", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5504, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 5505, - "name": "South Park: The Fractured But Whole", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 5506, - "name": "Kingdoms of Amalur: Reckoning", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5507, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5508, - "name": "Pizza Delivery 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5509, - "name": "Runic Rampage", - "site": "gamer_info_com", - "genres": [ - "инди", - "hack & slash" - ] - }, - { - "id": 5510, - "name": "Painkiller", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5511, - "name": "Portal 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5512, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5513, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 5514, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5515, - "name": "Rampage Knights", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 5516, - "name": "Lara Croft and the Temple of Osiris", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5517, - "name": "Plague Inc: Evolved", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 5518, - "name": "Knock-Knock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5519, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5520, - "name": "Rust", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 5521, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5522, - "name": "Painkiller: Battle Out of Hell", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5523, - "name": "Plants vs. Zombies", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5524, - "name": "Prey (2006)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5525, - "name": "Rayman Legends", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор", - "Платформер" - ] - }, - { - "id": 5526, - "name": "Pinstripe", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Головоломки", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 5527, - "name": "Magibot", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 5528, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5529, - "name": "South Park: The Stick of Truth", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5530, - "name": "Ryse: Son of Rome", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 5531, - "name": "Late Shift", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5532, - "name": "Painkiller: Overdose", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5533, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 5534, - "name": "Prince of Persia (2008)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5535, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5536, - "name": "Rayman Origins", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор", - "Платформер" - ] - }, - { - "id": 5537, - "name": "Pony Island", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5538, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5539, - "name": "Pirates of the Caribbean: At World’s End", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5540, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5541, - "name": "Paint the Town Red", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5542, - "name": "Prince of Persia: The Two Thrones", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5543, - "name": "Portal", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Головоломка" - ] - }, - { - "id": 5544, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5545, - "name": "Magicka", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5546, - "name": "Layers of Fear", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5547, - "name": "Rayman Raving Rabbids", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор" - ] - }, - { - "id": 5548, - "name": "Pizza Delivery 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5549, - "name": "Resident Evil: Revelations", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5550, - "name": "Lara Croft and the Guardian of Light", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5551, - "name": "SCP-087", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5552, - "name": "Portal 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Головоломка" - ] - }, - { - "id": 5553, - "name": "Panty Party", - "site": "stopgame_ru", - "genres": [ - "arcade", - "for adults" - ] - }, - { - "id": 5554, - "name": "Prospekt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5555, - "name": "Prey (2006)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5556, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5557, - "name": "Rayman Raving Rabbids 2", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5558, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5559, - "name": "Magicka 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5560, - "name": "SCP-087-B", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5561, - "name": "Resident Evil: Revelations 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5562, - "name": "Plague Inc: Evolved", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 5563, - "name": "Papers, Please", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5564, - "name": "Lara Croft and the Temple of Osiris", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5565, - "name": "Prototype", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5566, - "name": "Prince of Persia (2008)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5567, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5568, - "name": "Reus", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5569, - "name": "Real Horror Stories", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5570, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди", - "action" - ] - }, - { - "id": 5571, - "name": "Plants vs. Zombies", - "site": "ag_ru", - "genres": [ - "Экшены", - "Головоломки", - "Аркадные", - "Стратегии" - ] - }, - { - "id": 5572, - "name": "Manual Samuel", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5573, - "name": "Prince of Persia: The Two Thrones", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5574, - "name": "Rezrog", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5575, - "name": "Prototype 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5576, - "name": "Left 4 Dead", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5577, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5578, - "name": "Peace, Death!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5579, - "name": "Late Shift", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5580, - "name": "Prospekt", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5581, - "name": "Mario.EXE", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5582, - "name": "Redeemer", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Файтинг", - "Вид сверху" - ] - }, - { - "id": 5583, - "name": "SWAT 4", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика" - ] - }, - { - "id": 5584, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5585, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5586, - "name": "Prototype", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5587, - "name": "Psychonauts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5588, - "name": "Rise of the Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 5589, - "name": "Pillars of Eternity", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5590, - "name": "Reign Of Kings", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Средневековье", - "Ролевая", - "Выживание", - "Экшен", - "От первого лица" - ] - }, - { - "id": 5591, - "name": "Layers of Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5592, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5593, - "name": "Prototype 2", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5594, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5595, - "name": "Left 4 Dead 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5596, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 5597, - "name": "Pony Island", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Аркадные", - "Приключения" - ] - }, - { - "id": 5598, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5599, - "name": "PuniTy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5600, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5601, - "name": "Psychonauts", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5602, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5603, - "name": "Reigns", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Адвенчура", - "Аркада", - "Карточная", - "Ролевая" - ] - }, - { - "id": 5604, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5605, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5606, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5607, - "name": "Legend Hand of God", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5608, - "name": "PuniTy", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5609, - "name": "Portal", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 5610, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5611, - "name": "P·O·L·L·E·N", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5612, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5613, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5614, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5615, - "name": "Reigns: Her Majesty", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Аркада", - "Карточная", - "Ролевая", - "Симулятор" - ] - }, - { - "id": 5616, - "name": "P·O·L·L·E·N", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5617, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5618, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5619, - "name": "Portal 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Головоломки" - ] - }, - { - "id": 5620, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5621, - "name": "Quake 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5622, - "name": "Quake 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5623, - "name": "Masochisia", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5624, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5625, - "name": "Legend of Grimrock", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 5626, - "name": "Resident Evil", - "site": "playground_ru", - "genres": [ - "Экшен", - "Зомби", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5627, - "name": "Left 4 Dead", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5628, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5629, - "name": "Risen", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5630, - "name": "Quantum Break", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5631, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5632, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5633, - "name": "Quantum Break", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5634, - "name": "Prey (2006)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5635, - "name": "Pinstripe", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5636, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5637, - "name": "Masters of Anima", - "site": "gamespot_com", - "genres": [ - "MOBA", - "Strategy" - ] - }, - { - "id": 5638, - "name": "Raft", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Симулятор" - ] - }, - { - "id": 5639, - "name": "Rock of Ages", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 5640, - "name": "Legendary", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5641, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5642, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5643, - "name": "Spooky's House of Jump Scares", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5644, - "name": "Rage", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Шутер" - ] - }, - { - "id": 5645, - "name": "Raft", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5646, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5647, - "name": "Prince of Persia (2008)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5648, - "name": "Pirates of the Caribbean: At World’s End", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5649, - "name": "Max Payne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5650, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5651, - "name": "Life After Us: Shipwrecked", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5652, - "name": "Rampage Knights", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Инди" - ] - }, - { - "id": 5653, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 5654, - "name": "Root Of Evil: The Tailor", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5655, - "name": "Left 4 Dead 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5656, - "name": "Resident Evil 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5657, - "name": "Rage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5658, - "name": "Rayman Legends", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5659, - "name": "Prince of Persia: The Two Thrones", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5660, - "name": "Pizza Delivery 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5661, - "name": "Runic Rampage", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5662, - "name": "Sacred", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5663, - "name": "Star Wars: The Force Unleashed", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 5664, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5665, - "name": "Rayman Origins", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 5666, - "name": "Resident Evil 5", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Кооператив", - "Зомби", - "Шутер", - "Экшен" - ] - }, - { - "id": 5667, - "name": "Rampage Knights", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5668, - "name": "Legend Hand of God", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5669, - "name": "Life Goes On: Done to Death", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 5670, - "name": "Plague Inc: Evolved", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 5671, - "name": "Prospekt", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5672, - "name": "Sacred 3", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 5673, - "name": "Star Wars: The Force Unleashed II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5674, - "name": "Rayman Raving Rabbids", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Для детей" - ] - }, - { - "id": 5675, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5676, - "name": "Rust", - "site": "gamebomb_ru", - "genres": [ - "Многопользоватеский шутер", - "Боевик-приключения" - ] - }, - { - "id": 5677, - "name": "Max Payne 3", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5678, - "name": "Rayman Legends", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5679, - "name": "SteamWorld Dig", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5680, - "name": "Life Is Strange", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5681, - "name": "Sacred Citadel", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5682, - "name": "Rayman Raving Rabbids 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Для детей" - ] - }, - { - "id": 5683, - "name": "Prototype", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5684, - "name": "Legend of Grimrock", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5685, - "name": "Plants vs. Zombies", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 5686, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5687, - "name": "Real Horror Stories", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5688, - "name": "SteamWorld Dig 2", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Приключение" - ] - }, - { - "id": 5689, - "name": "Ryse: Son of Rome", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5690, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5691, - "name": "Rayman Origins", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5692, - "name": "Prototype 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5693, - "name": "Max: The Curse Of Brotherhood", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5694, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "stopgame_ru", - "genres": [ - "online", - "action" - ] - }, - { - "id": 5695, - "name": "Legendary", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5696, - "name": "Redeemer", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5697, - "name": "Limbo", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5698, - "name": "Resident Evil 6", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Кооператив", - "Шутер", - "Экшен" - ] - }, - { - "id": 5699, - "name": "SteamWorld Heist", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Стратегия", - "Пошаговая стратегия", - "Боевик" - ] - }, - { - "id": 5700, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5701, - "name": "Psychonauts", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 5702, - "name": "Reign Of Kings", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор" - ] - }, - { - "id": 5703, - "name": "Rayman Raving Rabbids", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5704, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5705, - "name": "Life After Us: Shipwrecked", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5706, - "name": "McPixel", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5707, - "name": "Pony Island", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5708, - "name": "Resident Evil 7: Biohazard", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5709, - "name": "Steampunk Tower 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5710, - "name": "Reigns", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Инди", - "Стратегия", - "Квест" - ] - }, - { - "id": 5711, - "name": "SCP-087", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5712, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5713, - "name": "PuniTy", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5714, - "name": "Rayman Raving Rabbids 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5715, - "name": "Little Nightmares", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5716, - "name": "Steel Rats", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик" - ] - }, - { - "id": 5717, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5718, - "name": "Portal", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5719, - "name": "Reigns: Her Majesty", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Инди", - "Стратегия", - "Квест" - ] - }, - { - "id": 5720, - "name": "Life Goes On: Done to Death", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 5721, - "name": "SCP-087-B", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5722, - "name": "Metal Gear Rising: Revengeance", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5723, - "name": "Saints Row IV", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "action" - ] - }, - { - "id": 5724, - "name": "Resident Evil", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5725, - "name": "Stories: The Path of Destinies", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5726, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5727, - "name": "Real Horror Stories", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5728, - "name": "Little Nightmares - The Depths (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5729, - "name": "Life Is Strange", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5730, - "name": "Portal 2", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5731, - "name": "P·O·L·L·E·N", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 5732, - "name": "Metal Wolf Chaos XD", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5733, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5734, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5735, - "name": "Saints Row: Gat out of Hell", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5736, - "name": "Street Fighter X Tekken", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 5737, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5738, - "name": "Redeemer", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5739, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5740, - "name": "Quake 4", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5741, - "name": "SWAT 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн", - "Стратегия", - "Симулятор", - "Шутер" - ] - }, - { - "id": 5742, - "name": "Prey (2006)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5743, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5744, - "name": "Limbo", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5745, - "name": "Metro 2033", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5746, - "name": "Salt and Sanctuary", - "site": "gamer_info_com", - "genres": [ - "RPG", - "платформеры" - ] - }, - { - "id": 5748, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5749, - "name": "Resident Evil 4", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5750, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5751, - "name": "Reign Of Kings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5752, - "name": "Loki: Heroes of Mythology", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5753, - "name": "Metro 2033 Redux", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5754, - "name": "Quantum Break", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5755, - "name": "Stupidella", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5756, - "name": "Prince of Persia (2008)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5757, - "name": "Samsara Room", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5758, - "name": "Resident Evil 5", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5759, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5760, - "name": "Resident Evil: Operation Raccoon City", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Кооператив", - "Зомби", - "Шутер", - "Экшен" - ] - }, - { - "id": 5761, - "name": "Little Nightmares", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5762, - "name": "Reigns", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5763, - "name": "Sudeki", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5764, - "name": "Lost Planet: Colonies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5765, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5766, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5767, - "name": "Raft", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5768, - "name": "Saw: The Video Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5769, - "name": "Prince of Persia: The Two Thrones", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5770, - "name": "Metro Exodus", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5771, - "name": "Resident Evil: Revelations", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5772, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5773, - "name": "Reigns: Her Majesty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5774, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5775, - "name": "Superhot", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5776, - "name": "Little Nightmares - The Depths (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5777, - "name": "Lost: Via Domus", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5778, - "name": "Scratches", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5779, - "name": "Rage", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Гонки", - "Экшены" - ] - }, - { - "id": 5780, - "name": "Prospekt", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5781, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5782, - "name": "Resident Evil 6", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5783, - "name": "Metro Last Light", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5784, - "name": "Resident Evil: Revelations 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ужасы" - ] - }, - { - "id": 5785, - "name": "Surgeon Simulator", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5786, - "name": "Resident Evil", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5787, - "name": "Love Is Strange (DEMO)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5788, - "name": "Seasons after Fall", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 5789, - "name": "Rampage Knights", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5790, - "name": "Resident Evil 7: Biohazard", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5791, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5792, - "name": "Prototype", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5793, - "name": "Metro Last Light Redux", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5794, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5795, - "name": "Tales from the Borderlands", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5796, - "name": "Reus", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Песочница", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 5797, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5798, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5799, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5800, - "name": "Serena", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения" - ] - }, - { - "id": 5801, - "name": "Rayman Legends", - "site": "ag_ru", - "genres": [ - "Платформеры" - ] - }, - { - "id": 5802, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5803, - "name": "Tales of Berseria", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 5804, - "name": "Rezrog", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5805, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5806, - "name": "METAL SLUG", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5807, - "name": "Prototype 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5808, - "name": "Sacred", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 5809, - "name": "Serious Sam 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5810, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5811, - "name": "Rayman Origins", - "site": "ag_ru", - "genres": [ - "Экшены", - "Семейные", - "Аркадные", - "Платформеры" - ] - }, - { - "id": 5812, - "name": "Loki: Heroes of Mythology", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 5813, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5814, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5815, - "name": "Tales of Zestiria", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5816, - "name": "Rise of the Tomb Raider", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 5817, - "name": "Psychonauts", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5818, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5819, - "name": "Serious Sam 3: BFE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5820, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5821, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5822, - "name": "Sacred 3", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 5823, - "name": "Rayman Raving Rabbids", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5824, - "name": "Resident Evil 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5825, - "name": "Lost Planet: Colonies", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5826, - "name": "METAL SLUG 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5827, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5828, - "name": "Resident Evil: Operation Raccoon City", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5829, - "name": "PuniTy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5830, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5831, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5832, - "name": "Tasty Planet: Back for Seconds", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5833, - "name": "Rayman Raving Rabbids 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5834, - "name": "Sacred Citadel", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Пристрели их всех" - ] - }, - { - "id": 5835, - "name": "Lost: Via Domus", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5836, - "name": "Resident Evil 5", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5837, - "name": "MINERVA: Metastasis", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5838, - "name": "Resident Evil: Revelations", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5839, - "name": "Tekken 7", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 5840, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5841, - "name": "P·O·L·L·E·N", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5842, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5843, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5844, - "name": "Resident Evil: Revelations 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5845, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5846, - "name": "Real Horror Stories", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5847, - "name": "Love Is Strange (DEMO)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5848, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5849, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5850, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5851, - "name": "Reus", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 5852, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5853, - "name": "Quake 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5854, - "name": "MOTHERGUNSHIP", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5855, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5856, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5857, - "name": "Redeemer", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 5858, - "name": "Rezrog", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 5859, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5860, - "name": "METAL SLUG", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5861, - "name": "Might & Magic Heroes VI", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5862, - "name": "Tesla vs Lovecraft", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5863, - "name": "Shadow Warrior", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5864, - "name": "Quantum Break", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5865, - "name": "Rise of the Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 5866, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5867, - "name": "Reign Of Kings", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Симуляторы" - ] - }, - { - "id": 5868, - "name": "Minecraft Story Mode", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5869, - "name": "Teslagrad", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Логика" - ] - }, - { - "id": 5870, - "name": "Resident Evil 6", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5871, - "name": "Machinarium", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5872, - "name": "Shadow of the Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5873, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5874, - "name": "METAL SLUG 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5875, - "name": "Reigns", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Симуляторы", - "Карточные" - ] - }, - { - "id": 5876, - "name": "The Banner Saga", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия", - "Ролевая игра" - ] - }, - { - "id": 5877, - "name": "Raft", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 5878, - "name": "Risen", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Открытый мир" - ] - }, - { - "id": 5879, - "name": "Mini Ninjas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5880, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5881, - "name": "Resident Evil 7: Biohazard", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 5882, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5883, - "name": "Saints Row IV", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5884, - "name": "MINERVA: Metastasis", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5885, - "name": "Reigns: Her Majesty", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Инди", - "Казуальные", - "Симуляторы", - "Карточные" - ] - }, - { - "id": 5886, - "name": "The Banner Saga 2", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия", - "Ролевая игра" - ] - }, - { - "id": 5887, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5888, - "name": "Rock of Ages", - "site": "playground_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5889, - "name": "Rage", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5890, - "name": "Magibot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 5891, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5892, - "name": "Mirror's Edge", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5893, - "name": "Shantae: Half-Genie Hero", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5894, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5895, - "name": "The Bard’s Tale (2004)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5896, - "name": "Resident Evil", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5897, - "name": "Saints Row: Gat out of Hell", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5898, - "name": "MOTHERGUNSHIP", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5899, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Аркада", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 5900, - "name": "Rampage Knights", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5901, - "name": "Risen", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 5902, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5903, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5904, - "name": "The Beginner's Guide", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5905, - "name": "Salt and Sanctuary", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5906, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5907, - "name": "Root Of Evil: The Tailor", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5908, - "name": "Monst", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5909, - "name": "Rock of Ages", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5910, - "name": "Rayman Legends", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5911, - "name": "Magicka", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 5912, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5913, - "name": "Machinarium", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5914, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5915, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5916, - "name": "Samsara Room", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5917, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 5918, - "name": "Runic Rampage", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5919, - "name": "Mortal Kombat IX", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5920, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5921, - "name": "The Binding of Isaac: Rebirth", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5922, - "name": "Rayman Origins", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5923, - "name": "Shrek 2: The Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5924, - "name": "Saw: The Video Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5925, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5926, - "name": "Root Of Evil: The Tailor", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5927, - "name": "Magibot", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5928, - "name": "Rust", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Адвенчура", - "Выживание", - "Экшен" - ] - }, - { - "id": 5929, - "name": "Magicka 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5930, - "name": "Resident Evil 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5931, - "name": "The Bunker", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5932, - "name": "Mortal Kombat X", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Fighting" - ] - }, - { - "id": 5933, - "name": "Runic Rampage", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5934, - "name": "Scratches", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5935, - "name": "Silence on The Line", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5936, - "name": "Rayman Raving Rabbids", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5937, - "name": "Resident Evil: Operation Raccoon City", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5938, - "name": "Rust", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Инди" - ] - }, - { - "id": 5939, - "name": "Ryse: Son of Rome", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 5940, - "name": "Resident Evil 5", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5941, - "name": "Seasons after Fall", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 5942, - "name": "The Cat Lady", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 5943, - "name": "Silent Hill: Alchemilla", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5944, - "name": "Mother Russia Bleeds", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 5945, - "name": "Resident Evil: Revelations", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5946, - "name": "Ryse: Son of Rome", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 5947, - "name": "Manual Samuel", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5948, - "name": "Magicka", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5949, - "name": "Rayman Raving Rabbids 2", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5950, - "name": "The Council", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 5951, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5952, - "name": "Serena", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5953, - "name": "Silverfall", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 5954, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Постапокалипсис", - "Ролевая", - "Выживание", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 5955, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5956, - "name": "Mario.EXE", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5957, - "name": "Resident Evil: Revelations 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5958, - "name": "Mount Your Friends", - "site": "gamespot_com", - "genres": [ - "Sports" - ] - }, - { - "id": 5959, - "name": "Magicka 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5960, - "name": "Real Horror Stories", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5961, - "name": "Serious Sam 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5962, - "name": "The Curse of Blackwater", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5963, - "name": "SCP-087", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5964, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5965, - "name": "SCP-087", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5966, - "name": "Silverfall: Earth Awakening", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5967, - "name": "SCP-087-B", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5968, - "name": "Serious Sam 3: BFE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5969, - "name": "Reus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5970, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5971, - "name": "Mr. Robot", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5972, - "name": "Redeemer", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5973, - "name": "SCP-087-B", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5974, - "name": "Manual Samuel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5975, - "name": "Singularity", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5976, - "name": "Resident Evil 6", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5977, - "name": "Marvel vs. Capcom: Infinite", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5978, - "name": "SINNER: Sacrifice for Redemption", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 5979, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5980, - "name": "The Darkness II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5981, - "name": "Reign Of Kings", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5982, - "name": "Rezrog", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5983, - "name": "Mario.EXE", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5984, - "name": "Slap-Man", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5985, - "name": "SINNER: Sacrifice for Redemption", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 5986, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5987, - "name": "SWAT 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5988, - "name": "The Elder Scrolls IV: Oblivion", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5989, - "name": "Resident Evil 7: Biohazard", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5990, - "name": "Mr.President!", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 5991, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5992, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5993, - "name": "Slay the Spire", - "site": "gamer_info_com", - "genres": [ - "инди", - "карточные", - "TBS" - ] - }, - { - "id": 5994, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5995, - "name": "Rise of the Tomb Raider", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 5996, - "name": "SWAT 4", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5997, - "name": "Reigns", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5998, - "name": "Masochisia", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5999, - "name": "Marvel vs. Capcom: Infinite", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6000, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6001, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6002, - "name": "Shadow Warrior", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 6003, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6004, - "name": "Sleeping Dogs", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6005, - "name": "Murdered: Soul Suspect", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6006, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6007, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6008, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6009, - "name": "Reigns: Her Majesty", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6010, - "name": "Shadow of the Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6011, - "name": "The Elder Scrolls V: Skyrim", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6012, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6013, - "name": "Masters of Anima", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Adventure" - ] - }, - { - "id": 6014, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6015, - "name": "Slender: The Arrival", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6016, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6017, - "name": "Masochisia", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6018, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6019, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6020, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6021, - "name": "Resident Evil", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 6022, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6023, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6024, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6025, - "name": "Slendytubbies", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6026, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6027, - "name": "Shantae: Half-Genie Hero", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6028, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6029, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6030, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6031, - "name": "Max Payne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6032, - "name": "Masters of Anima", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)", - "Adventure" - ] - }, - { - "id": 6033, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6034, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6035, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6036, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6037, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6038, - "name": "Slime Rancher", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "инди" - ] - }, - { - "id": 6039, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6040, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6041, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6042, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6043, - "name": "Sacred", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6044, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6045, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6046, - "name": "Max Payne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6047, - "name": "Sniper Elite", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6048, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6049, - "name": "Resident Evil: Operation Raccoon City", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6050, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6051, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6052, - "name": "Shrek 2: The Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6053, - "name": "Risen", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6054, - "name": "Sacred 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 6055, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6056, - "name": "The Evil Within", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик", - "Приключение", - "Боевик от третьего лица" - ] - }, - { - "id": 6057, - "name": "Sniper Elite 3", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6058, - "name": "Silence on The Line", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6059, - "name": "Resident Evil 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6060, - "name": "Sacred Citadel", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6061, - "name": "Resident Evil: Revelations", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6062, - "name": "Need for Speed: Underground", - "site": "gamespot_com", - "genres": [ - "Simulation", - "Driving/Racing" - ] - }, - { - "id": 6063, - "name": "Rock of Ages", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6064, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6065, - "name": "The Evil Within 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6066, - "name": "Max Payne 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6067, - "name": "Sniper Elite V2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6068, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6069, - "name": "Silent Hill: Alchemilla", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6070, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6071, - "name": "Resident Evil 5", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 6072, - "name": "Resident Evil: Revelations 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6073, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6074, - "name": "The Evil Within: The Assignment (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6075, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6076, - "name": "Never Alone", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6077, - "name": "Sacred", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 6078, - "name": "Sophie's Curse", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6079, - "name": "Silverfall", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6080, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6081, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6082, - "name": "The Evil Within: The Consequence (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6083, - "name": "Reus", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 6084, - "name": "Max: The Curse Of Brotherhood", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6085, - "name": "Root Of Evil: The Tailor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6086, - "name": "Max Payne 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6087, - "name": "South Park: The Fractured But Whole", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6088, - "name": "Sacred 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6089, - "name": "Neverending Nightmares", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6090, - "name": "Silverfall: Earth Awakening", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6091, - "name": "Saints Row IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6092, - "name": "The Evil Within: The Executioner (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6093, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6094, - "name": "Runic Rampage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6095, - "name": "Saints Row: Gat out of Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер", - "Аддон" - ] - }, - { - "id": 6096, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6097, - "name": "Sacred Citadel", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6098, - "name": "McPixel", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6099, - "name": "Singularity", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 6100, - "name": "The Expendabros", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 6101, - "name": "Max: The Curse Of Brotherhood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6102, - "name": "Salt and Sanctuary", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "RPG", - "Платформер" - ] - }, - { - "id": 6103, - "name": "Rezrog", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 6104, - "name": "Resident Evil 6", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6105, - "name": "Rust", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6106, - "name": "Slap-Man", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6107, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6108, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6109, - "name": "The Final Station", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 6110, - "name": "Samsara Room", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6111, - "name": "McPixel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6112, - "name": "Resident Evil 7: Biohazard", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6113, - "name": "Nevermind", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6114, - "name": "Ryse: Son of Rome", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6115, - "name": "South Park: The Stick of Truth", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6116, - "name": "Slay the Spire", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Карточные игры" - ] - }, - { - "id": 6117, - "name": "The First Tree", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6118, - "name": "Metal Gear Rising: Revengeance", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6119, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6120, - "name": "Saw: The Video Game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6121, - "name": "Rise of the Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 6122, - "name": "Scratches", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6123, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6124, - "name": "Metal Gear Rising: Revengeance", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6125, - "name": "The Forest", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 6126, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6127, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6128, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6129, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6130, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6131, - "name": "Seasons after Fall", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6132, - "name": "Metal Wolf Chaos XD", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6133, - "name": "The Hat Man Shadow Ward", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6134, - "name": "Saints Row IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6135, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6136, - "name": "Sleeping Dogs", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6137, - "name": "Neverwinter Nights", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6138, - "name": "Metal Wolf Chaos XD", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6139, - "name": "Serena", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6140, - "name": "SCP-087", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6141, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6142, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6143, - "name": "Metro 2033", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6144, - "name": "The Incredible Adventures of Van Helsing", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6145, - "name": "Serious Sam 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6146, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6147, - "name": "Saints Row: Gat out of Hell", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 6148, - "name": "Slender: The Arrival", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6149, - "name": "SCP-087-B", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6150, - "name": "Metro 2033", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6151, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6152, - "name": "Neverwinter Nights 2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6153, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6154, - "name": "Serious Sam 3: BFE", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6155, - "name": "The Incredible Adventures of Van Helsing II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6156, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6157, - "name": "Slendytubbies", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6158, - "name": "Salt and Sanctuary", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Пошаговая" - ] - }, - { - "id": 6159, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6160, - "name": "SINNER: Sacrifice for Redemption", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6161, - "name": "Metro 2033 Redux", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6162, - "name": "The Incredible Adventures of Van Helsing III", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6163, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6164, - "name": "NieR: Automata", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 6165, - "name": "Slime Rancher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6166, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6167, - "name": "Samsara Room", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6168, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6169, - "name": "Serious Sam Classic: The First Encounter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6170, - "name": "Metro 2033 Redux", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6171, - "name": "The Last Remnant", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6172, - "name": "SWAT 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6173, - "name": "Nightmare House 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6174, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6175, - "name": "Risen", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 6176, - "name": "Sniper Elite", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 6177, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6178, - "name": "Resident Evil: Operation Raccoon City", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6179, - "name": "Metro Exodus", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6180, - "name": "Saw: The Video Game", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 6181, - "name": "The Mask Man", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6182, - "name": "Metro Exodus", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6183, - "name": "Shadow Warrior", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6184, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6185, - "name": "Sniper Elite 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6186, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6187, - "name": "Rock of Ages", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Платформеры", - "Стратегии" - ] - }, - { - "id": 6188, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6189, - "name": "The Punisher (2005)", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6190, - "name": "Metro Last Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6191, - "name": "Resident Evil: Revelations", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6192, - "name": "Scratches", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 6193, - "name": "Shadow of the Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 6194, - "name": "Metro Last Light", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6195, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6196, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6197, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Аркадные", - "Стратегии" - ] - }, - { - "id": 6198, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Аддон" - ] - }, - { - "id": 6199, - "name": "The Secret of Monkey Island: Special Edition", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6200, - "name": "Seasons after Fall", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6201, - "name": "Sniper Elite V2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6202, - "name": "Resident Evil: Revelations 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6203, - "name": "Nox Timore", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6204, - "name": "Shantae: Half-Genie Hero", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6205, - "name": "The Slippery Slope", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6206, - "name": "Root Of Evil: The Tailor", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Казуальные", - "Симуляторы" - ] - }, - { - "id": 6207, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6208, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6209, - "name": "Metro Last Light Redux", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6210, - "name": "Metro Last Light Redux", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action" - ] - }, - { - "id": 6211, - "name": "Serena", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6212, - "name": "Sophie's Curse", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6213, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6214, - "name": "The Stanley Parable", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6215, - "name": "Reus", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 6216, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6217, - "name": "Runic Rampage", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 6218, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6219, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6220, - "name": "Shovel Knight: Shovel of Hope", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6221, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6222, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6223, - "name": "Serious Sam 2", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6224, - "name": "South Park: The Fractured But Whole", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6225, - "name": "The Walking Dead: A New Frontier", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6226, - "name": "Shrek 2: The Game", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6227, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6228, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6229, - "name": "Rezrog", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 6230, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6231, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6232, - "name": "Rust", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Шутеры", - "Экшены", - "ММО" - ] - }, - { - "id": 6233, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6234, - "name": "Silence on The Line", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6235, - "name": "The Walking Dead: Michonne", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6236, - "name": "Serious Sam 3: BFE", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6237, - "name": "Omensight", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6238, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6239, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6240, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6241, - "name": "Rise of the Tomb Raider", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6242, - "name": "Silent Hill: Alchemilla", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6243, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6244, - "name": "The Walking Dead: Season One", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6245, - "name": "Ryse: Son of Rome", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6246, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6247, - "name": "One Hand Clapping (DEMO)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6248, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6249, - "name": "Spooky's House of Jump Scares", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6250, - "name": "Silverfall", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6251, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6252, - "name": "The Walking Dead: Season Two", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6253, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6254, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6255, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6256, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6257, - "name": "One Night at Flumpty's", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6258, - "name": "Silverfall: Earth Awakening", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6259, - "name": "South Park: The Stick of Truth", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6260, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6261, - "name": "Serious Sam Classic: The First Encounter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6262, - "name": "The Witcher", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6263, - "name": "Sacred", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6264, - "name": "SCP-087", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6265, - "name": "One Night at Flumpty's 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6266, - "name": "Singularity", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6267, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6268, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6269, - "name": "Middle-earth: Shadow of Mordor", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 6270, - "name": "Star Wars: The Force Unleashed", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6271, - "name": "Serious Sam Classic: The Second Encounter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6272, - "name": "The Witcher 2: Assassins of Kings", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6273, - "name": "Slap-Man", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6274, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6275, - "name": "Middle-earth: Shadow of Mordor", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6276, - "name": "Sacred 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6277, - "name": "SCP-087-B", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6278, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6279, - "name": "Slay the Spire", - "site": "gameguru_ru", - "genres": [ - "Инди", - "На логику", - "CCG" - ] - }, - { - "id": 6280, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6281, - "name": "The Witcher 3: Wild Hunt", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6282, - "name": "Star Wars: The Force Unleashed II", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6283, - "name": "Shadow Warrior", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 6284, - "name": "One Piece: Burning Blood", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6285, - "name": "Might & Magic Heroes VI", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 6286, - "name": "Sacred Citadel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6287, - "name": "Sleeping Dogs", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6288, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6289, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6290, - "name": "Might & Magic Heroes VI", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 6291, - "name": "SINNER: Sacrifice for Redemption", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6292, - "name": "SteamWorld Dig", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6293, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6294, - "name": "Shadow of the Tomb Raider", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 6295, - "name": "Slender: The Arrival", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6296, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6297, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6298, - "name": "Orcs Must Die!", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 6299, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6300, - "name": "Minecraft Story Mode", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 6301, - "name": "SWAT 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6302, - "name": "SteamWorld Dig 2", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6303, - "name": "Minecraft Story Mode", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6304, - "name": "Slendytubbies", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6305, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6306, - "name": "Risen", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6307, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6308, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6309, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6310, - "name": "The Wolf Among Us", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6311, - "name": "Slime Rancher", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6312, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6313, - "name": "SteamWorld Heist", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6314, - "name": "Rock of Ages", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 6315, - "name": "Mini Ninjas", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6316, - "name": "Sniper Elite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6317, - "name": "Shantae: Half-Genie Hero", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6318, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6319, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6320, - "name": "Ori and the Blind Forest", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6321, - "name": "Mini Ninjas", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6322, - "name": "Thief", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6323, - "name": "Steampunk Tower 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6324, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6325, - "name": "Sniper Elite 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Тактический", - "Стелс", - "Шутер" - ] - }, - { - "id": 6326, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6327, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6328, - "name": "Shoot Your Nightmare Halloween Special", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6329, - "name": "Saints Row IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6330, - "name": "This Is The Police", - "site": "igromania_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 6331, - "name": "Steel Rats", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "гонки" - ] - }, - { - "id": 6332, - "name": "Sniper Elite V2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6333, - "name": "Mirror's Edge", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6334, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6335, - "name": "Root Of Evil: The Tailor", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6336, - "name": "Shovel Knight: Shovel of Hope", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6337, - "name": "Mirror's Edge", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6338, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6339, - "name": "TimeShift", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6340, - "name": "Sophie's Curse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6341, - "name": "Saints Row: Gat out of Hell", - "site": "iwantgames_ru", - "genres": [ - "Дополнение" - ] - }, - { - "id": 6342, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6343, - "name": "Outlast", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6344, - "name": "Stories: The Path of Destinies", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 6345, - "name": "Monst", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6346, - "name": "Runic Rampage", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6347, - "name": "South Park: The Fractured But Whole", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6348, - "name": "Shrek 2: The Game", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Аркада" - ] - }, - { - "id": 6349, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6350, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6351, - "name": "Titan Quest", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6352, - "name": "Monst", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6353, - "name": "Salt and Sanctuary", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6354, - "name": "Street Fighter X Tekken", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 6355, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 6356, - "name": "Mortal Kombat IX", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6357, - "name": "Silence on The Line", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6358, - "name": "Outlast: Whistleblower", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6359, - "name": "Titan Quest Anniversary Edition", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6360, - "name": "Rust", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6361, - "name": "Spooky's House of Jump Scares", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6362, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6363, - "name": "Mortal Kombat IX", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6364, - "name": "Samsara Room", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6366, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 6367, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6368, - "name": "Silent Hill: Alchemilla", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6369, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6370, - "name": "South Park: The Stick of Truth", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6371, - "name": "Ryse: Son of Rome", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6372, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6373, - "name": "Overlord", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6374, - "name": "Stupidella", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6375, - "name": "Saw: The Video Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6376, - "name": "Mortal Kombat X", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6377, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6378, - "name": "Silverfall", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6379, - "name": "Mortal Kombat X", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6380, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6381, - "name": "Star Wars: The Force Unleashed", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6382, - "name": "Overlord: Raising Hell", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6383, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6384, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6385, - "name": "Titan Souls", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6386, - "name": "Sudeki", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6387, - "name": "Scratches", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6388, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6389, - "name": "Silverfall: Earth Awakening", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6390, - "name": "Mother Russia Bleeds", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6391, - "name": "Star Wars: The Force Unleashed II", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6392, - "name": "SCP-087", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6393, - "name": "Titanfall 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "MMOG", - "Боевик" - ] - }, - { - "id": 6394, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6395, - "name": "Superhot", - "site": "gamer_info_com", - "genres": [ - "FPS", - "инди" - ] - }, - { - "id": 6396, - "name": "Mother Russia Bleeds", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 6397, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6398, - "name": "Seasons after Fall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6399, - "name": "Singularity", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6400, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6401, - "name": "Mount Your Friends", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6402, - "name": "To The Moon", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 6403, - "name": "Oxenfree", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6404, - "name": "SCP-087-B", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6405, - "name": "Surgeon Simulator", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6406, - "name": "Sacred", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6407, - "name": "SteamWorld Dig", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Приключения", - "Платформер" - ] - }, - { - "id": 6408, - "name": "Serena", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6409, - "name": "Slap-Man", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6410, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6411, - "name": "Mount Your Friends", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Simulation", - "Indie" - ] - }, - { - "id": 6412, - "name": "Tomb Raider", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6413, - "name": "Tales from the Borderlands", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6414, - "name": "SteamWorld Dig 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер" - ] - }, - { - "id": 6415, - "name": "SINNER: Sacrifice for Redemption", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6416, - "name": "Mr. Robot", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6417, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6418, - "name": "Sacred 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 6419, - "name": "Serious Sam 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6420, - "name": "Slay the Spire", - "site": "playground_ru", - "genres": [ - "Инди", - "Карточная", - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6421, - "name": "Torchlight", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6422, - "name": "Tales of Berseria", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6423, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6424, - "name": "Painkiller", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6425, - "name": "SteamWorld Heist", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 6426, - "name": "SWAT 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6427, - "name": "Mr. Robot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 6428, - "name": "Sacred Citadel", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6429, - "name": "Serious Sam 3: BFE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6430, - "name": "Mr.President!", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6431, - "name": "Torchlight II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6432, - "name": "Sleeping Dogs", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 6433, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6434, - "name": "Steampunk Tower 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6435, - "name": "Painkiller: Battle Out of Hell", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6436, - "name": "Tales of Zestiria", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6437, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6438, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6439, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6440, - "name": "Toren", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6441, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6442, - "name": "Slender: The Arrival", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 6443, - "name": "Steel Rats", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг" - ] - }, - { - "id": 6444, - "name": "Painkiller: Overdose", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6445, - "name": "Mr.President!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 6446, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6447, - "name": "Murdered: Soul Suspect", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6448, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6449, - "name": "Tormentum: Dark Sorrow", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6450, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6451, - "name": "Slendytubbies", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6452, - "name": "Serious Sam Classic: The First Encounter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6453, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6454, - "name": "Stories: The Path of Destinies", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6455, - "name": "Tasty Planet: Back for Seconds", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6456, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6457, - "name": "Paint the Town Red", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 6458, - "name": "Total Overdose", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6459, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action" - ] - }, - { - "id": 6460, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6461, - "name": "Slime Rancher", - "site": "playground_ru", - "genres": [ - "Открытый мир", - "От первого лица", - "Инди" - ] - }, - { - "id": 6462, - "name": "Murdered: Soul Suspect", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6463, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6464, - "name": "Serious Sam Classic: The Second Encounter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6465, - "name": "Street Fighter X Tekken", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 6466, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6467, - "name": "Tekken 7", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6468, - "name": "Transistor", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6469, - "name": "Spooky's House of Jump Scares", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6470, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6472, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6473, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6474, - "name": "Sniper Elite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стелс", - "Шутер" - ] - }, - { - "id": 6475, - "name": "Shadow Warrior", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6476, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6477, - "name": "Saints Row IV", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6478, - "name": "Trine", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6479, - "name": "Panty Party", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 6480, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6481, - "name": "Stupidella", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6482, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6483, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6484, - "name": "Sniper Elite 3", - "site": "playground_ru", - "genres": [ - "Стелс", - "Мультиплеер", - "Кооператив", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 6485, - "name": "Shadow of the Tomb Raider", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 6486, - "name": "Tesla vs Lovecraft", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "инди" - ] - }, - { - "id": 6487, - "name": "Star Wars: The Force Unleashed", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 6488, - "name": "Trine 2", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6489, - "name": "Saints Row: Gat out of Hell", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6490, - "name": "Sudeki", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6491, - "name": "Papers, Please", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6492, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6493, - "name": "Star Wars: The Force Unleashed II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6494, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6495, - "name": "Trine 3: The Artifacts of Power", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6496, - "name": "Teslagrad", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "инди", - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 6497, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6498, - "name": "Salt and Sanctuary", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Платформеры" - ] - }, - { - "id": 6499, - "name": "Sniper Elite V2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Стелс", - "Шутер" - ] - }, - { - "id": 6500, - "name": "Need for Speed: Underground", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 6501, - "name": "Superhot", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 6502, - "name": "SteamWorld Dig", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6503, - "name": "Troll Tale", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6504, - "name": "The Banner Saga", - "site": "gamer_info_com", - "genres": [ - "RPG", - "TBS" - ] - }, - { - "id": 6505, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6506, - "name": "Sophie's Curse", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6507, - "name": "Surgeon Simulator", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6508, - "name": "Shantae: Half-Genie Hero", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6509, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6510, - "name": "SteamWorld Dig 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6511, - "name": "Samsara Room", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 6512, - "name": "Peace, Death!", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 6513, - "name": "Never Alone", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6514, - "name": "Trollface Quest", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6515, - "name": "The Banner Saga 2", - "site": "gamer_info_com", - "genres": [ - "RPG", - "TBS" - ] - }, - { - "id": 6516, - "name": "SteamWorld Heist", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 6517, - "name": "South Park: The Fractured But Whole", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Юмор" - ] - }, - { - "id": 6518, - "name": "Sacred", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6519, - "name": "Tales from the Borderlands", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6520, - "name": "Shoot Your Nightmare Halloween Special", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6521, - "name": "Need for Speed: Underground", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6522, - "name": "Trollface Quest 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6523, - "name": "Saw: The Video Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6524, - "name": "Steampunk Tower 2", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия", - "Инди" - ] - }, - { - "id": 6525, - "name": "Neverending Nightmares", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6526, - "name": "Pillars of Eternity", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6527, - "name": "The Bard’s Tale (2004)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6528, - "name": "Tales of Berseria", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6529, - "name": "Trollface Quest 3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6530, - "name": "Sacred 3", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6531, - "name": "Steel Rats", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 6532, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6533, - "name": "Shovel Knight: Shovel of Hope", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6534, - "name": "Scratches", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6535, - "name": "Never Alone", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6536, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6537, - "name": "The Beginner's Guide", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 6538, - "name": "Stories: The Path of Destinies", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6539, - "name": "Trollface Quest 4", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6540, - "name": "Tales of Zestiria", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6541, - "name": "Sacred Citadel", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6542, - "name": "Shrek 2: The Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6543, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6544, - "name": "Seasons after Fall", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 6545, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6546, - "name": "Street Fighter X Tekken", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 6547, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6548, - "name": "Trollface Quest 5", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6549, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6550, - "name": "Neverending Nightmares", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6551, - "name": "Nevermind", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6552, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6554, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6555, - "name": "Serena", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 6556, - "name": "Silence on The Line", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6557, - "name": "Tasty Planet: Back for Seconds", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6558, - "name": "Trollface Quest 6", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6559, - "name": "South Park: The Stick of Truth", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Юмор" - ] - }, - { - "id": 6560, - "name": "Stupidella", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Квест" - ] - }, - { - "id": 6561, - "name": "The Binding of Isaac: Rebirth", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 6562, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6563, - "name": "Trollface Quest 7", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6564, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6565, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6566, - "name": "Tekken 7", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 6567, - "name": "Nevermind", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6568, - "name": "Silent Hill: Alchemilla", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6569, - "name": "Sudeki", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6570, - "name": "The Bunker", - "site": "gamer_info_com", - "genres": [ - "инди", - "квесты" - ] - }, - { - "id": 6571, - "name": "Serious Sam 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6572, - "name": "Pinstripe", - "site": "gamespot_com", - "genres": [ - "Miscellaneous" - ] - }, - { - "id": 6573, - "name": "Trollface Quest: Video Memes", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6574, - "name": "Superhot", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Шутер" - ] - }, - { - "id": 6575, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6576, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6577, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6578, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6579, - "name": "Silverfall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6580, - "name": "Twin Sector", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6581, - "name": "The Cat Lady", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6582, - "name": "Neverwinter Nights", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6583, - "name": "Surgeon Simulator", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6584, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6585, - "name": "Saints Row IV", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 6586, - "name": "Serious Sam 3: BFE", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6587, - "name": "Tesla vs Lovecraft", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 6588, - "name": "Neverwinter Nights", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6589, - "name": "Silverfall: Earth Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6590, - "name": "Tales from the Borderlands", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6591, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6592, - "name": "The Council", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6593, - "name": "Two Worlds", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6594, - "name": "Neverwinter Nights 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6595, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6596, - "name": "Tales of Berseria", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 6597, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6598, - "name": "Teslagrad", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 6599, - "name": "Pizza Delivery 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6600, - "name": "Singularity", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6601, - "name": "Neverwinter Nights 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6602, - "name": "Saints Row: Gat out of Hell", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6603, - "name": "Typical Nightmare", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6604, - "name": "The Curse of Blackwater", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6605, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6606, - "name": "Tales of Zestiria", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6607, - "name": "NieR: Automata", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 6608, - "name": "The Banner Saga", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 6609, - "name": "Ultimate Epic Battle Simulator", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 6610, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6611, - "name": "Slap-Man", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6612, - "name": "Serious Sam Classic: The First Encounter", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6613, - "name": "Salt and Sanctuary", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6614, - "name": "Plague Inc: Evolved", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 6615, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6616, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6617, - "name": "Nightmare House 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6618, - "name": "NieR: Automata", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6619, - "name": "The Banner Saga 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 6620, - "name": "Ultra Street Fighter IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6621, - "name": "Tasty Planet: Back for Seconds", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6622, - "name": "Slay the Spire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6623, - "name": "The Darkness II", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 6624, - "name": "Serious Sam Classic: The Second Encounter", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6625, - "name": "Samsara Room", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6626, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6627, - "name": "Plants vs. Zombies", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 6628, - "name": "The Bard’s Tale (2004)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6629, - "name": "Undertale", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6630, - "name": "Tekken 7", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 6631, - "name": "Nosferatu: The Wrath of Malachi", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6632, - "name": "Nightmare House 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6633, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6634, - "name": "Shadow Warrior", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6635, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6636, - "name": "Sleeping Dogs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6637, - "name": "Saw: The Video Game", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6638, - "name": "Unfair Mario", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6639, - "name": "The Beginner's Guide", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6640, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6641, - "name": "Nox Timore", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6642, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 6643, - "name": "Tesla vs Lovecraft", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Инди" - ] - }, - { - "id": 6644, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6645, - "name": "Shadow of the Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6646, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6647, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6648, - "name": "Slender: The Arrival", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6649, - "name": "Nosferatu: The Wrath of Malachi", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6650, - "name": "Scratches", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6651, - "name": "Unmechanical", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 6652, - "name": "Teslagrad", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6653, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6654, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6655, - "name": "The Binding of Isaac: Rebirth", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Ролевые игры" - ] - }, - { - "id": 6656, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6657, - "name": "Slendytubbies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6658, - "name": "Unmechanical: Extended (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6659, - "name": "The Banner Saga", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 6660, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6661, - "name": "Pony Island", - "site": "gamespot_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 6662, - "name": "Nox Timore", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6663, - "name": "Seasons after Fall", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6664, - "name": "Omensight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6665, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6666, - "name": "The Bunker", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6667, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6668, - "name": "Until the last", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6669, - "name": "The Banner Saga 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 6670, - "name": "Slime Rancher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6671, - "name": "Shantae: Half-Genie Hero", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Приключения" - ] - }, - { - "id": 6672, - "name": "The Cat Lady", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6673, - "name": "Serena", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6674, - "name": "The Bard’s Tale (2004)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6675, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6676, - "name": "One Hand Clapping (DEMO)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6677, - "name": "Portal", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6678, - "name": "Valiant Hearts: The Great War", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6679, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6680, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 6681, - "name": "Sniper Elite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6682, - "name": "Shoot Your Nightmare Halloween Special", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6683, - "name": "The Beginner's Guide", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6684, - "name": "The Council", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6685, - "name": "Spooky's House of Jump Scares", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6686, - "name": "Vanish", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6687, - "name": "Serious Sam 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6688, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6689, - "name": "One Night at Flumpty's", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6690, - "name": "Omensight", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6691, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "RPG" - ] - }, - { - "id": 6692, - "name": "Portal 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6693, - "name": "The Curse of Blackwater", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6694, - "name": "Shovel Knight: Shovel of Hope", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6695, - "name": "Sniper Elite 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6696, - "name": "Victor Vran", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6697, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "playground_ru", - "genres": [ - "Слэшер", - "От третьего лица", - "Научная фантастика", - "Ролевая", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 6698, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6699, - "name": "The Binding of Isaac: Rebirth", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "RPG" - ] - }, - { - "id": 6700, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6701, - "name": "Serious Sam 3: BFE", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6702, - "name": "One Night at Flumpty's 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6703, - "name": "One Hand Clapping (DEMO)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6704, - "name": "Sniper Elite V2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6705, - "name": "Shrek 2: The Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6706, - "name": "Prey (2006)", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6707, - "name": "Vikings: Wolves of Midgard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6708, - "name": "The Bunker", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6709, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6710, - "name": "Star Wars: The Force Unleashed", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6711, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6712, - "name": "The Cat Lady", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6713, - "name": "Silence on The Line", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6714, - "name": "One Piece: Burning Blood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6715, - "name": "Prince of Persia (2008)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6716, - "name": "Visage", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6717, - "name": "Sophie's Curse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6718, - "name": "One Night at Flumpty's", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6719, - "name": "The Darkness II", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 6720, - "name": "Star Wars: The Force Unleashed II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6721, - "name": "The Evil Within", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6722, - "name": "The Council", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6723, - "name": "Wanted: Weapons of Fate", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6724, - "name": "Serious Sam Classic: The First Encounter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6725, - "name": "Silent Hill: Alchemilla", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6726, - "name": "South Park: The Fractured But Whole", - "site": "iwantgames_ru", - "genres": [ - "РПГ", - "Приключения" - ] - }, - { - "id": 6727, - "name": "One Night at Flumpty's 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6728, - "name": "Orcs Must Die!", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 6729, - "name": "Prince of Persia: The Two Thrones", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6730, - "name": "The Curse of Blackwater", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6731, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 6732, - "name": "The Evil Within 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения" - ] - }, - { - "id": 6733, - "name": "SteamWorld Dig", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Аркада", - "Постапокалипсис", - "Вестерн" - ] - }, - { - "id": 6734, - "name": "Warcraft III: Reign of Chaos", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6735, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6736, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6737, - "name": "Silverfall", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 6738, - "name": "Serious Sam Classic: The Second Encounter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6739, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6740, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6741, - "name": "Warcraft III: The Frozen Throne", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6742, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6743, - "name": "The Darkness II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6744, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6745, - "name": "SteamWorld Dig 2", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Вестерн", - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 6746, - "name": "One Piece: Burning Blood", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6747, - "name": "Silverfall: Earth Awakening", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6748, - "name": "Prospekt", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6749, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6750, - "name": "Warhammer 40.000: Space Marine", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6751, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6752, - "name": "Shadow Warrior", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6753, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6754, - "name": "SteamWorld Heist", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6755, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6756, - "name": "Ori and the Blind Forest", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6757, - "name": "Warhammer: Chaosbane", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 6758, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6759, - "name": "South Park: The Stick of Truth", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6760, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6761, - "name": "Shadow of the Tomb Raider", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6762, - "name": "Singularity", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6763, - "name": "Orcs Must Die!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 6764, - "name": "Prototype", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6765, - "name": "Steampunk Tower 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6766, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6767, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6768, - "name": "We Happy Few", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6769, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6770, - "name": "The Expendabros", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6771, - "name": "Slap-Man", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6772, - "name": "The Elder Scrolls V: Skyrim", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6773, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6774, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6775, - "name": "Outlast", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6776, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6777, - "name": "Steel Rats", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Мотоциклы", - "Аркада", - "Экшен", - "Платформер" - ] - }, - { - "id": 6778, - "name": "Whack The Thief", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6779, - "name": "Prototype 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6780, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6781, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6782, - "name": "The Final Station", - "site": "gamer_info_com", - "genres": [ - "survival" - ] - }, - { - "id": 6783, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6784, - "name": "Slay the Spire", - "site": "ag_ru", - "genres": [ - "Стратегии", - "Инди", - "Карточные" - ] - }, - { - "id": 6785, - "name": "Shantae: Half-Genie Hero", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6786, - "name": "Whack Your Boss", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6787, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6788, - "name": "Stories: The Path of Destinies", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 6789, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6790, - "name": "Ori and the Blind Forest", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6791, - "name": "The First Tree", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 6792, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6793, - "name": "Outlast: Whistleblower", - "site": "mobygames_com", - "genres": [ - "Action", - "DLC / add-on" - ] - }, - { - "id": 6794, - "name": "Whack Your Ex", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6795, - "name": "Sleeping Dogs", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6796, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6797, - "name": "Shoot Your Nightmare Halloween Special", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6798, - "name": "Psychonauts", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 6799, - "name": "Street Fighter X Tekken", - "site": "playground_ru", - "genres": [ - "Аркада", - "Файтинг" - ] - }, - { - "id": 6800, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6801, - "name": "The Forest", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "survival" - ] - }, - { - "id": 6802, - "name": "Whack Your Neighbour", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6803, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6804, - "name": "The Evil Within", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6805, - "name": "Slender: The Arrival", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6806, - "name": "Overlord", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6808, - "name": "Outlast", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6809, - "name": "The Evil Within", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6810, - "name": "Shovel Knight: Shovel of Hope", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6811, - "name": "Whack Your Teacher", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6812, - "name": "The Hat Man Shadow Ward", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 6813, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6814, - "name": "PuniTy", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6815, - "name": "The Evil Within 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6816, - "name": "The Evil Within 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 6817, - "name": "Stupidella", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6818, - "name": "Slendytubbies", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6819, - "name": "Outlast: Whistleblower", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6820, - "name": "What Remains of Edith Finch", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6821, - "name": "Shrek 2: The Game", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6822, - "name": "Overlord: Raising Hell", - "site": "mobygames_com", - "genres": [ - "Compilation", - "DLC / add-on" - ] - }, - { - "id": 6823, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6824, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6825, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6826, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6827, - "name": "P·O·L·L·E·N", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6828, - "name": "Sudeki", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 6829, - "name": "Will Rock", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6830, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6831, - "name": "Slime Rancher", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 6832, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6833, - "name": "Oxenfree", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6834, - "name": "Silence on The Line", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6835, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6836, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6837, - "name": "Overlord", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6838, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6839, - "name": "Willy-Nilly Knight", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 6840, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6841, - "name": "Superhot", - "site": "playground_ru", - "genres": [ - "Виртуальная реальность", - "От первого лица", - "Инди", - "Шутер" - ] - }, - { - "id": 6842, - "name": "Sniper Elite", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6843, - "name": "Quake 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6844, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 6845, - "name": "Silent Hill: Alchemilla", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6846, - "name": "The Expendabros", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6847, - "name": "Wizard of Legend", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6848, - "name": "Surgeon Simulator", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6849, - "name": "Painkiller", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6850, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6851, - "name": "The Expendabros", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6852, - "name": "Sniper Elite 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 6853, - "name": "The Final Station", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6854, - "name": "The Last Remnant", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6855, - "name": "Quantum Break", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 6856, - "name": "Overlord: Raising Hell", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6857, - "name": "Silverfall", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6858, - "name": "Wolfenstein", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6859, - "name": "Tales from the Borderlands", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 6860, - "name": "The Final Station", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 6861, - "name": "The First Tree", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6862, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6863, - "name": "Sniper Elite V2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6864, - "name": "Painkiller: Battle Out of Hell", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6865, - "name": "The Mask Man", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6866, - "name": "Wolfenstein: The New Order", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6867, - "name": "Silverfall: Earth Awakening", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 6868, - "name": "The Forest", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Инди" - ] - }, - { - "id": 6869, - "name": "The First Tree", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6870, - "name": "Tales of Berseria", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 6871, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6872, - "name": "The Punisher (2005)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6873, - "name": "Oxenfree", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6874, - "name": "Wolfenstein: The Old Blood", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6875, - "name": "Sophie's Curse", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 6876, - "name": "The Hat Man Shadow Ward", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6877, - "name": "Painkiller: Overdose", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6878, - "name": "Raft", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 6879, - "name": "Singularity", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6880, - "name": "The Incredible Adventures of Van Helsing", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 6881, - "name": "Tales of Zestiria", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 6882, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6883, - "name": "Woolfe - The Red Hood Diaries", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6884, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 6885, - "name": "Painkiller", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6886, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6887, - "name": "Rage", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6888, - "name": "Slap-Man", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6889, - "name": "Worms 2", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 6890, - "name": "The Slippery Slope", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6891, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6892, - "name": "The Forest", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия" - ] - }, - { - "id": 6893, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6894, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6895, - "name": "Paint the Town Red", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6896, - "name": "Painkiller: Battle Out of Hell", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6897, - "name": "South Park: The Fractured But Whole", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 6898, - "name": "The Hat Man Shadow Ward", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6899, - "name": "X-Blades / Ониблэйд", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6900, - "name": "Tasty Planet: Back for Seconds", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6901, - "name": "The Last Remnant", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6902, - "name": "The Stanley Parable", - "site": "gamer_info_com", - "genres": [ - "визуальные новеллы", - "инди", - "приключения" - ] - }, - { - "id": 6903, - "name": "Slay the Spire", - "site": "stopgame_ru", - "genres": [ - "cards" - ] - }, - { - "id": 6904, - "name": "Spooky's House of Jump Scares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6905, - "name": "X-Morph: Defense", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия" - ] - }, - { - "id": 6906, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6907, - "name": "Panty Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6908, - "name": "The Mask Man", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6909, - "name": "Rampage Knights", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 6910, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6911, - "name": "The Walking Dead: A New Frontier", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6912, - "name": "Tekken 7", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Файтинг", - "Аркада", - "Экшен", - "Виртуальная реальность" - ] - }, - { - "id": 6913, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6914, - "name": "Sleeping Dogs", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 6915, - "name": "Painkiller: Overdose", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6916, - "name": "The Punisher (2005)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6917, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6918, - "name": "Yandere School", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6919, - "name": "The Walking Dead: Michonne", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6920, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6921, - "name": "Papers, Please", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Puzzle" - ] - }, - { - "id": 6922, - "name": "Rayman Legends", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6923, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6924, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6925, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6926, - "name": "Star Wars: The Force Unleashed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6927, - "name": "Year Walk", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 6928, - "name": "Slender: The Arrival", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6929, - "name": "The Slippery Slope", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6930, - "name": "The Walking Dead: Season One", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6931, - "name": "Paint the Town Red", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Action", - "Indie" - ] - }, - { - "id": 6932, - "name": "The Last Remnant", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 6933, - "name": "South Park: The Stick of Truth", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6934, - "name": "Tesla vs Lovecraft", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Шутер", - "Вид сверху" - ] - }, - { - "id": 6935, - "name": "Rayman Origins", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6936, - "name": "Peace, Death!", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 6937, - "name": "Star Wars: The Force Unleashed II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6938, - "name": "The Stanley Parable", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6939, - "name": "Slendytubbies", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6940, - "name": "ZOMBI", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6941, - "name": "The Walking Dead: Season Two", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6942, - "name": "The Mask Man", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6943, - "name": "Teslagrad", - "site": "playground_ru", - "genres": [ - "Логические", - "Инди", - "Платформер" - ] - }, - { - "id": 6944, - "name": "The Walking Dead: A New Frontier", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6945, - "name": "SteamWorld Dig", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6946, - "name": "Zero Escape: Zero Time Dilemma", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6947, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6948, - "name": "Slime Rancher", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6949, - "name": "The Punisher (2005)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6950, - "name": "The Witcher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6951, - "name": "Panty Party", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 6952, - "name": "The Walking Dead: Michonne", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6953, - "name": "Pillars of Eternity", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6954, - "name": "Rayman Raving Rabbids", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 6955, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6956, - "name": "The Banner Saga", - "site": "playground_ru", - "genres": [ - "Тактика", - "Ролевая", - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6957, - "name": "Ziggurat", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6958, - "name": "SteamWorld Dig 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6959, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6960, - "name": "The Walking Dead: Season One", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6961, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6962, - "name": "Sniper Elite", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6963, - "name": "The Slippery Slope", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6964, - "name": "Zombie Driver HD", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6965, - "name": "The Walking Dead: Season Two", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6966, - "name": "Papers, Please", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6967, - "name": "The Witcher 3: Wild Hunt", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6968, - "name": "Rayman Raving Rabbids 2", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 6969, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6970, - "name": "SteamWorld Heist", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Стратегии" - ] - }, - { - "id": 6971, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6972, - "name": "The Banner Saga 2", - "site": "playground_ru", - "genres": [ - "Инди", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 6973, - "name": "The Stanley Parable", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6974, - "name": "The Witcher", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6975, - "name": "Zombie Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6976, - "name": "Sniper Elite 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6977, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6978, - "name": "Steampunk Tower 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6979, - "name": "The Bard’s Tale (2004)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6980, - "name": "The Witcher 2: Assassins of Kings", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6981, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6982, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6983, - "name": "The Walking Dead: A New Frontier", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6984, - "name": "Zombie Vikings", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6985, - "name": "Peace, Death!", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 6986, - "name": "Sniper Elite V2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6987, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6988, - "name": "Real Horror Stories", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6989, - "name": "The Witcher 3: Wild Hunt", - "site": "gameguru_ru", - "genres": [ - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 6990, - "name": "Steel Rats", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6991, - "name": "Zuma Deluxe", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 6992, - "name": "The Walking Dead: Michonne", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6993, - "name": "The Beginner's Guide", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Юмор" - ] - }, - { - "id": 6994, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6995, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 6996, - "name": "Sophie's Curse", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6997, - "name": "The Wolf Among Us", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6998, - "name": "The Walking Dead: Season One", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6999, - "name": "Stories: The Path of Destinies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7000, - "name": "Pillars of Eternity", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7001, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 7002, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7003, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7004, - "name": "Zuma's Revenge!", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7005, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 7006, - "name": "Redeemer", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7007, - "name": "Thief", - "site": "gamer_info_com", - "genres": [ - "stealth" - ] - }, - { - "id": 7008, - "name": "South Park: The Fractured But Whole", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7009, - "name": "The Wolf Among Us", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7010, - "name": "Street Fighter X Tekken", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7011, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7012, - "name": "The Binding of Isaac: Rebirth", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7013, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7014, - "name": "The Walking Dead: Season Two", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7015, - "name": "This Is The Police", - "site": "gamer_info_com", - "genres": [ - "инди", - "RTS" - ] - }, - { - "id": 7016, - "name": "Thief", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стелс" - ] - }, - { - "id": 7017, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7018, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7019, - "name": "Reign Of Kings", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7021, - "name": "Pinstripe", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7022, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7023, - "name": "This Is The Police", - "site": "gameguru_ru", - "genres": [ - "Стратегия", - "Менеджемент", - "Квест" - ] - }, - { - "id": 7024, - "name": "TimeShift", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7025, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7026, - "name": "The Bunker", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Кооператив", - "Ретро", - "Экшен" - ] - }, - { - "id": 7027, - "name": "The Witcher", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7028, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7029, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7030, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7031, - "name": "TimeShift", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7032, - "name": "Stupidella", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7033, - "name": "Pirates of the Caribbean: At World’s End", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7034, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7035, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7036, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7037, - "name": "Titan Quest", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7038, - "name": "The Cat Lady", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Квест" - ] - }, - { - "id": 7039, - "name": "Titan Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7040, - "name": "Reigns", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7041, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7042, - "name": "South Park: The Stick of Truth", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7043, - "name": "Sudeki", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7044, - "name": "Pizza Delivery 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7045, - "name": "Titan Quest Anniversary Edition", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7046, - "name": "Невероятные Приключения Трояна", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7047, - "name": "Titan Quest Anniversary Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7048, - "name": "The Council", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7049, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7050, - "name": "Reigns: Her Majesty", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7051, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7052, - "name": "Pinstripe", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 7053, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7054, - "name": "Невероятные Приключения Трояна 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7055, - "name": "Superhot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7056, - "name": "Plague Inc: Evolved", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 7057, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7058, - "name": "The Curse of Blackwater", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7059, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7060, - "name": "Невероятные Приключения Трояна 3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7061, - "name": "Pirates of the Caribbean: At World’s End", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7062, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 7063, - "name": "Surgeon Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7064, - "name": "Resident Evil", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7065, - "name": "Titan Souls", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 7066, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7067, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7068, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7069, - "name": "Предатор", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7070, - "name": "Plants vs. Zombies", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 7071, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7072, - "name": "Titanfall 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Роботы" - ] - }, - { - "id": 7073, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7074, - "name": "Pizza Delivery 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7075, - "name": "Tales from the Borderlands", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7076, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7077, - "name": "The Darkness II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7078, - "name": "The Witcher 3: Wild Hunt", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7079, - "name": "Titan Souls", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7080, - "name": "Стоп-Гоп", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7081, - "name": "To The Moon", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7082, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7083, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7084, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7085, - "name": "The Elder Scrolls IV: Oblivion", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7086, - "name": "Tales of Berseria", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7087, - "name": "Стоп-Гоп 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7088, - "name": "Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 7089, - "name": "Spooky's House of Jump Scares", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7090, - "name": "Titanfall 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7091, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7092, - "name": "Plague Inc: Evolved", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 7093, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7094, - "name": "Resident Evil 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 7095, - "name": "Torchlight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 7096, - "name": "Pony Island", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle", - "Adventure" - ] - }, - { - "id": 7097, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7098, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7099, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7100, - "name": "To The Moon", - "site": "gamer_info_com", - "genres": [ - "RPG", - "визуальные новеллы" - ] - }, - { - "id": 7101, - "name": "Tales of Zestiria", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 7102, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7103, - "name": "Torchlight II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7104, - "name": "Plants vs. Zombies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7105, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7106, - "name": "The Wolf Among Us", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7107, - "name": "Toren", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 7108, - "name": "Star Wars: The Force Unleashed", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7109, - "name": "Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7110, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7111, - "name": "Resident Evil 5", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7112, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7113, - "name": "The Elder Scrolls V: Skyrim", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7114, - "name": "Tormentum: Dark Sorrow", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 7115, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7116, - "name": "Portal", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 7117, - "name": "Star Wars: The Force Unleashed II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7118, - "name": "Torchlight", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7119, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7120, - "name": "Tasty Planet: Back for Seconds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7121, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7122, - "name": "Total Overdose", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7123, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7124, - "name": "Torchlight II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7125, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7126, - "name": "Transistor", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7127, - "name": "Thief", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 7128, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7129, - "name": "Tekken 7", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Аркады" - ] - }, - { - "id": 7130, - "name": "Pony Island", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 7131, - "name": "SteamWorld Dig", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 7132, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7133, - "name": "Trine", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 7134, - "name": "Toren", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "платформеры" - ] - }, - { - "id": 7135, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7136, - "name": "This Is The Police", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Приключения" - ] - }, - { - "id": 7137, - "name": "Portal 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 7138, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7139, - "name": "SteamWorld Dig 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Приключения" - ] - }, - { - "id": 7140, - "name": "Resident Evil 6", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7141, - "name": "Trine 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 7142, - "name": "Portal", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7143, - "name": "Tormentum: Dark Sorrow", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "квесты" - ] - }, - { - "id": 7144, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7145, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7146, - "name": "TimeShift", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7147, - "name": "Trine 3: The Artifacts of Power", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер" - ] - }, - { - "id": 7148, - "name": "SteamWorld Heist", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Платформеры", - "Стратегии" - ] - }, - { - "id": 7149, - "name": "Tesla vs Lovecraft", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7150, - "name": "Prey (2006)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7151, - "name": "Total Overdose", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7152, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7153, - "name": "Resident Evil 7: Biohazard", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "Survival", - "Adventure" - ] - }, - { - "id": 7154, - "name": "Troll Tale", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7155, - "name": "The Evil Within", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 7156, - "name": "Titan Quest", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7157, - "name": "Steampunk Tower 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 7158, - "name": "Teslagrad", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7159, - "name": "Portal 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7160, - "name": "Transistor", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 7161, - "name": "Trollface Quest", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7162, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7163, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7164, - "name": "Titan Quest Anniversary Edition", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7165, - "name": "The Evil Within 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 7166, - "name": "Steel Rats", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Симуляторы" - ] - }, - { - "id": 7167, - "name": "The Banner Saga", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7168, - "name": "Trollface Quest 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7169, - "name": "Prey (2006)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7170, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7171, - "name": "Trine", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 7172, - "name": "Prince of Persia (2008)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7173, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7174, - "name": "Trollface Quest 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7175, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7176, - "name": "The Evil Within: The Assignment (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7177, - "name": "The Banner Saga 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7178, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7179, - "name": "Prince of Persia (2008)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7180, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7181, - "name": "Trine 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7182, - "name": "Trollface Quest 4", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7183, - "name": "Stories: The Path of Destinies", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Платформеры" - ] - }, - { - "id": 7184, - "name": "Spooky's House of Jump Scares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7185, - "name": "The Evil Within: The Consequence (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7186, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7187, - "name": "The Bard’s Tale (2004)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7188, - "name": "Trollface Quest 5", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7189, - "name": "Prince of Persia: The Two Thrones", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7190, - "name": "Titan Souls", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7191, - "name": "Trine 3: The Artifacts of Power", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 7192, - "name": "Street Fighter X Tekken", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 7193, - "name": "Prince of Persia: The Two Thrones", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7194, - "name": "Trollface Quest 6", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7195, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 7196, - "name": "The Evil Within: The Executioner (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7197, - "name": "The Beginner's Guide", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7198, - "name": "Titanfall 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 7199, - "name": "Troll Tale", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7200, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 7201, - "name": "Trollface Quest 7", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7203, - "name": "Prospekt", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7204, - "name": "Star Wars: The Force Unleashed", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7205, - "name": "The Expendabros", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 7206, - "name": "Trollface Quest: Video Memes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7207, - "name": "To The Moon", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 7208, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7209, - "name": "Trollface Quest", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7210, - "name": "Prospekt", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7211, - "name": "Stupidella", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7212, - "name": "Resident Evil: Revelations", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7213, - "name": "Twin Sector", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7214, - "name": "Trollface Quest 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7215, - "name": "Star Wars: The Force Unleashed II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7216, - "name": "The Binding of Isaac: Rebirth", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 7217, - "name": "The Final Station", - "site": "playground_ru", - "genres": [ - "Ретро", - "Постапокалипсис", - "Выживание", - "Зомби", - "Платформер" - ] - }, - { - "id": 7218, - "name": "Prototype", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7219, - "name": "Two Worlds", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7220, - "name": "Sudeki", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7221, - "name": "Trollface Quest 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7222, - "name": "Prototype", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7223, - "name": "Typical Nightmare", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7224, - "name": "The First Tree", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди" - ] - }, - { - "id": 7225, - "name": "The Bunker", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7226, - "name": "SteamWorld Dig", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7227, - "name": "Resident Evil: Revelations 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 7228, - "name": "Prototype 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7229, - "name": "Ultimate Epic Battle Simulator", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 7230, - "name": "Trollface Quest 4", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7231, - "name": "Superhot", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены" - ] - }, - { - "id": 7232, - "name": "The Cat Lady", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7233, - "name": "SteamWorld Dig 2", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7234, - "name": "The Forest", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Выживание", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 7235, - "name": "Ultra Street Fighter IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7236, - "name": "Prototype 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7237, - "name": "Trollface Quest 5", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7238, - "name": "Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 7239, - "name": "Psychonauts", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7240, - "name": "Undertale", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7241, - "name": "Surgeon Simulator", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Экшены", - "Симуляторы", - "Образовательные" - ] - }, - { - "id": 7242, - "name": "The Hat Man Shadow Ward", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7243, - "name": "The Council", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7244, - "name": "Reus", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 7245, - "name": "SteamWorld Heist", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7246, - "name": "Trollface Quest 6", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7247, - "name": "Torchlight", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7248, - "name": "Unfair Mario", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7249, - "name": "PuniTy", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7250, - "name": "Tales from the Borderlands", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7251, - "name": "Psychonauts", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7252, - "name": "The Curse of Blackwater", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7253, - "name": "The Incredible Adventures of Van Helsing", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "Ролевая" - ] - }, - { - "id": 7254, - "name": "Unmechanical", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 7255, - "name": "Steampunk Tower 2", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7256, - "name": "Trollface Quest 7", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7257, - "name": "Torchlight II", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7258, - "name": "P·O·L·L·E·N", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7259, - "name": "The Incredible Adventures of Van Helsing II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7260, - "name": "Unmechanical: Extended (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7261, - "name": "Tales of Berseria", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7262, - "name": "PuniTy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7263, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7264, - "name": "Steel Rats", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7265, - "name": "Trollface Quest: Video Memes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7266, - "name": "Until the last", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7267, - "name": "The Incredible Adventures of Van Helsing III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7268, - "name": "Toren", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7269, - "name": "Rezrog", - "site": "gamespot_com", - "genres": [ - "Roguelike", - "Role-Playing" - ] - }, - { - "id": 7270, - "name": "Quake 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7271, - "name": "Tales of Zestiria", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7272, - "name": "The Darkness II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7273, - "name": "Valiant Hearts: The Great War", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Квест" - ] - }, - { - "id": 7274, - "name": "Tormentum: Dark Sorrow", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7275, - "name": "Twin Sector", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 7276, - "name": "Stories: The Path of Destinies", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7277, - "name": "The Last Remnant", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7278, - "name": "P·O·L·L·E·N", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 7279, - "name": "Vanish", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7280, - "name": "The Elder Scrolls IV: Oblivion", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7281, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7282, - "name": "Total Overdose", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7283, - "name": "Two Worlds", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7284, - "name": "Rise of the Tomb Raider", - "site": "gamespot_com", - "genres": [ - "VR", - "Action", - "Adventure" - ] - }, - { - "id": 7285, - "name": "The Mask Man", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7286, - "name": "Quantum Break", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7287, - "name": "Victor Vran", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 7288, - "name": "Quake 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7289, - "name": "Street Fighter X Tekken", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7290, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7291, - "name": "Tasty Planet: Back for Seconds", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Семейные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 7292, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7293, - "name": "Transistor", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7294, - "name": "The Punisher (2005)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7295, - "name": "Typical Nightmare", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7296, - "name": "Vikings: Wolves of Midgard", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7297, - "name": "Raft", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7299, - "name": "Visage", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7300, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7301, - "name": "Tekken 7", - "site": "ag_ru", - "genres": [ - "Экшены", - "Спортивные", - "Файтинги" - ] - }, - { - "id": 7302, - "name": "Trine", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7303, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7304, - "name": "Ultimate Epic Battle Simulator", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7305, - "name": "Quantum Break", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7306, - "name": "The Secret of Monkey Island: Special Edition", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Юмор", - "Квест" - ] - }, - { - "id": 7307, - "name": "Wanted: Weapons of Fate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7308, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7309, - "name": "Stupidella", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7310, - "name": "The Slippery Slope", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7311, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7312, - "name": "Trine 2", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7313, - "name": "Ultra Street Fighter IV", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7314, - "name": "Rage", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7315, - "name": "The Elder Scrolls V: Skyrim", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7316, - "name": "Warcraft III: Reign of Chaos", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 7317, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7318, - "name": "Raft", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 7319, - "name": "Warcraft III: The Frozen Throne", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 7320, - "name": "Undertale", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 7321, - "name": "Trine 3: The Artifacts of Power", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7322, - "name": "The Stanley Parable", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Юмор", - "От первого лица" - ] - }, - { - "id": 7323, - "name": "Sudeki", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7324, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7325, - "name": "Rampage Knights", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7326, - "name": "Tesla vs Lovecraft", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7327, - "name": "Warhammer 40.000: Space Marine", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7328, - "name": "Troll Tale", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7329, - "name": "Unfair Mario", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7330, - "name": "Superhot", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7331, - "name": "The Walking Dead: A New Frontier", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Зомби" - ] - }, - { - "id": 7332, - "name": "Teslagrad", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Семейные", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 7333, - "name": "Warhammer: Chaosbane", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7334, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7335, - "name": "Trollface Quest", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7336, - "name": "Risen", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7337, - "name": "Rage", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7338, - "name": "Rayman Legends", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7339, - "name": "Unmechanical", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические", - "инди" - ] - }, - { - "id": 7340, - "name": "We Happy Few", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7341, - "name": "Trollface Quest 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7342, - "name": "The Banner Saga", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 7343, - "name": "Surgeon Simulator", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7344, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7345, - "name": "The Walking Dead: Michonne", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7346, - "name": "Rock of Ages", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 7347, - "name": "Whack The Thief", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7348, - "name": "Trollface Quest 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7349, - "name": "Unmechanical: Extended (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7350, - "name": "Rayman Origins", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7351, - "name": "The Banner Saga 2", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 7352, - "name": "Rampage Knights", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 7353, - "name": "Trollface Quest 4", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7354, - "name": "The Walking Dead: Season One", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7355, - "name": "Whack Your Boss", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7356, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7357, - "name": "Tales from the Borderlands", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7358, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 7359, - "name": "Until the last", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7360, - "name": "The Bard’s Tale (2004)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7361, - "name": "Trollface Quest 5", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7362, - "name": "Whack Your Ex", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7363, - "name": "The Walking Dead: Season Two", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7364, - "name": "Rayman Raving Rabbids", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7365, - "name": "The Evil Within", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 7366, - "name": "Tales of Berseria", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7367, - "name": "Rayman Legends", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7368, - "name": "Valiant Hearts: The Great War", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7369, - "name": "Whack Your Neighbour", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7370, - "name": "Trollface Quest 6", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7371, - "name": "The Beginner's Guide", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7372, - "name": "The Witcher", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 7373, - "name": "The Evil Within 2", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 7374, - "name": "Whack Your Teacher", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7375, - "name": "Rayman Raving Rabbids 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7376, - "name": "Tales of Zestiria", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7377, - "name": "Trollface Quest 7", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7378, - "name": "Vanish", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7379, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7380, - "name": "Root Of Evil: The Tailor", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7381, - "name": "Rayman Origins", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7382, - "name": "What Remains of Edith Finch", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7383, - "name": "Trollface Quest: Video Memes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7384, - "name": "The Witcher 2: Assassins of Kings", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 7385, - "name": "Real Horror Stories", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7386, - "name": "The Evil Within: The Assignment (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7387, - "name": "Victor Vran", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 7388, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7389, - "name": "Will Rock", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7390, - "name": "The Binding of Isaac: Rebirth", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7391, - "name": "Twin Sector", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл" - ] - }, - { - "id": 7392, - "name": "Redeemer", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7393, - "name": "Vikings: Wolves of Midgard", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7394, - "name": "The Evil Within: The Consequence (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7395, - "name": "Willy-Nilly Knight", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 7396, - "name": "Rayman Raving Rabbids", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7397, - "name": "The Witcher 3: Wild Hunt", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 7398, - "name": "Tasty Planet: Back for Seconds", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7399, - "name": "Runic Rampage", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7400, - "name": "The Bunker", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 7401, - "name": "Wizard of Legend", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 7402, - "name": "Two Worlds", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7403, - "name": "Visage", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 7404, - "name": "The Evil Within: The Executioner (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7405, - "name": "Reign Of Kings", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7406, - "name": "Tekken 7", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7407, - "name": "Rayman Raving Rabbids 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7408, - "name": "Wolfenstein", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7409, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7410, - "name": "Typical Nightmare", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7411, - "name": "Wanted: Weapons of Fate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7412, - "name": "The Expendabros", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7413, - "name": "Wolfenstein: The New Order", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7414, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7415, - "name": "The Cat Lady", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7416, - "name": "Ultimate Epic Battle Simulator", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7417, - "name": "Real Horror Stories", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7418, - "name": "Reigns", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7419, - "name": "Rust", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7420, - "name": "Wolfenstein: The Old Blood", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон", - "Самостоятельный" - ] - }, - { - "id": 7421, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7422, - "name": "Warcraft III: Reign of Chaos", - "site": "gamer_info_com", - "genres": [ - "RTS" - ] - }, - { - "id": 7423, - "name": "The Final Station", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7424, - "name": "Tesla vs Lovecraft", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7425, - "name": "Ultra Street Fighter IV", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 7426, - "name": "Woolfe - The Red Hood Diaries", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер" - ] - }, - { - "id": 7427, - "name": "The Council", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 7428, - "name": "Redeemer", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7429, - "name": "Reigns: Her Majesty", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7430, - "name": "Warcraft III: The Frozen Throne", - "site": "gamer_info_com", - "genres": [ - "RTS" - ] - }, - { - "id": 7431, - "name": "The Wolf Among Us", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7432, - "name": "Ryse: Son of Rome", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Adventure" - ] - }, - { - "id": 7433, - "name": "The First Tree", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7434, - "name": "Worms 2", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 7435, - "name": "Undertale", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 7436, - "name": "Teslagrad", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7437, - "name": "The Curse of Blackwater", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7438, - "name": "Thief", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7439, - "name": "Warhammer 40.000: Space Marine", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7440, - "name": "X-Blades / Ониблэйд", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7441, - "name": "Resident Evil", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7442, - "name": "Unfair Mario", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7443, - "name": "Reign Of Kings", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 7444, - "name": "The Forest", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен", - "Приключения", - "Симуляторы" - ] - }, - { - "id": 7445, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7446, - "name": "The Banner Saga", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 7447, - "name": "X-Morph: Defense", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Стратегия", - "В реальном времени" - ] - }, - { - "id": 7448, - "name": "Warhammer: Chaosbane", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7449, - "name": "This Is The Police", - "site": "playground_ru", - "genres": [ - "Пошаговая", - "Адвенчура", - "Стратегия", - "Менеджер" - ] - }, - { - "id": 7450, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7451, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7452, - "name": "Unmechanical", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7453, - "name": "The Hat Man Shadow Ward", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7454, - "name": "Yandere School", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7455, - "name": "The Banner Saga 2", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 7456, - "name": "We Happy Few", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7457, - "name": "Unmechanical: Extended (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7458, - "name": "Reigns", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 7459, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7460, - "name": "SCP-087", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7461, - "name": "TimeShift", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 7462, - "name": "Year Walk", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 7463, - "name": "The Darkness II", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7464, - "name": "The Incredible Adventures of Van Helsing", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7465, - "name": "Until the last", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7466, - "name": "Whack The Thief", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7467, - "name": "ZOMBI", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7468, - "name": "The Bard’s Tale (2004)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7469, - "name": "SCP-087-B", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7470, - "name": "Titan Quest", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 7471, - "name": "Resident Evil 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7472, - "name": "The Incredible Adventures of Van Helsing II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7473, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gameguru_ru", - "genres": [ - "Визуальный роман", - "Квест" - ] - }, - { - "id": 7474, - "name": "Reigns: Her Majesty", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 7475, - "name": "Whack Your Boss", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7476, - "name": "The Elder Scrolls IV: Oblivion", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7477, - "name": "Valiant Hearts: The Great War", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7478, - "name": "The Beginner's Guide", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7479, - "name": "Titan Quest Anniversary Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7480, - "name": "Ziggurat", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7481, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7482, - "name": "The Incredible Adventures of Van Helsing III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7483, - "name": "Resident Evil 5", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7484, - "name": "Whack Your Ex", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7485, - "name": "Resident Evil", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7486, - "name": "Zombie Driver HD", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 7487, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7488, - "name": "Vanish", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7489, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7490, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7491, - "name": "Zombie Party", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Инди" - ] - }, - { - "id": 7492, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7493, - "name": "SWAT 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7494, - "name": "Whack Your Neighbour", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7495, - "name": "The Last Remnant", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7496, - "name": "Victor Vran", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7497, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7498, - "name": "Zombie Vikings", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Beat-em-up" - ] - }, - { - "id": 7499, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7500, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7501, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7502, - "name": "The Binding of Isaac: Rebirth", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7503, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7504, - "name": "Whack Your Teacher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7505, - "name": "The Mask Man", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7506, - "name": "Zuma Deluxe", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 7507, - "name": "Vikings: Wolves of Midgard", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7508, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7509, - "name": "Titan Souls", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7510, - "name": "The Bunker", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7511, - "name": "The Elder Scrolls V: Skyrim", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7512, - "name": "What Remains of Edith Finch", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7513, - "name": "Zuma's Revenge!", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 7514, - "name": "Resident Evil 6", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7515, - "name": "The Punisher (2005)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7516, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7517, - "name": "Visage", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7518, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7519, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7520, - "name": "Titanfall 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 7521, - "name": "The Cat Lady", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7522, - "name": "Will Rock", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7523, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7524, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7525, - "name": "Wanted: Weapons of Fate", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7526, - "name": "The Secret of Monkey Island: Special Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7527, - "name": "Resident Evil 7: Biohazard", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7528, - "name": "Resident Evil 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7529, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7530, - "name": "Willy-Nilly Knight", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 7531, - "name": "To The Moon", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Квест" - ] - }, - { - "id": 7532, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7533, - "name": "The Council", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7534, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7535, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7536, - "name": "The Slippery Slope", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7537, - "name": "Warcraft III: Reign of Chaos", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 7538, - "name": "Resident Evil 5", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7539, - "name": "Tomb Raider", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7540, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7541, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7542, - "name": "Wizard of Legend", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "hack & slash" - ] - }, - { - "id": 7543, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7544, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7545, - "name": "The Curse of Blackwater", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7546, - "name": "Warcraft III: The Frozen Throne", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 7547, - "name": "The Stanley Parable", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7548, - "name": "Невероятные Приключения Трояна", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7549, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7550, - "name": "Wolfenstein", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7551, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7552, - "name": "Torchlight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 7553, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7554, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7555, - "name": "Невероятные Приключения Трояна 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7556, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7557, - "name": "The Walking Dead: A New Frontier", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7558, - "name": "Warhammer 40.000: Space Marine", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7559, - "name": "Wolfenstein: The New Order", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7560, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7561, - "name": "Torchlight II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7562, - "name": "Невероятные Приключения Трояна 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7563, - "name": "Sacred", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7564, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7565, - "name": "The Evil Within", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 7566, - "name": "The Darkness II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7567, - "name": "Warhammer: Chaosbane", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7568, - "name": "The Walking Dead: Michonne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7569, - "name": "Предатор", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7570, - "name": "Wolfenstein: The Old Blood", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7571, - "name": "Toren", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 7572, - "name": "Resident Evil 6", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7573, - "name": "The Evil Within 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 7574, - "name": "Стоп-Гоп", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7575, - "name": "Sacred 3", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7576, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7577, - "name": "We Happy Few", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 7578, - "name": "The Elder Scrolls IV: Oblivion", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7579, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 7580, - "name": "The Walking Dead: Season One", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7581, - "name": "Tormentum: Dark Sorrow", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Квест", - "Логические", - "Фэнтези" - ] - }, - { - "id": 7582, - "name": "Resident Evil 7: Biohazard", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7583, - "name": "Стоп-Гоп 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7584, - "name": "Whack The Thief", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7585, - "name": "The Evil Within: The Assignment (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7586, - "name": "Worms 2", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "TBS" - ] - }, - { - "id": 7587, - "name": "Sacred Citadel", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 7588, - "name": "The Walking Dead: Season Two", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7589, - "name": "Resident Evil: Operation Raccoon City", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7590, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7591, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7592, - "name": "Whack Your Boss", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7593, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7594, - "name": "Total Overdose", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 7595, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7596, - "name": "X-Blades / Ониблэйд", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7597, - "name": "Whack Your Ex", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7598, - "name": "The Evil Within: The Consequence (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7599, - "name": "The Witcher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7600, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7601, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7602, - "name": "Whack Your Neighbour", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7603, - "name": "Transistor", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Пошаговая", - "Научная фантастика", - "Ролевая", - "Экшен" - ] - }, - { - "id": 7604, - "name": "Resident Evil: Revelations", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7605, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7606, - "name": "X-Morph: Defense", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "tower defense" - ] - }, - { - "id": 7607, - "name": "The Witcher 2: Assassins of Kings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7608, - "name": "Whack Your Teacher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7609, - "name": "The Evil Within: The Executioner (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7610, - "name": "The Elder Scrolls V: Skyrim", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7611, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7612, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7613, - "name": "Trine", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 7614, - "name": "Yandere School", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7615, - "name": "Resident Evil: Revelations 2", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 7616, - "name": "The Expendabros", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 7617, - "name": "What Remains of Edith Finch", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7618, - "name": "The Witcher 3: Wild Hunt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7619, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7620, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7621, - "name": "Year Walk", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "приключения" - ] - }, - { - "id": 7622, - "name": "Trine 2", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 7623, - "name": "The Final Station", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7624, - "name": "Saints Row IV", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7625, - "name": "Will Rock", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 7626, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7627, - "name": "Reus", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 7628, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7629, - "name": "ZOMBI", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 7630, - "name": "Willy-Nilly Knight", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7631, - "name": "Trine 3: The Artifacts of Power", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 7632, - "name": "The First Tree", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7633, - "name": "Resident Evil: Operation Raccoon City", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7634, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7635, - "name": "Saints Row: Gat out of Hell", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7636, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7637, - "name": "Wizard of Legend", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 7638, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "визуальные новеллы", - "логические" - ] - }, - { - "id": 7639, - "name": "Troll Tale", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7640, - "name": "Rezrog", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7641, - "name": "Resident Evil: Revelations", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7642, - "name": "The Forest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7643, - "name": "The Wolf Among Us", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7644, - "name": "Salt and Sanctuary", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7645, - "name": "Trollface Quest", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7646, - "name": "Wolfenstein", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 7647, - "name": "Ziggurat", - "site": "gamer_info_com", - "genres": [ - "FPS", - "инди" - ] - }, - { - "id": 7648, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7649, - "name": "The Hat Man Shadow Ward", - "site": "ag_ru", - "genres": [ - "Спортивные", - "Ролевые", - "Приключения", - "Инди", - "Экшены", - "Гонки", - "Казуальные", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 7650, - "name": "Resident Evil: Revelations 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7651, - "name": "Thief", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7652, - "name": "Trollface Quest 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7653, - "name": "Samsara Room", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7654, - "name": "Zombie Driver HD", - "site": "gamer_info_com", - "genres": [ - "гонки" - ] - }, - { - "id": 7655, - "name": "Rise of the Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7656, - "name": "Wolfenstein: The New Order", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 7657, - "name": "The Evil Within", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7658, - "name": "The Incredible Adventures of Van Helsing", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7659, - "name": "Trollface Quest 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7660, - "name": "Saw: The Video Game", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7661, - "name": "This Is The Police", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7662, - "name": "Zombie Party", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7663, - "name": "Reus", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 7664, - "name": "The Evil Within 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7665, - "name": "Trollface Quest 4", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7666, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7667, - "name": "Wolfenstein: The Old Blood", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 7668, - "name": "The Incredible Adventures of Van Helsing II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7669, - "name": "TimeShift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7670, - "name": "Zombie Vikings", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7671, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7672, - "name": "Trollface Quest 5", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7673, - "name": "Scratches", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7674, - "name": "The Evil Within: The Assignment (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7675, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7676, - "name": "The Incredible Adventures of Van Helsing III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 7677, - "name": "Titan Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7678, - "name": "Rezrog", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 7679, - "name": "Zuma Deluxe", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7680, - "name": "Trollface Quest 6", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7681, - "name": "Worms 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 7682, - "name": "The Evil Within: The Consequence (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7683, - "name": "Seasons after Fall", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7684, - "name": "The Last Remnant", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Стратегии" - ] - }, - { - "id": 7685, - "name": "X-Blades / Ониблэйд", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7686, - "name": "Trollface Quest 7", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7687, - "name": "Zuma's Revenge!", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 7688, - "name": "Titan Quest Anniversary Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7689, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7690, - "name": "The Evil Within: The Executioner (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7691, - "name": "X-Morph: Defense", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7692, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7693, - "name": "The Mask Man", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7694, - "name": "Rise of the Tomb Raider", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7695, - "name": "Trollface Quest: Video Memes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7696, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7697, - "name": "Yandere School", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7698, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7699, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7700, - "name": "The Expendabros", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7701, - "name": "The Punisher (2005)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7702, - "name": "Twin Sector", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7703, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7704, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7705, - "name": "Serena", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7706, - "name": "Year Walk", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7707, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7708, - "name": "The Final Station", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7709, - "name": "Risen", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7710, - "name": "Serious Sam 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7711, - "name": "The Secret of Monkey Island: Special Edition", - "site": "ag_ru", - "genres": [ - "Приключения", - "Семейные" - ] - }, - { - "id": 7712, - "name": "Two Worlds", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 7713, - "name": "Titan Souls", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7714, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7715, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7716, - "name": "ZOMBI", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 7717, - "name": "The First Tree", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7718, - "name": "The Slippery Slope", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7719, - "name": "Typical Nightmare", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7720, - "name": "Serious Sam 3: BFE", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 7721, - "name": "Titanfall 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7722, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7723, - "name": "Невероятные Приключения Трояна", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7724, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7725, - "name": "Rock of Ages", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7726, - "name": "The Forest", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7727, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7728, - "name": "Ultimate Epic Battle Simulator", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Стратегия" - ] - }, - { - "id": 7729, - "name": "The Stanley Parable", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7730, - "name": "Ziggurat", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7731, - "name": "Невероятные Приключения Трояна 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7732, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7733, - "name": "To The Moon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7734, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7735, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7736, - "name": "The Hat Man Shadow Ward", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7737, - "name": "Ultra Street Fighter IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7738, - "name": "Zombie Driver HD", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7739, - "name": "Невероятные Приключения Трояна 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7740, - "name": "Tomb Raider", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7741, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7742, - "name": "The Walking Dead: A New Frontier", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7743, - "name": "Zombie Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7744, - "name": "Risen", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7745, - "name": "The Incredible Adventures of Van Helsing", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7746, - "name": "Undertale", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 7747, - "name": "Предатор", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7748, - "name": "Root Of Evil: The Tailor", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7749, - "name": "Torchlight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7750, - "name": "Zombie Vikings", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7751, - "name": "Unfair Mario", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7752, - "name": "Shadow Warrior", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7753, - "name": "The Walking Dead: Michonne", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7754, - "name": "The Incredible Adventures of Van Helsing II", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7755, - "name": "Стоп-Гоп", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7756, - "name": "Torchlight II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7757, - "name": "Zuma Deluxe", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7758, - "name": "Runic Rampage", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7759, - "name": "Rock of Ages", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Racing", - "Indie" - ] - }, - { - "id": 7760, - "name": "Unmechanical", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 7761, - "name": "The Walking Dead: Season One", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7762, - "name": "Стоп-Гоп 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7763, - "name": "Shadow of the Tomb Raider", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7764, - "name": "The Incredible Adventures of Van Helsing III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7765, - "name": "Toren", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7766, - "name": "Zuma's Revenge!", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 7767, - "name": "Unmechanical: Extended (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7768, - "name": "Rust", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7769, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7770, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7771, - "name": "The Walking Dead: Season Two", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7772, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Racing", - "Indie" - ] - }, - { - "id": 7773, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7774, - "name": "The Last Remnant", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7775, - "name": "Tormentum: Dark Sorrow", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7776, - "name": "Until the last", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7777, - "name": "Ryse: Son of Rome", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7778, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7779, - "name": "Shantae: Half-Genie Hero", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7780, - "name": "The Witcher", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7781, - "name": "The Mask Man", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7782, - "name": "Total Overdose", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7783, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7784, - "name": "Valiant Hearts: The Great War", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада", - "Адвенчура" - ] - }, - { - "id": 7785, - "name": "Root Of Evil: The Tailor", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 7786, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7787, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7788, - "name": "The Witcher 2: Assassins of Kings", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7789, - "name": "Vanish", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7790, - "name": "The Punisher (2005)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7791, - "name": "Transistor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7792, - "name": "Невероятные Приключения Трояна", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7793, - "name": "Runic Rampage", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7794, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7795, - "name": "SCP-087", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7796, - "name": "Trine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7797, - "name": "Victor Vran", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7798, - "name": "The Secret of Monkey Island: Special Edition", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7799, - "name": "Невероятные Приключения Трояна 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7800, - "name": "The Witcher 3: Wild Hunt", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7801, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7802, - "name": "Невероятные Приключения Трояна 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7803, - "name": "Trine 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7804, - "name": "Rust", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Action", - "Massively Multiplayer", - "Indie", - "Adventure" - ] - }, - { - "id": 7805, - "name": "Vikings: Wolves of Midgard", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 7806, - "name": "The Slippery Slope", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7807, - "name": "SCP-087-B", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7808, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7809, - "name": "Shrek 2: The Game", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7810, - "name": "Предатор", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7811, - "name": "Trine 3: The Artifacts of Power", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Головоломки", - "Приключения" - ] - }, - { - "id": 7812, - "name": "Стоп-Гоп", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7813, - "name": "The Stanley Parable", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7814, - "name": "Visage", - "site": "playground_ru", - "genres": [ - "Ужасы", - "От первого лица" - ] - }, - { - "id": 7815, - "name": "SINNER: Sacrifice for Redemption", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 7816, - "name": "Silence on The Line", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7817, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7818, - "name": "Ryse: Son of Rome", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7819, - "name": "Стоп-Гоп 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7820, - "name": "Troll Tale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7821, - "name": "Silent Hill: Alchemilla", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7822, - "name": "Wanted: Weapons of Fate", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7823, - "name": "The Walking Dead: A New Frontier", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7824, - "name": "The Wolf Among Us", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7825, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7826, - "name": "SWAT 4", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 7827, - "name": "Warcraft III: Reign of Chaos", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7828, - "name": "Trollface Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7829, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 7830, - "name": "The Walking Dead: Michonne", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7831, - "name": "Thief", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7832, - "name": "Warcraft III: The Frozen Throne", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7833, - "name": "Silverfall", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7834, - "name": "Trollface Quest 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7835, - "name": "SCP-087", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7836, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Puzzle", - "Adventure" - ] - }, - { - "id": 7837, - "name": "The Walking Dead: Season One", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7838, - "name": "This Is The Police", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 7839, - "name": "Warhammer 40.000: Space Marine", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Кооператив", - "Научная фантастика", - "Шутер", - "Экшен" - ] - }, - { - "id": 7840, - "name": "Silverfall: Earth Awakening", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7841, - "name": "Trollface Quest 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7842, - "name": "SCP-087-B", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7843, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7844, - "name": "The Walking Dead: Season Two", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7845, - "name": "TimeShift", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7846, - "name": "Warhammer: Chaosbane", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 7847, - "name": "Trollface Quest 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7848, - "name": "Titan Quest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7849, - "name": "SINNER: Sacrifice for Redemption", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 7850, - "name": "The Witcher", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7851, - "name": "We Happy Few", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Инди", - "Ужасы" - ] - }, - { - "id": 7852, - "name": "Singularity", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7853, - "name": "Trollface Quest 5", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7854, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition" - ] - }, - { - "id": 7855, - "name": "Titan Quest Anniversary Edition", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7856, - "name": "Whack The Thief", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7857, - "name": "SWAT 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7858, - "name": "Slap-Man", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7859, - "name": "The Witcher 2: Assassins of Kings", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7860, - "name": "Trollface Quest 6", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7861, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7862, - "name": "Whack Your Boss", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7863, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7864, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7865, - "name": "Slay the Spire", - "site": "gamespot_com", - "genres": [ - "Card Game" - ] - }, - { - "id": 7866, - "name": "The Witcher 3: Wild Hunt", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7867, - "name": "Trollface Quest 7", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7868, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7869, - "name": "Whack Your Ex", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7870, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7871, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7872, - "name": "Trollface Quest: Video Memes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7873, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7874, - "name": "Whack Your Neighbour", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7875, - "name": "Sleeping Dogs", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7876, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 7877, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7878, - "name": "Twin Sector", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7879, - "name": "Whack Your Teacher", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7880, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7881, - "name": "Titan Souls", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 7882, - "name": "Slender: The Arrival", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7883, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7884, - "name": "Two Worlds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7885, - "name": "What Remains of Edith Finch", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 7886, - "name": "Titanfall 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7887, - "name": "The Wolf Among Us", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7888, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 7889, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7890, - "name": "Slendytubbies", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7891, - "name": "Typical Nightmare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7892, - "name": "Will Rock", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7893, - "name": "To The Moon", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 7894, - "name": "Thief", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7895, - "name": "Sacred", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7896, - "name": "Ultimate Epic Battle Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7897, - "name": "Slime Rancher", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7898, - "name": "Willy-Nilly Knight", - "site": "playground_ru", - "genres": [ - "Тактика", - "Ролевая", - "Инди", - "Фэнтези" - ] - }, - { - "id": 7899, - "name": "Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7900, - "name": "This Is The Police", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7901, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 7902, - "name": "Ultra Street Fighter IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7903, - "name": "Sacred 3", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 7904, - "name": "Torchlight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7905, - "name": "Wizard of Legend", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Подземелья", - "Инди", - "Ролевая", - "Экшен" - ] - }, - { - "id": 7906, - "name": "Sniper Elite", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 7907, - "name": "TimeShift", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7908, - "name": "Undertale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7909, - "name": "Torchlight II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7910, - "name": "Sniper Elite 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7911, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 7912, - "name": "Sacred Citadel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7913, - "name": "Wolfenstein", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 7914, - "name": "Titan Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7915, - "name": "Unfair Mario", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7916, - "name": "Toren", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7917, - "name": "Sacred", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7918, - "name": "Wolfenstein: The New Order", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица" - ] - }, - { - "id": 7919, - "name": "Titan Quest Anniversary Edition", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7920, - "name": "Sniper Elite V2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Shooter", - "Tactical" - ] - }, - { - "id": 7921, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7922, - "name": "Unmechanical", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7923, - "name": "Tormentum: Dark Sorrow", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7924, - "name": "Wolfenstein: The Old Blood", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7925, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7926, - "name": "Unmechanical: Extended (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7927, - "name": "Sacred 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 7928, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7929, - "name": "Total Overdose", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7930, - "name": "Sophie's Curse", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7931, - "name": "Woolfe - The Red Hood Diaries", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Слэшер", - "Платформер" - ] - }, - { - "id": 7932, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7933, - "name": "Until the last", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7934, - "name": "Transistor", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 7935, - "name": "Worms 2", - "site": "playground_ru", - "genres": [ - "Логические", - "Стратегия" - ] - }, - { - "id": 7936, - "name": "Titan Souls", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7937, - "name": "Sacred Citadel", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7938, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7939, - "name": "South Park: The Fractured But Whole", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 7940, - "name": "Valiant Hearts: The Great War", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7941, - "name": "Trine", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Аркадные" - ] - }, - { - "id": 7942, - "name": "X-Blades / Ониблэйд", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7943, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7944, - "name": "Titanfall 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7945, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7946, - "name": "Saints Row IV", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7947, - "name": "Trine 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7948, - "name": "Vanish", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7949, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7950, - "name": "X-Morph: Defense", - "site": "playground_ru", - "genres": [ - "Инди", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 7951, - "name": "To The Moon", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7952, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7953, - "name": "Trine 3: The Artifacts of Power", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7954, - "name": "Victor Vran", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7955, - "name": "Saints Row: Gat out of Hell", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7956, - "name": "Yandere School", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7957, - "name": "South Park: The Stick of Truth", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 7958, - "name": "Troll Tale", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7959, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7960, - "name": "Vikings: Wolves of Midgard", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7961, - "name": "Year Walk", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7962, - "name": "Tomb Raider", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7963, - "name": "Salt and Sanctuary", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7964, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7965, - "name": "ZOMBI", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7966, - "name": "Visage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7967, - "name": "Samsara Room", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7968, - "name": "Torchlight", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7969, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7970, - "name": "Saints Row IV", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7971, - "name": "Zero Escape: Zero Time Dilemma", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7972, - "name": "Trollface Quest", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7973, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7974, - "name": "Wanted: Weapons of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7975, - "name": "Saw: The Video Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7976, - "name": "Torchlight II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7977, - "name": "Ziggurat", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7978, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7979, - "name": "Trollface Quest 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7980, - "name": "Saints Row: Gat out of Hell", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7981, - "name": "Warcraft III: Reign of Chaos", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7982, - "name": "Scratches", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7983, - "name": "Toren", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7984, - "name": "Zombie Driver HD", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7985, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7986, - "name": "Trollface Quest 3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7987, - "name": "Warcraft III: The Frozen Throne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7988, - "name": "Zombie Party", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7989, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7990, - "name": "Seasons after Fall", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7991, - "name": "Salt and Sanctuary", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 7992, - "name": "Tormentum: Dark Sorrow", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7993, - "name": "Trollface Quest 4", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7994, - "name": "Warhammer 40.000: Space Marine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7995, - "name": "Zombie Vikings", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7996, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7997, - "name": "Samsara Room", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7998, - "name": "Total Overdose", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7999, - "name": "Serena", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8000, - "name": "Trollface Quest 5", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8001, - "name": "Zuma Deluxe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8002, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8003, - "name": "Warhammer: Chaosbane", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ", - "Приключения" - ] - }, - { - "id": 8004, - "name": "Saw: The Video Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8005, - "name": "Transistor", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8006, - "name": "Trollface Quest 6", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8007, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8008, - "name": "Serious Sam 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8009, - "name": "Zuma's Revenge!", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 8010, - "name": "We Happy Few", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 8011, - "name": "Scratches", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8012, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8013, - "name": "Trine", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8014, - "name": "Trollface Quest 7", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8015, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8016, - "name": "Whack The Thief", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8017, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8018, - "name": "Serious Sam 3: BFE", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8019, - "name": "Trollface Quest: Video Memes", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8020, - "name": "Trine 2", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8021, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8022, - "name": "Seasons after Fall", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8023, - "name": "Whack Your Boss", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8024, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8025, - "name": "Twin Sector", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8026, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8027, - "name": "Trine 3: The Artifacts of Power", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8028, - "name": "Spooky's House of Jump Scares", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8029, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8030, - "name": "Whack Your Ex", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8031, - "name": "Serena", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8032, - "name": "Two Worlds", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 8033, - "name": "Troll Tale", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8034, - "name": "Serious Sam Classic: The First Encounter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8035, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8036, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8037, - "name": "Whack Your Neighbour", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8038, - "name": "Typical Nightmare", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 8039, - "name": "Serious Sam Classic: The Second Encounter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8040, - "name": "Trollface Quest", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8041, - "name": "Невероятные Приключения Трояна", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8042, - "name": "Whack Your Teacher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8043, - "name": "Serious Sam 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8044, - "name": "Star Wars: The Force Unleashed", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8045, - "name": "Ultimate Epic Battle Simulator", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 8046, - "name": "Невероятные Приключения Трояна 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8047, - "name": "Trollface Quest 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8048, - "name": "What Remains of Edith Finch", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8049, - "name": "Shadow Warrior", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8050, - "name": "Невероятные Приключения Трояна 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8051, - "name": "Serious Sam 3: BFE", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8052, - "name": "Ultra Street Fighter IV", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 8053, - "name": "Star Wars: The Force Unleashed II", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8054, - "name": "Trollface Quest 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8055, - "name": "Will Rock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8056, - "name": "Предатор", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8057, - "name": "Undertale", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди" - ] - }, - { - "id": 8058, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8059, - "name": "Willy-Nilly Knight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8060, - "name": "Shadow of the Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8061, - "name": "Trollface Quest 4", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8062, - "name": "SteamWorld Dig", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8063, - "name": "Стоп-Гоп", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8064, - "name": "Unfair Mario", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8065, - "name": "Стоп-Гоп 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8066, - "name": "Wizard of Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8067, - "name": "Trollface Quest 5", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8068, - "name": "Serious Sam Classic: The First Encounter", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8069, - "name": "SteamWorld Dig 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8070, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8071, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8072, - "name": "Unmechanical", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 8073, - "name": "Wolfenstein", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8074, - "name": "Trollface Quest 6", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8075, - "name": "SteamWorld Heist", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8076, - "name": "Serious Sam Classic: The Second Encounter", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8077, - "name": "Unmechanical: Extended (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 8078, - "name": "Shantae: Half-Genie Hero", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8079, - "name": "Wolfenstein: The New Order", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8080, - "name": "Trollface Quest 7", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8081, - "name": "Until the last", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 8082, - "name": "Shoot Your Nightmare Halloween Special", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8083, - "name": "Wolfenstein: The Old Blood", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 8084, - "name": "Trollface Quest: Video Memes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8085, - "name": "Steampunk Tower 2", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8086, - "name": "Shadow Warrior", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8087, - "name": "Shovel Knight: Shovel of Hope", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8088, - "name": "Valiant Hearts: The Great War", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Ролевые", - "Приключения" - ] - }, - { - "id": 8089, - "name": "Woolfe - The Red Hood Diaries", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8090, - "name": "Twin Sector", - "site": "stopgame_ru", - "genres": [ - "logic", - "action" - ] - }, - { - "id": 8091, - "name": "Shadow of the Tomb Raider", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8092, - "name": "Steel Rats", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 8093, - "name": "Vanish", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 8094, - "name": "Worms 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8095, - "name": "Shrek 2: The Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8096, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8097, - "name": "Two Worlds", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8098, - "name": "Stories: The Path of Destinies", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8099, - "name": "X-Blades / Ониблэйд", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8100, - "name": "Victor Vran", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8101, - "name": "Typical Nightmare", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8102, - "name": "Silence on The Line", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8103, - "name": "Shantae: Half-Genie Hero", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8104, - "name": "Street Fighter X Tekken", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Fighting" - ] - }, - { - "id": 8105, - "name": "Vikings: Wolves of Midgard", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 8106, - "name": "X-Morph: Defense", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8107, - "name": "Silent Hill: Alchemilla", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8108, - "name": "Ultimate Epic Battle Simulator", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8110, - "name": "Visage", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 8111, - "name": "Shoot Your Nightmare Halloween Special", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8112, - "name": "Yandere School", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8113, - "name": "Ultra Street Fighter IV", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8114, - "name": "Silverfall", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8115, - "name": "Wanted: Weapons of Fate", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8116, - "name": "Stupidella", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8117, - "name": "Shovel Knight: Shovel of Hope", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8118, - "name": "Undertale", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8119, - "name": "Year Walk", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8120, - "name": "Warcraft III: Reign of Chaos", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8121, - "name": "Silverfall: Earth Awakening", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8122, - "name": "Shrek 2: The Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8123, - "name": "Unfair Mario", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8124, - "name": "ZOMBI", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8125, - "name": "Sudeki", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8126, - "name": "Warcraft III: The Frozen Throne", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8127, - "name": "Unmechanical", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8128, - "name": "Silence on The Line", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8129, - "name": "Singularity", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8130, - "name": "Zero Escape: Zero Time Dilemma", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8131, - "name": "Warhammer 40.000: Space Marine", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8132, - "name": "Silent Hill: Alchemilla", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8133, - "name": "Unmechanical: Extended (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8134, - "name": "Superhot", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 8135, - "name": "Warhammer: Chaosbane", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 8136, - "name": "Ziggurat", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8137, - "name": "Slap-Man", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8138, - "name": "Surgeon Simulator", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8139, - "name": "Until the last", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8140, - "name": "We Happy Few", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8141, - "name": "Silverfall", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8142, - "name": "Zombie Driver HD", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8143, - "name": "Tales from the Borderlands", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8144, - "name": "Slay the Spire", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8145, - "name": "Whack The Thief", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8146, - "name": "Valiant Hearts: The Great War", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8147, - "name": "Silverfall: Earth Awakening", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8148, - "name": "Zombie Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8149, - "name": "Tales of Berseria", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8150, - "name": "Whack Your Boss", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8151, - "name": "Sleeping Dogs", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 8152, - "name": "Vanish", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8153, - "name": "Zombie Vikings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8154, - "name": "Whack Your Ex", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 8155, - "name": "Tales of Zestiria", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8156, - "name": "Singularity", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8157, - "name": "Victor Vran", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8158, - "name": "Zuma Deluxe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8159, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8160, - "name": "Slender: The Arrival", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8161, - "name": "Whack Your Neighbour", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8162, - "name": "Slap-Man", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8163, - "name": "Vikings: Wolves of Midgard", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 8164, - "name": "Tasty Planet: Back for Seconds", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8165, - "name": "Zuma's Revenge!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8166, - "name": "Slendytubbies", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8167, - "name": "Whack Your Teacher", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8168, - "name": "Visage", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 8169, - "name": "Slay the Spire", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie" - ] - }, - { - "id": 8170, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8171, - "name": "Tekken 7", - "site": "gamespot_com", - "genres": [ - "VR", - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 8172, - "name": "What Remains of Edith Finch", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 8173, - "name": "Wanted: Weapons of Fate", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8174, - "name": "Slime Rancher", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Role-Playing (RPG)" - ] - }, - { - "id": 8175, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8176, - "name": "Sleeping Dogs", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8177, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8178, - "name": "Warcraft III: Reign of Chaos", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8179, - "name": "Will Rock", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8180, - "name": "Sniper Elite", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation" - ] - }, - { - "id": 8181, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8182, - "name": "Tesla vs Lovecraft", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 8183, - "name": "Warcraft III: The Frozen Throne", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8184, - "name": "Slender: The Arrival", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8185, - "name": "Willy-Nilly Knight", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8186, - "name": "Sniper Elite 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8187, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8188, - "name": "Slendytubbies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8189, - "name": "Wizard of Legend", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8190, - "name": "Warhammer 40.000: Space Marine", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8191, - "name": "Teslagrad", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8192, - "name": "Невероятные Приключения Трояна", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8193, - "name": "Sniper Elite V2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8194, - "name": "Warhammer: Chaosbane", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8195, - "name": "Slime Rancher", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8196, - "name": "Wolfenstein", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8197, - "name": "Невероятные Приключения Трояна 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8198, - "name": "The Banner Saga", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8199, - "name": "Wolfenstein: The New Order", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8200, - "name": "We Happy Few", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8201, - "name": "Невероятные Приключения Трояна 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8202, - "name": "Sophie's Curse", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8203, - "name": "Sniper Elite", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8204, - "name": "The Banner Saga 2", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8205, - "name": "Whack The Thief", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8206, - "name": "Wolfenstein: The Old Blood", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8207, - "name": "Предатор", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8208, - "name": "The Bard’s Tale (2004)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8209, - "name": "South Park: The Fractured But Whole", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8210, - "name": "Whack Your Boss", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8211, - "name": "Woolfe - The Red Hood Diaries", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8212, - "name": "Sniper Elite 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8213, - "name": "Стоп-Гоп", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8214, - "name": "The Beginner's Guide", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8215, - "name": "Worms 2", - "site": "ag_ru", - "genres": [ - "Файтинги", - "Стратегии" - ] - }, - { - "id": 8216, - "name": "Whack Your Ex", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8217, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8218, - "name": "Стоп-Гоп 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8219, - "name": "Sniper Elite V2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8220, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8221, - "name": "X-Blades / Ониблэйд", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8222, - "name": "Whack Your Neighbour", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8223, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8224, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8225, - "name": "X-Morph: Defense", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Стратегии" - ] - }, - { - "id": 8226, - "name": "Sophie's Curse", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 8227, - "name": "Whack Your Teacher", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8228, - "name": "The Binding of Isaac: Rebirth", - "site": "gamespot_com", - "genres": [ - "Action", - "Roguelike", - "3D", - "Adventure" - ] - }, - { - "id": 8229, - "name": "Yandere School", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 8230, - "name": "What Remains of Edith Finch", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8231, - "name": "South Park: The Stick of Truth", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8232, - "name": "The Bunker", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8233, - "name": "South Park: The Fractured But Whole", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8234, - "name": "Year Walk", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8235, - "name": "Will Rock", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8236, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8237, - "name": "The Cat Lady", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8238, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8239, - "name": "Willy-Nilly Knight", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8240, - "name": "ZOMBI", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8241, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8242, - "name": "Zero Escape: Zero Time Dilemma", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 8243, - "name": "Wizard of Legend", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8244, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8245, - "name": "The Council", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8246, - "name": "Ziggurat", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Инди", - "Экшены" - ] - }, - { - "id": 8247, - "name": "South Park: The Stick of Truth", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8248, - "name": "Wolfenstein", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8249, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8250, - "name": "Zombie Driver HD", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Гонки", - "Инди", - "Экшены" - ] - }, - { - "id": 8251, - "name": "Wolfenstein: The New Order", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8252, - "name": "The Curse of Blackwater", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8253, - "name": "Zombie Party", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8254, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8255, - "name": "Wolfenstein: The Old Blood", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 8256, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8257, - "name": "Zombie Vikings", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8258, - "name": "Woolfe - The Red Hood Diaries", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8259, - "name": "The Darkness II", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8260, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8261, - "name": "Zuma Deluxe", - "site": "ag_ru", - "genres": [ - "Казуальные" - ] - }, - { - "id": 8262, - "name": "Worms 2", - "site": "stopgame_ru", - "genres": [ - "strategy", - "logic" - ] - }, - { - "id": 8263, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8264, - "name": "Zuma's Revenge!", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Головоломки" - ] - }, - { - "id": 8265, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8266, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8267, - "name": "X-Blades / Ониблэйд", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8268, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8269, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8270, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8271, - "name": "X-Morph: Defense", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 8272, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8273, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8274, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8275, - "name": "Yandere School", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8276, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8277, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamespot_com", - "genres": [ - "VR", - "Role-Playing" - ] - }, - { - "id": 8278, - "name": "Year Walk", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8279, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8280, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8281, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8282, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8283, - "name": "Невероятные Приключения Трояна", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8284, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8285, - "name": "ZOMBI", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8286, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8287, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8288, - "name": "Невероятные Приключения Трояна 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8289, - "name": "Zero Escape: Zero Time Dilemma", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8290, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8291, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 8292, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8293, - "name": "Невероятные Приключения Трояна 3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8294, - "name": "Ziggurat", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8295, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8296, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8297, - "name": "Предатор", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8298, - "name": "The Evil Within", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Adventure" - ] - }, - { - "id": 8299, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8300, - "name": "Zombie Driver HD", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8301, - "name": "Стоп-Гоп", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8302, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8303, - "name": "The Evil Within 2", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8304, - "name": "Zombie Party", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8305, - "name": "Стоп-Гоп 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8306, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8307, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8308, - "name": "Zombie Vikings", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8309, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8310, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 8311, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8312, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8313, - "name": "Zuma Deluxe", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8314, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8315, - "name": "Spooky's House of Jump Scares", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8316, - "name": "Zuma's Revenge!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8317, - "name": "The Expendabros", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8318, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8319, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8320, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8321, - "name": "The Final Station", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8322, - "name": "Star Wars: The Force Unleashed", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8323, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8324, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Puzzle", - "Adventure" - ] - }, - { - "id": 8325, - "name": "The First Tree", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8326, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8327, - "name": "Star Wars: The Force Unleashed II", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8328, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8329, - "name": "The Forest", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8330, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8331, - "name": "SteamWorld Dig", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8332, - "name": "Невероятные Приключения Трояна", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8333, - "name": "The Hat Man Shadow Ward", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8334, - "name": "Невероятные Приключения Трояна 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8335, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8336, - "name": "SteamWorld Dig 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8337, - "name": "Spooky's House of Jump Scares", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8338, - "name": "Невероятные Приключения Трояна 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8339, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8340, - "name": "SteamWorld Heist", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8341, - "name": "Предатор", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8342, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8343, - "name": "Стоп-Гоп", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8344, - "name": "Steampunk Tower 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 8345, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8346, - "name": "The Last Remnant", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8347, - "name": "Стоп-Гоп 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8348, - "name": "The Mask Man", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8349, - "name": "Star Wars: The Force Unleashed", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8350, - "name": "Steel Rats", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Racing" - ] - }, - { - "id": 8351, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8352, - "name": "The Punisher (2005)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8353, - "name": "Star Wars: The Force Unleashed II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8354, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8355, - "name": "Stories: The Path of Destinies", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8356, - "name": "SteamWorld Dig", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8357, - "name": "The Slippery Slope", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8358, - "name": "Street Fighter X Tekken", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8359, - "name": "SteamWorld Dig 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8361, - "name": "SteamWorld Heist", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8362, - "name": "The Stanley Parable", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8363, - "name": "Stupidella", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8364, - "name": "The Walking Dead: A New Frontier", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8365, - "name": "Steampunk Tower 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8366, - "name": "The Walking Dead: Michonne", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8367, - "name": "Sudeki", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8368, - "name": "Steel Rats", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8369, - "name": "The Walking Dead: Season One", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8370, - "name": "The Walking Dead: Season Two", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8371, - "name": "Stories: The Path of Destinies", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8372, - "name": "Superhot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8373, - "name": "The Witcher", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8374, - "name": "Surgeon Simulator", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 8375, - "name": "Street Fighter X Tekken", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8376, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8378, - "name": "Tales from the Borderlands", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8379, - "name": "The Witcher 3: Wild Hunt", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8380, - "name": "Stupidella", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8381, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8382, - "name": "Tales of Berseria", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8383, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8384, - "name": "Sudeki", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8385, - "name": "Tales of Zestiria", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8386, - "name": "The Wolf Among Us", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8387, - "name": "Superhot", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8388, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8389, - "name": "Thief", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8390, - "name": "Surgeon Simulator", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8391, - "name": "This Is The Police", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 8392, - "name": "Tasty Planet: Back for Seconds", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 8393, - "name": "Tales from the Borderlands", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8394, - "name": "TimeShift", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8395, - "name": "Tekken 7", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Action" - ] - }, - { - "id": 8396, - "name": "Tales of Berseria", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8397, - "name": "Titan Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8398, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8399, - "name": "Tales of Zestiria", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8400, - "name": "Titan Quest Anniversary Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8401, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8402, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8403, - "name": "Tesla vs Lovecraft", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8404, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8405, - "name": "Tasty Planet: Back for Seconds", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8406, - "name": "Titan Souls", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8407, - "name": "Teslagrad", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 8408, - "name": "Titanfall 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8409, - "name": "Tekken 7", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8410, - "name": "The Banner Saga", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 8411, - "name": "To The Moon", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8412, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8413, - "name": "The Banner Saga 2", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 8414, - "name": "Tomb Raider", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8415, - "name": "The Bard’s Tale (2004)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8416, - "name": "Tesla vs Lovecraft", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8417, - "name": "Torchlight", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8418, - "name": "Teslagrad", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8419, - "name": "The Beginner's Guide", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8420, - "name": "Torchlight II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8421, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8422, - "name": "The Banner Saga", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8423, - "name": "Toren", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8424, - "name": "The Binding of Isaac: Rebirth", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8425, - "name": "The Banner Saga 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8426, - "name": "Tormentum: Dark Sorrow", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8427, - "name": "The Bard’s Tale (2004)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8428, - "name": "The Bunker", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8429, - "name": "Total Overdose", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8430, - "name": "The Beginner's Guide", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8431, - "name": "The Cat Lady", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8432, - "name": "Transistor", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8433, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8434, - "name": "The Council", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Adventure" - ] - }, - { - "id": 8435, - "name": "The Binding of Isaac: Rebirth", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8436, - "name": "The Curse of Blackwater", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8437, - "name": "Trine", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8438, - "name": "The Bunker", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8439, - "name": "Trine 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8440, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8441, - "name": "The Cat Lady", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8442, - "name": "Trine 3: The Artifacts of Power", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8443, - "name": "Troll Tale", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8444, - "name": "The Council", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8445, - "name": "The Darkness II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8446, - "name": "Trollface Quest", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8447, - "name": "The Elder Scrolls IV: Oblivion", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8448, - "name": "The Curse of Blackwater", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8449, - "name": "Trollface Quest 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8450, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8451, - "name": "Trollface Quest 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8452, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8453, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8454, - "name": "Trollface Quest 4", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8455, - "name": "Trollface Quest 5", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8456, - "name": "The Darkness II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8457, - "name": "The Elder Scrolls V: Skyrim", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8458, - "name": "Trollface Quest 6", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8459, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8460, - "name": "Trollface Quest 7", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8461, - "name": "The Elder Scrolls IV: Oblivion", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8462, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8463, - "name": "Trollface Quest: Video Memes", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8464, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8465, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8466, - "name": "Twin Sector", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8467, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8468, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8469, - "name": "Two Worlds", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8470, - "name": "The Elder Scrolls V: Skyrim", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8471, - "name": "The Evil Within", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8472, - "name": "Typical Nightmare", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8473, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8474, - "name": "The Evil Within 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8475, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8476, - "name": "Ultimate Epic Battle Simulator", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 8477, - "name": "The Evil Within: The Assignment (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8478, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8479, - "name": "The Evil Within: The Consequence (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8480, - "name": "Ultra Street Fighter IV", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 8481, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8482, - "name": "The Evil Within: The Executioner (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8483, - "name": "The Evil Within", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8484, - "name": "Undertale", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8485, - "name": "The Expendabros", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8486, - "name": "The Evil Within 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8487, - "name": "Unfair Mario", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8488, - "name": "The Final Station", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8489, - "name": "The Evil Within: The Assignment (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8490, - "name": "The First Tree", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8491, - "name": "The Evil Within: The Consequence (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8492, - "name": "Unmechanical", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8493, - "name": "Unmechanical: Extended (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8494, - "name": "The Forest", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8495, - "name": "The Evil Within: The Executioner (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8496, - "name": "Until the last", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 8497, - "name": "The Hat Man Shadow Ward", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8498, - "name": "The Expendabros", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8499, - "name": "Valiant Hearts: The Great War", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8500, - "name": "The Incredible Adventures of Van Helsing", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8501, - "name": "The Final Station", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8502, - "name": "The Incredible Adventures of Van Helsing II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8503, - "name": "Vanish", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8504, - "name": "The First Tree", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Adventure" - ] - }, - { - "id": 8505, - "name": "The Incredible Adventures of Van Helsing III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8506, - "name": "The Forest", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8507, - "name": "Victor Vran", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8508, - "name": "The Last Remnant", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8509, - "name": "The Hat Man Shadow Ward", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8510, - "name": "Vikings: Wolves of Midgard", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8511, - "name": "The Mask Man", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8512, - "name": "The Punisher (2005)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8513, - "name": "The Incredible Adventures of Van Helsing", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8514, - "name": "Visage", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8515, - "name": "The Incredible Adventures of Van Helsing II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8516, - "name": "The Secret of Monkey Island: Special Edition", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8517, - "name": "Wanted: Weapons of Fate", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8518, - "name": "The Slippery Slope", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8519, - "name": "The Incredible Adventures of Van Helsing III", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8520, - "name": "Warcraft III: Reign of Chaos", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8521, - "name": "The Stanley Parable", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8522, - "name": "Warcraft III: The Frozen Throne", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8523, - "name": "The Last Remnant", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8524, - "name": "The Walking Dead: A New Frontier", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8525, - "name": "Warhammer 40.000: Space Marine", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8526, - "name": "The Walking Dead: Michonne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8527, - "name": "The Mask Man", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8528, - "name": "Warhammer: Chaosbane", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8529, - "name": "The Walking Dead: Season One", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8530, - "name": "The Punisher (2005)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8531, - "name": "The Walking Dead: Season Two", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8532, - "name": "We Happy Few", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8533, - "name": "The Secret of Monkey Island: Special Edition", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8534, - "name": "Whack The Thief", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8535, - "name": "The Witcher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8536, - "name": "Whack Your Boss", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8537, - "name": "The Slippery Slope", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8538, - "name": "The Witcher 2: Assassins of Kings", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8539, - "name": "Whack Your Ex", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8540, - "name": "The Stanley Parable", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8541, - "name": "Whack Your Neighbour", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8542, - "name": "The Witcher 3: Wild Hunt", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8543, - "name": "Whack Your Teacher", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8544, - "name": "The Walking Dead: A New Frontier", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8545, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8546, - "name": "What Remains of Edith Finch", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8547, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8548, - "name": "The Walking Dead: Michonne", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8549, - "name": "Will Rock", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8550, - "name": "The Walking Dead: Season One", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8551, - "name": "The Wolf Among Us", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8552, - "name": "The Walking Dead: Season Two", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8553, - "name": "Willy-Nilly Knight", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8554, - "name": "Thief", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8555, - "name": "The Witcher", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8556, - "name": "Wizard of Legend", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8557, - "name": "This Is The Police", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 8558, - "name": "The Witcher 2: Assassins of Kings", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8559, - "name": "Wolfenstein", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8560, - "name": "TimeShift", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8561, - "name": "Wolfenstein: The New Order", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8562, - "name": "The Witcher 3: Wild Hunt", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8563, - "name": "Titan Quest", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8564, - "name": "Wolfenstein: The Old Blood", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8565, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8566, - "name": "Titan Quest Anniversary Edition", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8567, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8568, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8569, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8570, - "name": "Worms 2", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8571, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8572, - "name": "The Wolf Among Us", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8573, - "name": "X-Blades / Ониблэйд", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8574, - "name": "X-Morph: Defense", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8575, - "name": "Titan Souls", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8576, - "name": "Thief", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8577, - "name": "Titanfall 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8578, - "name": "Yandere School", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8579, - "name": "This Is The Police", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Simulation", - "Adventure" - ] - }, - { - "id": 8580, - "name": "To The Moon", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8581, - "name": "Year Walk", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8582, - "name": "TimeShift", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8583, - "name": "ZOMBI", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8584, - "name": "Tomb Raider", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8585, - "name": "Titan Quest", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8586, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8587, - "name": "Torchlight", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8588, - "name": "Titan Quest Anniversary Edition", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8589, - "name": "Ziggurat", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8590, - "name": "Torchlight II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8591, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8592, - "name": "Zombie Driver HD", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8593, - "name": "Toren", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8594, - "name": "Zombie Party", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 8595, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8596, - "name": "Tormentum: Dark Sorrow", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 8597, - "name": "Zombie Vikings", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 8598, - "name": "Total Overdose", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8599, - "name": "Titan Souls", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8600, - "name": "Zuma Deluxe", - "site": "gamespot_com", - "genres": [ - "Matching/Stacking", - "Puzzle" - ] - }, - { - "id": 8601, - "name": "Transistor", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8602, - "name": "Titanfall 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8603, - "name": "Zuma's Revenge!", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 8604, - "name": "Trine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8605, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8606, - "name": "To The Moon", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8607, - "name": "Trine 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8608, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8609, - "name": "Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8610, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8611, - "name": "Trine 3: The Artifacts of Power", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8612, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8613, - "name": "Torchlight", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8614, - "name": "Troll Tale", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8615, - "name": "Невероятные Приключения Трояна", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8616, - "name": "Torchlight II", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8617, - "name": "Невероятные Приключения Трояна 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8618, - "name": "Trollface Quest", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8619, - "name": "Невероятные Приключения Трояна 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8620, - "name": "Toren", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8621, - "name": "Trollface Quest 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8622, - "name": "Предатор", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8623, - "name": "Tormentum: Dark Sorrow", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8624, - "name": "Trollface Quest 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8625, - "name": "Стоп-Гоп", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8626, - "name": "Total Overdose", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8627, - "name": "Trollface Quest 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8628, - "name": "Стоп-Гоп 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8629, - "name": "Trollface Quest 5", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8630, - "name": "Transistor", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8631, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8632, - "name": "Trollface Quest 6", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8633, - "name": "Trine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8634, - "name": "Trollface Quest 7", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8635, - "name": "Trine 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8636, - "name": "Trollface Quest: Video Memes", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8637, - "name": "Trine 3: The Artifacts of Power", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8638, - "name": "Twin Sector", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8639, - "name": "Troll Tale", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8640, - "name": "Two Worlds", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8641, - "name": "Trollface Quest", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8642, - "name": "Typical Nightmare", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8643, - "name": "Trollface Quest 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8644, - "name": "Trollface Quest 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8645, - "name": "Ultimate Epic Battle Simulator", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 8646, - "name": "Trollface Quest 4", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8647, - "name": "Trollface Quest 5", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8648, - "name": "Ultra Street Fighter IV", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8649, - "name": "Trollface Quest 6", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8650, - "name": "Undertale", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie" - ] - }, - { - "id": 8651, - "name": "Trollface Quest 7", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8652, - "name": "Unfair Mario", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8653, - "name": "Trollface Quest: Video Memes", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8654, - "name": "Unmechanical", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 8655, - "name": "Twin Sector", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8656, - "name": "Unmechanical: Extended (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8657, - "name": "Two Worlds", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8658, - "name": "Until the last", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 8659, - "name": "Typical Nightmare", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8660, - "name": "Valiant Hearts: The Great War", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8661, - "name": "Ultimate Epic Battle Simulator", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 8662, - "name": "Vanish", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8663, - "name": "Ultra Street Fighter IV", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8664, - "name": "Victor Vran", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8665, - "name": "Undertale", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8666, - "name": "Vikings: Wolves of Midgard", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8667, - "name": "Unfair Mario", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8668, - "name": "Visage", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure", - "Simulation", - "Early Access" - ] - }, - { - "id": 8669, - "name": "Unmechanical", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8670, - "name": "Wanted: Weapons of Fate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8671, - "name": "Unmechanical: Extended (DLC)", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8672, - "name": "Warcraft III: Reign of Chaos", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8673, - "name": "Warcraft III: The Frozen Throne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8674, - "name": "Until the last", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8675, - "name": "Warhammer 40.000: Space Marine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8676, - "name": "Valiant Hearts: The Great War", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8677, - "name": "Vanish", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8678, - "name": "Warhammer: Chaosbane", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8679, - "name": "Victor Vran", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8680, - "name": "We Happy Few", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8681, - "name": "Vikings: Wolves of Midgard", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8682, - "name": "Whack The Thief", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8683, - "name": "Visage", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8684, - "name": "Whack Your Boss", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8685, - "name": "Wanted: Weapons of Fate", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8686, - "name": "Whack Your Ex", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8687, - "name": "Whack Your Neighbour", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8688, - "name": "Warcraft III: Reign of Chaos", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8689, - "name": "Whack Your Teacher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8690, - "name": "Warcraft III: The Frozen Throne", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8691, - "name": "What Remains of Edith Finch", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8692, - "name": "Warhammer 40.000: Space Marine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8693, - "name": "Will Rock", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8694, - "name": "Warhammer: Chaosbane", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8695, - "name": "Willy-Nilly Knight", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8696, - "name": "We Happy Few", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8697, - "name": "Wizard of Legend", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8698, - "name": "Whack The Thief", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8699, - "name": "Whack Your Boss", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8700, - "name": "Wolfenstein", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8701, - "name": "Whack Your Ex", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8702, - "name": "Wolfenstein: The New Order", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8703, - "name": "Whack Your Neighbour", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8704, - "name": "Whack Your Teacher", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8705, - "name": "Wolfenstein: The Old Blood", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8706, - "name": "What Remains of Edith Finch", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8707, - "name": "Woolfe - The Red Hood Diaries", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8708, - "name": "Will Rock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8709, - "name": "Worms 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8710, - "name": "Willy-Nilly Knight", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8711, - "name": "X-Blades / Ониблэйд", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8712, - "name": "Wizard of Legend", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8713, - "name": "X-Morph: Defense", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 8714, - "name": "Wolfenstein", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8715, - "name": "Yandere School", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8716, - "name": "Wolfenstein: The New Order", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8717, - "name": "Year Walk", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8718, - "name": "Wolfenstein: The Old Blood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8719, - "name": "ZOMBI", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8720, - "name": "Woolfe - The Red Hood Diaries", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8721, - "name": "Worms 2", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8722, - "name": "Zero Escape: Zero Time Dilemma", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8723, - "name": "X-Blades / Ониблэйд", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8724, - "name": "Ziggurat", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8725, - "name": "X-Morph: Defense", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 8726, - "name": "Yandere School", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8727, - "name": "Zombie Driver HD", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Racing", - "Indie" - ] - }, - { - "id": 8728, - "name": "Year Walk", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8729, - "name": "Zombie Party", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8730, - "name": "ZOMBI", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8731, - "name": "Zombie Vikings", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8732, - "name": "Zero Escape: Zero Time Dilemma", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8733, - "name": "Zuma Deluxe", - "site": "store_steampowered_com", - "genres": [ - "Casual" - ] - }, - { - "id": 8734, - "name": "Ziggurat", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8735, - "name": "Zuma's Revenge!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Casual" - ] - }, - { - "id": 8736, - "name": "Zombie Driver HD", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 8737, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8738, - "name": "Zombie Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8739, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8740, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8741, - "name": "Zombie Vikings", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8742, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8743, - "name": "Zuma Deluxe", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8744, - "name": "Невероятные Приключения Трояна", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8745, - "name": "Zuma's Revenge!", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8746, - "name": "Невероятные Приключения Трояна 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8747, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8748, - "name": "Невероятные Приключения Трояна 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8749, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8750, - "name": "Предатор", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8751, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8752, - "name": "Стоп-Гоп", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8753, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8754, - "name": "Стоп-Гоп 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8755, - "name": "Невероятные Приключения Трояна", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8756, - "name": "Невероятные Приключения Трояна 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8757, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 8758, - "name": "Невероятные Приключения Трояна 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8759, - "name": "Предатор", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8760, - "name": "Стоп-Гоп", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8761, - "name": "Стоп-Гоп 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8762, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8763, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8764, - "name": "BLACK ROSE", - "site": "metacritic_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 8765, - "name": "Bad Dream: Bridge", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8766, - "name": "Bad Dream: Butcher", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8767, - "name": "Bad Dream: Coma", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "General", - "Adventure" - ] - }, - { - "id": 8768, - "name": "Bad Dream: Cyclops", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8769, - "name": "Bad Dream: Graveyard", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8770, - "name": "Bad Dream: Hospital", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8771, - "name": "Bad Dream: Memories", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8772, - "name": "Baldur's Gate", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "PC-style RPG", - "Western-Style" - ] - }, - { - "id": 8773, - "name": "Bastion", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 8774, - "name": "Batman: The Telltale Series", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 8775, - "name": "Battlefield 1", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Tactical", - "General", - "Action" - ] - }, - { - "id": 8776, - "name": "Battlefield 1942", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 8777, - "name": "Battlefield 4", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Modern", - "Arcade", - "Action" - ] - }, - { - "id": 8778, - "name": "Battlefield Hardline", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Modern", - "Tactical", - "Arcade", - "Action" - ] - }, - { - "id": 8779, - "name": "Battlefield V", - "site": "metacritic_com", - "genres": [ - "Action", - "Shooter", - "Tactical", - "First-Person" - ] - }, - { - "id": 8780, - "name": "Battlefield Vietnam", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 8781, - "name": "Bayonetta", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 8782, - "name": "Beholder", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 8783, - "name": "Beholder (Beta)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8784, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8785, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8786, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8787, - "name": "Ben and Ed", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 8788, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8789, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8790, - "name": "Ben and Ed: Blood Party", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 8791, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8792, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8793, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8794, - "name": "Besiege", - "site": "metacritic_com", - "genres": [ - "Strategy", - "Real-Time", - "General" - ] - }, - { - "id": 8795, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8796, - "name": "Borderlands: The Pre-Sequel!", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Shoot 'Em Up" - ] - }, - { - "id": 8797, - "name": "Beyond Good and Evil", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8798, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8799, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8800, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8801, - "name": "Brink", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8802, - "name": "Binary Domain", - "site": "metacritic_com", - "genres": [ - "Tactical", - "General", - "Action Adventure", - "Third-Person", - "Action", - "Sci-Fi", - "Shooter" - ] - }, - { - "id": 8803, - "name": "Broforce", - "site": "spong_com", - "genres": [] - }, - { - "id": 8804, - "name": "Broken Age", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 8805, - "name": "Brothers: A Tale Of Two Sons", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8806, - "name": "Brutal Legend", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8807, - "name": "BioShock", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8808, - "name": "Buff Knight Advanced", - "site": "spong_com", - "genres": [] - }, - { - "id": 8809, - "name": "Bulb Boy", - "site": "spong_com", - "genres": [] - }, - { - "id": 8810, - "name": "Bulletstorm", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8811, - "name": "Call of Cthulhu (2018)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8812, - "name": "BioShock 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8813, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8814, - "name": "Call of Duty", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game: Infantry" - ] - }, - { - "id": 8815, - "name": "BioShock Infinite", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8816, - "name": "Call of Duty 2", - "site": "spong_com", - "genres": [ - "Combat Game: Infantry" - ] - }, - { - "id": 8817, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8818, - "name": "Call of Duty 4: Modern Warfare", - "site": "spong_com", - "genres": [] - }, - { - "id": 8819, - "name": "Call of Duty: Advanced Warfare", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8820, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8821, - "name": "Call of Duty: Black Ops", - "site": "spong_com", - "genres": [] - }, - { - "id": 8822, - "name": "Call of Duty: Black Ops II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8823, - "name": "Call of Duty: Black Ops III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8824, - "name": "Call of Duty: Ghosts", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8825, - "name": "Bionic Commando", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General", - "Action Adventure", - "Third-Person", - "Sci-Fi" - ] - }, - { - "id": 8826, - "name": "Call of Duty: Infinite Warfare", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8827, - "name": "Call of Duty: Modern Warfare 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8828, - "name": "Call of Duty: Modern Warfare 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8829, - "name": "Black Mesa", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8830, - "name": "Call of Duty: United Offensive", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 8831, - "name": "Call of Duty: WWII", - "site": "spong_com", - "genres": [] - }, - { - "id": 8832, - "name": "Call of Duty: World at War", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8833, - "name": "BlackSite: Area 51", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8834, - "name": "Canabalt", - "site": "spong_com", - "genres": [] - }, - { - "id": 8835, - "name": "Castlevania: Lords of Shadow", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8836, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8837, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8838, - "name": "Blades of Time", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 8839, - "name": "Castlevania: Lords of Shadow 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8840, - "name": "Cat Mario", - "site": "spong_com", - "genres": [] - }, - { - "id": 8841, - "name": "Cat Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 8842, - "name": "Child Of Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 8843, - "name": "Blades of Time: Dismal Swamp", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 8844, - "name": "Children of Morta", - "site": "spong_com", - "genres": [] - }, - { - "id": 8845, - "name": "Blood Omen 2: Legacy of Kain", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8846, - "name": "Choice Chamber", - "site": "spong_com", - "genres": [] - }, - { - "id": 8847, - "name": "Claire", - "site": "spong_com", - "genres": [] - }, - { - "id": 8848, - "name": "Clustertruck", - "site": "spong_com", - "genres": [] - }, - { - "id": 8849, - "name": "Cold Fear", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8850, - "name": "Costume Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 8851, - "name": "BloodRayne", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Third-Person", - "Action", - "Shooter", - "Arcade" - ] - }, - { - "id": 8852, - "name": "Costume Quest 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8853, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "spong_com", - "genres": [] - }, - { - "id": 8854, - "name": "Cross of the Dutchman", - "site": "spong_com", - "genres": [] - }, - { - "id": 8855, - "name": "BloodRayne 2", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Third-Person", - "Action", - "Shooter", - "Arcade" - ] - }, - { - "id": 8856, - "name": "Cry Of Fear", - "site": "spong_com", - "genres": [] - }, - { - "id": 8857, - "name": "Crysis", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8858, - "name": "Crysis 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8859, - "name": "Crysis Warhead", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8860, - "name": "BloodRayne: Betrayal", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Platformer", - "General", - "Action Adventure", - "2D", - "Third-Person", - "Action", - "Shooter" - ] - }, - { - "id": 8861, - "name": "Cube Escape: Arles", - "site": "spong_com", - "genres": [] - }, - { - "id": 8862, - "name": "Cube Escape: Case 23", - "site": "spong_com", - "genres": [] - }, - { - "id": 8863, - "name": "Cube Escape: Harvey's Box", - "site": "spong_com", - "genres": [] - }, - { - "id": 8864, - "name": "Bloodbath kavkaz", - "site": "metacritic_com", - "genres": [ - "Top-Down", - "Shooter", - "Action", - "Shoot-'Em-Up" - ] - }, - { - "id": 8865, - "name": "Cube Escape: Seasons", - "site": "spong_com", - "genres": [] - }, - { - "id": 8866, - "name": "Cube Escape: The Lake", - "site": "spong_com", - "genres": [] - }, - { - "id": 8867, - "name": "Cube Escape: The Mill", - "site": "spong_com", - "genres": [] - }, - { - "id": 8868, - "name": "Bloodline: Линия крови", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8869, - "name": "DOOM (2016)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8870, - "name": "Dark Deception", - "site": "spong_com", - "genres": [] - }, - { - "id": 8871, - "name": "Dark Messiah of Might and Magic", - "site": "spong_com", - "genres": [ - "Strategy", - "Adventure: Role Playing" - ] - }, - { - "id": 8872, - "name": "Dark Sector", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8873, - "name": "Dark Souls II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8874, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "spong_com", - "genres": [ - "Compilation", - "Adventure: Role Playing" - ] - }, - { - "id": 8875, - "name": "Blue Estate", - "site": "metacritic_com", - "genres": [ - "Light Gun", - "Shooter", - "Action", - "Rail" - ] - }, - { - "id": 8876, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8877, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8878, - "name": "Blues and Bullets: Episode 1", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8879, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8880, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8881, - "name": "Dark Souls III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8882, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8883, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8884, - "name": "Dark Souls: Prepare to Die Edition", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8885, - "name": "Boogeyman", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "Action Adventure", - "3D", - "Survival" - ] - }, - { - "id": 8886, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8887, - "name": "Dark Souls: Remastered", - "site": "spong_com", - "genres": [] - }, - { - "id": 8888, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8889, - "name": "Borderlands", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8890, - "name": "Darksiders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8891, - "name": "Darksiders II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8892, - "name": "Darksiders III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8893, - "name": "Daylight", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8894, - "name": "Dead Island", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8895, - "name": "Dead Island: Riptide", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Adventure: Survival Horror" - ] - }, - { - "id": 8896, - "name": "Borderlands 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8897, - "name": "Dead Island: Ryder White", - "site": "spong_com", - "genres": [] - }, - { - "id": 8898, - "name": "Dead Rising", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8899, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8900, - "name": "Dead Space", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8901, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8902, - "name": "Dead Space 2", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8903, - "name": "Dead Space 3", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8904, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8905, - "name": "Dead Space 3: Awakened (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8906, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8907, - "name": "Dead by Daylight", - "site": "spong_com", - "genres": [] - }, - { - "id": 8908, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8909, - "name": "Deadlight", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8910, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8911, - "name": "Deadpool", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 8912, - "name": "DeathSpank", - "site": "spong_com", - "genres": [] - }, - { - "id": 8913, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8914, - "name": "DeathSpank: Thongs of Virtue", - "site": "spong_com", - "genres": [] - }, - { - "id": 8915, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8916, - "name": "Deathtrap", - "site": "spong_com", - "genres": [] - }, - { - "id": 8917, - "name": "Demigod", - "site": "spong_com", - "genres": [ - "Strategy", - "Beat 'Em Up", - "Adventure: Role Playing" - ] - }, - { - "id": 8918, - "name": "Descenders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8919, - "name": "Detention", - "site": "spong_com", - "genres": [] - }, - { - "id": 8920, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8921, - "name": "Borderlands: The Pre-Sequel!", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8922, - "name": "Devil May Cry 4", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 8923, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8924, - "name": "Devil May Cry 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 8925, - "name": "Diablo II", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8926, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8927, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8928, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8929, - "name": "Diablo III", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8930, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8931, - "name": "Dig or Die", - "site": "spong_com", - "genres": [] - }, - { - "id": 8932, - "name": "Disciples II: Dark Prophecy", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8933, - "name": "Disciples III: Renaissance", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 8934, - "name": "Brink", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8935, - "name": "Dishonored", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8936, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8937, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8938, - "name": "Distraint", - "site": "spong_com", - "genres": [] - }, - { - "id": 8939, - "name": "Disturbed", - "site": "spong_com", - "genres": [] - }, - { - "id": 8940, - "name": "Broforce", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 8941, - "name": "DmC: Devil May Cry", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash", - "Beat 'Em Up" - ] - }, - { - "id": 8942, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8943, - "name": "Doki Doki Literature Club", - "site": "spong_com", - "genres": [] - }, - { - "id": 8944, - "name": "Don't Knock Twice", - "site": "spong_com", - "genres": [] - }, - { - "id": 8945, - "name": "Doom 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8946, - "name": "Broken Age", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Point-and-Click", - "Adventure", - "3D" - ] - }, - { - "id": 8947, - "name": "Double Dragon Neon", - "site": "spong_com", - "genres": [] - }, - { - "id": 8948, - "name": "Down Stairs", - "site": "spong_com", - "genres": [] - }, - { - "id": 8949, - "name": "Downfall", - "site": "spong_com", - "genres": [] - }, - { - "id": 8950, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "spong_com", - "genres": [] - }, - { - "id": 8951, - "name": "Brothers: A Tale Of Two Sons", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "Linear" - ] - }, - { - "id": 8952, - "name": "Dragon Age II", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8953, - "name": "Dragon Age: Inquisition", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8954, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8955, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8956, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8957, - "name": "Brutal Legend", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "General", - "Action Adventure", - "Action", - "Open-World" - ] - }, - { - "id": 8958, - "name": "Dragon Age: Origins", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Combat Game" - ] - }, - { - "id": 8959, - "name": "Dragon Age: Origins - Awakening", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8960, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8961, - "name": "Buff Knight Advanced", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 8962, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8963, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8964, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8965, - "name": "Dragon's Dogma: Dark Arisen", - "site": "spong_com", - "genres": [] - }, - { - "id": 8966, - "name": "Bulb Boy", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Point-and-Click" - ] - }, - { - "id": 8967, - "name": "Draw a Stickman: Episode 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 8968, - "name": "Draw a Stickman: Episode 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8969, - "name": "DreadOut", - "site": "spong_com", - "genres": [] - }, - { - "id": 8970, - "name": "Dropsy", - "site": "spong_com", - "genres": [] - }, - { - "id": 8971, - "name": "Dude, Stop", - "site": "spong_com", - "genres": [] - }, - { - "id": 8972, - "name": "Duke Nukem Forever", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8973, - "name": "Dungeon Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 8974, - "name": "Bulletstorm", - "site": "metacritic_com", - "genres": [ - "First-Person", - "General", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8975, - "name": "Dying Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 8976, - "name": "Call of Cthulhu (2018)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8977, - "name": "Dying Light: The Following (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8978, - "name": "Emily Wants To Play", - "site": "spong_com", - "genres": [] - }, - { - "id": 8979, - "name": "Escape Dead Island", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8980, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Survival", - "Horror" - ] - }, - { - "id": 8981, - "name": "Evil Defenders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8982, - "name": "Evoland", - "site": "spong_com", - "genres": [] - }, - { - "id": 8983, - "name": "Call of Duty", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8984, - "name": "Evoland 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8985, - "name": "Eyes The Horror Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 8986, - "name": "F.E.A.R.", - "site": "spong_com", - "genres": [ - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 8987, - "name": "F.E.A.R. 2: Project Origin", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 8988, - "name": "F.E.A.R. 2: Reborn", - "site": "spong_com", - "genres": [] - }, - { - "id": 8989, - "name": "F.E.A.R. 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8990, - "name": "F.E.A.R. Extraction Point", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game", - "Add-on pack" - ] - }, - { - "id": 8991, - "name": "Call of Duty 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 8992, - "name": "F.E.A.R. Perseus Mandate", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Add-on pack" - ] - }, - { - "id": 8993, - "name": "Fable (2004)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8994, - "name": "Call of Duty 4: Modern Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 8995, - "name": "Fable III", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8996, - "name": "Fahrenheit", - "site": "spong_com", - "genres": [] - }, - { - "id": 8997, - "name": "Failman", - "site": "spong_com", - "genres": [] - }, - { - "id": 8998, - "name": "Fallout 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8999, - "name": "Call of Duty: Advanced Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9000, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9001, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9002, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9003, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9004, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9005, - "name": "Fallout 4", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9006, - "name": "Call of Duty: Black Ops", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9007, - "name": "Fallout: New Vegas", - "site": "spong_com", - "genres": [] - }, - { - "id": 9008, - "name": "Family Trouble", - "site": "spong_com", - "genres": [] - }, - { - "id": 9009, - "name": "Far Cry", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9010, - "name": "Far Cry 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9011, - "name": "Call of Duty: Black Ops II", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade", - "Modern" - ] - }, - { - "id": 9012, - "name": "Far Cry 3: Blood Dragon", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9013, - "name": "FarSky", - "site": "spong_com", - "genres": [] - }, - { - "id": 9014, - "name": "Final Fantasy III (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9015, - "name": "Final Fantasy IV (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9016, - "name": "Call of Duty: Black Ops III", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 9017, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9018, - "name": "Final Fantasy X HD Remaster", - "site": "spong_com", - "genres": [] - }, - { - "id": 9019, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9020, - "name": "Final Fantasy XIII", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9021, - "name": "Final Fantasy XIII-2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9022, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9023, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9024, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9025, - "name": "Final Fantasy XV", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9026, - "name": "Call of Duty: Ghosts", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9027, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9028, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9029, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9030, - "name": "Call of Duty: Infinite Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Tactical" - ] - }, - { - "id": 9031, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9032, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9033, - "name": "Firewatch", - "site": "spong_com", - "genres": [] - }, - { - "id": 9034, - "name": "First Winter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9035, - "name": "Five Minutes", - "site": "spong_com", - "genres": [] - }, - { - "id": 9036, - "name": "Call of Duty: Modern Warfare 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9037, - "name": "Five Nights At Freddy's", - "site": "spong_com", - "genres": [] - }, - { - "id": 9038, - "name": "Five Nights At Freddy's 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9039, - "name": "Five Nights At Freddy's 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9040, - "name": "Five Nights At Freddy's 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9041, - "name": "Five Nights at Freddy's 3D", - "site": "spong_com", - "genres": [] - }, - { - "id": 9042, - "name": "Five Nights in Anime v3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9043, - "name": "Fran Bow", - "site": "spong_com", - "genres": [] - }, - { - "id": 9044, - "name": "Call of Duty: Modern Warfare 3", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9045, - "name": "Front Mission Evolved", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9046, - "name": "GUN", - "site": "spong_com", - "genres": [] - }, - { - "id": 9047, - "name": "Gemini Rue", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9048, - "name": "Geometry Dash", - "site": "spong_com", - "genres": [] - }, - { - "id": 9049, - "name": "Ghost of a Tale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9050, - "name": "Call of Duty: United Offensive", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Tactical", - "Third-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9051, - "name": "Goat Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9052, - "name": "Grand Theft Auto III", - "site": "spong_com", - "genres": [] - }, - { - "id": 9053, - "name": "Grand Theft Auto: San Andreas", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Combat Game" - ] - }, - { - "id": 9054, - "name": "Grand Theft Auto: Vice City", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Combat Game" - ] - }, - { - "id": 9055, - "name": "Grandpa", - "site": "spong_com", - "genres": [] - }, - { - "id": 9056, - "name": "Call of Duty: WWII", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 9057, - "name": "Grey", - "site": "spong_com", - "genres": [] - }, - { - "id": 9058, - "name": "Grim Fandango Remastered", - "site": "spong_com", - "genres": [] - }, - { - "id": 9059, - "name": "Grow Home", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9060, - "name": "Grow Up", - "site": "spong_com", - "genres": [] - }, - { - "id": 9061, - "name": "Call of Duty: World at War", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9062, - "name": "Guns, Gore & Cannoli", - "site": "spong_com", - "genres": [] - }, - { - "id": 9063, - "name": "Guns, Gore & Cannoli 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9064, - "name": "Half-Life", - "site": "spong_com", - "genres": [ - "Puzzle", - "Shoot 'Em Up" - ] - }, - { - "id": 9065, - "name": "Half-Life 2", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9066, - "name": "Half-Life 2: Episode One", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Add-on pack" - ] - }, - { - "id": 9067, - "name": "Canabalt", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action", - "General" - ] - }, - { - "id": 9068, - "name": "Half-Life 2: Episode Two", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9069, - "name": "Half-Life: Blue Shift", - "site": "spong_com", - "genres": [ - "Puzzle", - "Shoot 'Em Up" - ] - }, - { - "id": 9070, - "name": "Half-Life: Opposing Force", - "site": "spong_com", - "genres": [ - "Add-on pack" - ] - }, - { - "id": 9071, - "name": "Half-Life: Paranoia", - "site": "spong_com", - "genres": [] - }, - { - "id": 9072, - "name": "Castlevania: Lords of Shadow", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9073, - "name": "Half-Life: The Xeno Project", - "site": "spong_com", - "genres": [] - }, - { - "id": 9074, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9075, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9076, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9077, - "name": "Halo: Combat Evolved", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9078, - "name": "Hand of Fate", - "site": "spong_com", - "genres": [ - "Strategy: Trading" - ] - }, - { - "id": 9079, - "name": "Hand of Fate 2", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9080, - "name": "Happy Room", - "site": "spong_com", - "genres": [] - }, - { - "id": 9081, - "name": "Happy Wheels", - "site": "spong_com", - "genres": [] - }, - { - "id": 9082, - "name": "Castlevania: Lords of Shadow 2", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9083, - "name": "Hard Reset", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9084, - "name": "Cat Mario", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9085, - "name": "Hard Reset: Exile (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9086, - "name": "Harts", - "site": "spong_com", - "genres": [] - }, - { - "id": 9087, - "name": "Hatred", - "site": "spong_com", - "genres": [] - }, - { - "id": 9088, - "name": "Hellblade: Senua's Sacrifice", - "site": "spong_com", - "genres": [] - }, - { - "id": 9089, - "name": "Hellgate: London", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9090, - "name": "Her Story", - "site": "spong_com", - "genres": [] - }, - { - "id": 9091, - "name": "Cat Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9092, - "name": "Hero Academy", - "site": "spong_com", - "genres": [] - }, - { - "id": 9093, - "name": "Heroes of Might and Magic III", - "site": "spong_com", - "genres": [ - "Adventure", - "Combat Game" - ] - }, - { - "id": 9094, - "name": "Heroes of Might and Magic IV", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9095, - "name": "Heroes of Might and Magic V", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9096, - "name": "Child Of Light", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action", - "General" - ] - }, - { - "id": 9097, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9098, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "spong_com", - "genres": [ - "Strategy: God game", - "Add-on pack" - ] - }, - { - "id": 9099, - "name": "Homefront", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9100, - "name": "Hotel Remorse", - "site": "spong_com", - "genres": [] - }, - { - "id": 9101, - "name": "Children of Morta", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General" - ] - }, - { - "id": 9102, - "name": "Hotline Miami", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9103, - "name": "Hotline Miami 2: Wrong Number", - "site": "spong_com", - "genres": [] - }, - { - "id": 9104, - "name": "House Flipper", - "site": "spong_com", - "genres": [] - }, - { - "id": 9105, - "name": "Choice Chamber", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 9106, - "name": "I Am Alive", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9107, - "name": "I am Setsuna", - "site": "spong_com", - "genres": [] - }, - { - "id": 9108, - "name": "I hate this game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9109, - "name": "INSIDE", - "site": "spong_com", - "genres": [ - "Platform", - "Puzzle" - ] - }, - { - "id": 9110, - "name": "Claire", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General" - ] - }, - { - "id": 9111, - "name": "Indivisible", - "site": "spong_com", - "genres": [] - }, - { - "id": 9112, - "name": "Injustice: Gods Among Us", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9113, - "name": "Into the Breach", - "site": "spong_com", - "genres": [] - }, - { - "id": 9114, - "name": "Clustertruck", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 9115, - "name": "Jade Empire", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9116, - "name": "Jotun", - "site": "spong_com", - "genres": [] - }, - { - "id": 9117, - "name": "Just Cause", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9118, - "name": "Just Cause 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9119, - "name": "Cold Fear", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 9120, - "name": "Kane & Lynch 2: Dog Days", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9121, - "name": "Killer is Dead: Nightmare Edition", - "site": "spong_com", - "genres": [] - }, - { - "id": 9122, - "name": "King's Bounty: Armored Princess", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 9123, - "name": "King's Bounty: Crossworlds", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 9124, - "name": "Costume Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9125, - "name": "King's Bounty: Dark Side", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9126, - "name": "King's Bounty: The Legend", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9127, - "name": "King's Bounty: Warriors of the North", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9128, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9129, - "name": "Kingdom Rush", - "site": "spong_com", - "genres": [] - }, - { - "id": 9130, - "name": "Kingdom: Classic", - "site": "spong_com", - "genres": [] - }, - { - "id": 9131, - "name": "Kingdoms of Amalur: Reckoning", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9132, - "name": "Knock-Knock", - "site": "spong_com", - "genres": [] - }, - { - "id": 9133, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "spong_com", - "genres": [] - }, - { - "id": 9134, - "name": "Lara Croft and the Guardian of Light", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9135, - "name": "Lara Croft and the Temple of Osiris", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9136, - "name": "Late Shift", - "site": "spong_com", - "genres": [] - }, - { - "id": 9137, - "name": "Layers of Fear", - "site": "spong_com", - "genres": [] - }, - { - "id": 9138, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9139, - "name": "Left 4 Dead", - "site": "spong_com", - "genres": [] - }, - { - "id": 9140, - "name": "Left 4 Dead 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9141, - "name": "Legend Hand of God", - "site": "spong_com", - "genres": [] - }, - { - "id": 9142, - "name": "Legend of Grimrock", - "site": "spong_com", - "genres": [ - "Adventure", - "Combat Game" - ] - }, - { - "id": 9143, - "name": "Legendary", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9144, - "name": "Life After Us: Shipwrecked", - "site": "spong_com", - "genres": [] - }, - { - "id": 9145, - "name": "Life Goes On: Done to Death", - "site": "spong_com", - "genres": [] - }, - { - "id": 9146, - "name": "Life Is Strange", - "site": "spong_com", - "genres": [] - }, - { - "id": 9147, - "name": "Limbo", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9148, - "name": "Little Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9149, - "name": "Little Nightmares - The Depths (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9150, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9151, - "name": "Loki: Heroes of Mythology", - "site": "spong_com", - "genres": [] - }, - { - "id": 9152, - "name": "Lost Planet: Colonies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9153, - "name": "Lost: Via Domus", - "site": "spong_com", - "genres": [] - }, - { - "id": 9154, - "name": "Love Is Strange (DEMO)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9155, - "name": "METAL SLUG", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9156, - "name": "METAL SLUG 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9157, - "name": "MINERVA: Metastasis", - "site": "spong_com", - "genres": [] - }, - { - "id": 9158, - "name": "MOTHERGUNSHIP", - "site": "spong_com", - "genres": [] - }, - { - "id": 9159, - "name": "Machinarium", - "site": "spong_com", - "genres": [] - }, - { - "id": 9160, - "name": "Magibot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9161, - "name": "Magicka", - "site": "spong_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 9162, - "name": "Magicka 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9163, - "name": "Manual Samuel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9164, - "name": "Mario.EXE", - "site": "spong_com", - "genres": [] - }, - { - "id": 9165, - "name": "Marvel vs. Capcom: Infinite", - "site": "spong_com", - "genres": [] - }, - { - "id": 9166, - "name": "Masochisia", - "site": "spong_com", - "genres": [] - }, - { - "id": 9167, - "name": "Masters of Anima", - "site": "spong_com", - "genres": [] - }, - { - "id": 9168, - "name": "Max Payne", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9169, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9170, - "name": "Max Payne 3", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9171, - "name": "Max: The Curse Of Brotherhood", - "site": "spong_com", - "genres": [] - }, - { - "id": 9172, - "name": "McPixel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9173, - "name": "Metal Gear Rising: Revengeance", - "site": "spong_com", - "genres": [ - "Strategy: Stealth" - ] - }, - { - "id": 9174, - "name": "Metal Wolf Chaos XD", - "site": "spong_com", - "genres": [] - }, - { - "id": 9175, - "name": "Metro 2033", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9176, - "name": "Metro 2033 Redux", - "site": "spong_com", - "genres": [] - }, - { - "id": 9177, - "name": "Metro Exodus", - "site": "spong_com", - "genres": [] - }, - { - "id": 9178, - "name": "Metro Last Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 9179, - "name": "Metro Last Light Redux", - "site": "spong_com", - "genres": [] - }, - { - "id": 9180, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9181, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9182, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9183, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9184, - "name": "Middle-earth: Shadow of Mordor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9185, - "name": "Might & Magic Heroes VI", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9186, - "name": "Minecraft Story Mode", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9187, - "name": "Mini Ninjas", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9188, - "name": "Mirror's Edge", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9189, - "name": "Monst", - "site": "spong_com", - "genres": [] - }, - { - "id": 9190, - "name": "Mortal Kombat IX", - "site": "spong_com", - "genres": [] - }, - { - "id": 9191, - "name": "Mortal Kombat X", - "site": "spong_com", - "genres": [] - }, - { - "id": 9192, - "name": "Mother Russia Bleeds", - "site": "spong_com", - "genres": [] - }, - { - "id": 9193, - "name": "Mount Your Friends", - "site": "spong_com", - "genres": [] - }, - { - "id": 9194, - "name": "Mr. Robot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9195, - "name": "Mr.President!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9196, - "name": "Murdered: Soul Suspect", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9197, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "spong_com", - "genres": [ - "Compilation", - "Beat 'Em Up" - ] - }, - { - "id": 9198, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9199, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9200, - "name": "Costume Quest 2", - "site": "metacritic_com", - "genres": [ - "Japanese-Style", - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9201, - "name": "Need for Speed: Underground", - "site": "spong_com", - "genres": [ - "Racing" - ] - }, - { - "id": 9202, - "name": "Never Alone", - "site": "spong_com", - "genres": [] - }, - { - "id": 9203, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9204, - "name": "Neverending Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9205, - "name": "Nevermind", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9206, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9207, - "name": "Cross of the Dutchman", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9208, - "name": "Neverwinter Nights", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9209, - "name": "Neverwinter Nights 2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9210, - "name": "NieR: Automata", - "site": "spong_com", - "genres": [] - }, - { - "id": 9211, - "name": "Nightmare House 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9212, - "name": "Cry Of Fear", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror", - "Survival" - ] - }, - { - "id": 9213, - "name": "Nosferatu: The Wrath of Malachi", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9214, - "name": "Nox Timore", - "site": "spong_com", - "genres": [] - }, - { - "id": 9215, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "spong_com", - "genres": [] - }, - { - "id": 9216, - "name": "Omensight", - "site": "spong_com", - "genres": [] - }, - { - "id": 9217, - "name": "Crysis", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9218, - "name": "One Hand Clapping (DEMO)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9219, - "name": "One Night at Flumpty's", - "site": "spong_com", - "genres": [] - }, - { - "id": 9220, - "name": "One Night at Flumpty's 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9221, - "name": "One Piece: Burning Blood", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9222, - "name": "Orcs Must Die!", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure" - ] - }, - { - "id": 9223, - "name": "Crysis 2", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9224, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9225, - "name": "Ori and the Blind Forest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9226, - "name": "Outlast", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9227, - "name": "Crysis Warhead", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9228, - "name": "Outlast: Whistleblower", - "site": "spong_com", - "genres": [] - }, - { - "id": 9229, - "name": "Overlord", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9230, - "name": "Overlord: Raising Hell", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9231, - "name": "Cube Escape: Arles", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9232, - "name": "Oxenfree", - "site": "spong_com", - "genres": [] - }, - { - "id": 9233, - "name": "Cube Escape: Case 23", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9234, - "name": "Painkiller", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 9235, - "name": "Painkiller: Battle Out of Hell", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9236, - "name": "Painkiller: Overdose", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9237, - "name": "Cube Escape: Harvey's Box", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9238, - "name": "Paint the Town Red", - "site": "spong_com", - "genres": [] - }, - { - "id": 9239, - "name": "Panty Party", - "site": "spong_com", - "genres": [] - }, - { - "id": 9240, - "name": "Papers, Please", - "site": "spong_com", - "genres": [] - }, - { - "id": 9241, - "name": "Peace, Death!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9242, - "name": "Cube Escape: Seasons", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9243, - "name": "Pillars of Eternity", - "site": "spong_com", - "genres": [] - }, - { - "id": 9244, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9245, - "name": "Cube Escape: The Lake", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9246, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9247, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9248, - "name": "Cube Escape: The Mill", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9249, - "name": "Pinstripe", - "site": "spong_com", - "genres": [] - }, - { - "id": 9250, - "name": "DOOM (2016)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9251, - "name": "Pirates of the Caribbean: At World’s End", - "site": "spong_com", - "genres": [] - }, - { - "id": 9252, - "name": "Pizza Delivery 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9253, - "name": "Plague Inc: Evolved", - "site": "spong_com", - "genres": [] - }, - { - "id": 9254, - "name": "Dark Deception", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 9255, - "name": "Plants vs. Zombies", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9256, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9257, - "name": "Pony Island", - "site": "spong_com", - "genres": [] - }, - { - "id": 9258, - "name": "Portal", - "site": "spong_com", - "genres": [ - "Puzzle: Physics" - ] - }, - { - "id": 9259, - "name": "Portal 2", - "site": "spong_com", - "genres": [ - "Puzzle: Physics" - ] - }, - { - "id": 9260, - "name": "Prey (2006)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9261, - "name": "Dark Messiah of Might and Magic", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Shooter", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9262, - "name": "Prince of Persia (2008)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9263, - "name": "Prince of Persia: The Two Thrones", - "site": "spong_com", - "genres": [ - "Adventure", - "Platform" - ] - }, - { - "id": 9264, - "name": "Prospekt", - "site": "spong_com", - "genres": [] - }, - { - "id": 9265, - "name": "Prototype", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9266, - "name": "Dark Sector", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Action", - "Third-Person", - "Arcade" - ] - }, - { - "id": 9267, - "name": "Prototype 2", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9268, - "name": "Psychonauts", - "site": "spong_com", - "genres": [ - "Adventure", - "Platform" - ] - }, - { - "id": 9269, - "name": "PuniTy", - "site": "spong_com", - "genres": [] - }, - { - "id": 9270, - "name": "P·O·L·L·E·N", - "site": "spong_com", - "genres": [] - }, - { - "id": 9271, - "name": "Dark Souls II", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9272, - "name": "Quake 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9273, - "name": "Quantum Break", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9274, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9275, - "name": "Raft", - "site": "spong_com", - "genres": [] - }, - { - "id": 9276, - "name": "Rage", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9277, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9278, - "name": "Rampage Knights", - "site": "spong_com", - "genres": [] - }, - { - "id": 9279, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9280, - "name": "Rayman Legends", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9281, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9282, - "name": "Rayman Origins", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9283, - "name": "Rayman Raving Rabbids", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9284, - "name": "Rayman Raving Rabbids 2", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9285, - "name": "Real Horror Stories", - "site": "spong_com", - "genres": [] - }, - { - "id": 9286, - "name": "Dark Souls III", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9287, - "name": "Redeemer", - "site": "spong_com", - "genres": [] - }, - { - "id": 9288, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9289, - "name": "Reign Of Kings", - "site": "spong_com", - "genres": [] - }, - { - "id": 9290, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9291, - "name": "Reigns", - "site": "spong_com", - "genres": [] - }, - { - "id": 9292, - "name": "Reigns: Her Majesty", - "site": "spong_com", - "genres": [] - }, - { - "id": 9293, - "name": "Dark Souls: Prepare to Die Edition", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9294, - "name": "Resident Evil", - "site": "spong_com", - "genres": [] - }, - { - "id": 9295, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9296, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9297, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9298, - "name": "Resident Evil 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9299, - "name": "Resident Evil 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 9300, - "name": "Dark Souls: Remastered", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9301, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9302, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9303, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9304, - "name": "Resident Evil 6", - "site": "spong_com", - "genres": [] - }, - { - "id": 9305, - "name": "Resident Evil 7: Biohazard", - "site": "spong_com", - "genres": [] - }, - { - "id": 9306, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9307, - "name": "Darksiders", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Modern", - "Fantasy", - "Open-World" - ] - }, - { - "id": 9308, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9309, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9310, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9311, - "name": "Resident Evil: Operation Raccoon City", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 9312, - "name": "Darksiders II", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Modern", - "Fantasy", - "Open-World" - ] - }, - { - "id": 9313, - "name": "Resident Evil: Revelations", - "site": "spong_com", - "genres": [] - }, - { - "id": 9314, - "name": "Resident Evil: Revelations 2", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9315, - "name": "Reus", - "site": "spong_com", - "genres": [ - "Strategy: God game" - ] - }, - { - "id": 9316, - "name": "Rezrog", - "site": "spong_com", - "genres": [] - }, - { - "id": 9317, - "name": "Darksiders III", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Open-World" - ] - }, - { - "id": 9318, - "name": "Rise of the Tomb Raider", - "site": "spong_com", - "genres": [] - }, - { - "id": 9319, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9320, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9321, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9322, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9323, - "name": "Daylight", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror", - "Survival" - ] - }, - { - "id": 9324, - "name": "Risen", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9325, - "name": "Rock of Ages", - "site": "spong_com", - "genres": [] - }, - { - "id": 9326, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "spong_com", - "genres": [] - }, - { - "id": 9327, - "name": "Root Of Evil: The Tailor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9328, - "name": "Runic Rampage", - "site": "spong_com", - "genres": [] - }, - { - "id": 9329, - "name": "Dead Island", - "site": "metacritic_com", - "genres": [ - "Horror", - "Survival", - "Action Adventure" - ] - }, - { - "id": 9330, - "name": "Rust", - "site": "spong_com", - "genres": [] - }, - { - "id": 9331, - "name": "Ryse: Son of Rome", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9332, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9333, - "name": "Dead Island: Riptide", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9334, - "name": "SCP-087", - "site": "spong_com", - "genres": [] - }, - { - "id": 9335, - "name": "SCP-087-B", - "site": "spong_com", - "genres": [] - }, - { - "id": 9336, - "name": "Dead Island: Ryder White", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9337, - "name": "SINNER: Sacrifice for Redemption", - "site": "spong_com", - "genres": [] - }, - { - "id": 9338, - "name": "SWAT 4", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9339, - "name": "Dead Rising", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9340, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9341, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9342, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9343, - "name": "Dead Space", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9344, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9345, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9346, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9347, - "name": "Dead Space 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9348, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9349, - "name": "Sacred", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9350, - "name": "Sacred 3", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9351, - "name": "Dead Space 3", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Shooter", - "Action Adventure", - "Third-Person", - "Linear", - "Action" - ] - }, - { - "id": 9352, - "name": "Sacred Citadel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9353, - "name": "Dead Space 3: Awakened (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9354, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9355, - "name": "Dead by Daylight", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9356, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9357, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9358, - "name": "Saints Row IV", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9359, - "name": "Deadlight", - "site": "metacritic_com", - "genres": [ - "General", - "Platformer", - "2D", - "Action" - ] - }, - { - "id": 9360, - "name": "Saints Row: Gat out of Hell", - "site": "spong_com", - "genres": [] - }, - { - "id": 9361, - "name": "Salt and Sanctuary", - "site": "spong_com", - "genres": [] - }, - { - "id": 9362, - "name": "Deadpool", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "3D", - "Arcade", - "Shooter", - "Action Adventure", - "Third-Person", - "Linear", - "Action" - ] - }, - { - "id": 9363, - "name": "Samsara Room", - "site": "spong_com", - "genres": [] - }, - { - "id": 9364, - "name": "Saw: The Video Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9365, - "name": "Scratches", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9366, - "name": "DeathSpank", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG", - "Action" - ] - }, - { - "id": 9367, - "name": "Seasons after Fall", - "site": "spong_com", - "genres": [ - "Platform", - "Adventure" - ] - }, - { - "id": 9368, - "name": "Serena", - "site": "spong_com", - "genres": [] - }, - { - "id": 9369, - "name": "DeathSpank: Thongs of Virtue", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9370, - "name": "Serious Sam 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9371, - "name": "Serious Sam 3: BFE", - "site": "spong_com", - "genres": [] - }, - { - "id": 9372, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9373, - "name": "Deathtrap", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9374, - "name": "Serious Sam Classic: The First Encounter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9375, - "name": "Serious Sam Classic: The Second Encounter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9376, - "name": "Demigod", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Fantasy", - "MOBA", - "Strategy" - ] - }, - { - "id": 9377, - "name": "Shadow Warrior", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9378, - "name": "Shadow of the Tomb Raider", - "site": "spong_com", - "genres": [] - }, - { - "id": 9379, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9380, - "name": "Descenders", - "site": "metacritic_com", - "genres": [ - "Automobile", - "Arcade", - "Racing" - ] - }, - { - "id": 9381, - "name": "Shantae: Half-Genie Hero", - "site": "spong_com", - "genres": [] - }, - { - "id": 9382, - "name": "Shoot Your Nightmare Halloween Special", - "site": "spong_com", - "genres": [] - }, - { - "id": 9383, - "name": "Shovel Knight: Shovel of Hope", - "site": "spong_com", - "genres": [] - }, - { - "id": 9384, - "name": "Shrek 2: The Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9385, - "name": "Silence on The Line", - "site": "spong_com", - "genres": [] - }, - { - "id": 9386, - "name": "Silent Hill: Alchemilla", - "site": "spong_com", - "genres": [] - }, - { - "id": 9387, - "name": "Silverfall", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9388, - "name": "Silverfall: Earth Awakening", - "site": "spong_com", - "genres": [] - }, - { - "id": 9389, - "name": "Singularity", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9390, - "name": "Slap-Man", - "site": "spong_com", - "genres": [] - }, - { - "id": 9391, - "name": "Slay the Spire", - "site": "spong_com", - "genres": [] - }, - { - "id": 9392, - "name": "Sleeping Dogs", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9393, - "name": "Slender: The Arrival", - "site": "spong_com", - "genres": [] - }, - { - "id": 9394, - "name": "Slendytubbies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9395, - "name": "Slime Rancher", - "site": "spong_com", - "genres": [] - }, - { - "id": 9396, - "name": "Sniper Elite", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9397, - "name": "Sniper Elite 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9398, - "name": "Sniper Elite V2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9399, - "name": "Sophie's Curse", - "site": "spong_com", - "genres": [] - }, - { - "id": 9400, - "name": "South Park: The Fractured But Whole", - "site": "spong_com", - "genres": [] - }, - { - "id": 9401, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9402, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9403, - "name": "South Park: The Stick of Truth", - "site": "spong_com", - "genres": [] - }, - { - "id": 9404, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9405, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9406, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9407, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9408, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9409, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9410, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9411, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9412, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9413, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9414, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9415, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9416, - "name": "Spooky's House of Jump Scares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9417, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9418, - "name": "Star Wars: The Force Unleashed", - "site": "spong_com", - "genres": [] - }, - { - "id": 9419, - "name": "Star Wars: The Force Unleashed II", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash", - "Adventure" - ] - }, - { - "id": 9420, - "name": "SteamWorld Dig", - "site": "spong_com", - "genres": [] - }, - { - "id": 9421, - "name": "SteamWorld Dig 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9422, - "name": "SteamWorld Heist", - "site": "spong_com", - "genres": [] - }, - { - "id": 9423, - "name": "Steampunk Tower 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9424, - "name": "Steel Rats", - "site": "spong_com", - "genres": [] - }, - { - "id": 9425, - "name": "Stories: The Path of Destinies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9426, - "name": "Street Fighter X Tekken", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9428, - "name": "Stupidella", - "site": "spong_com", - "genres": [] - }, - { - "id": 9429, - "name": "Sudeki", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9430, - "name": "Superhot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9431, - "name": "Surgeon Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9432, - "name": "Tales from the Borderlands", - "site": "spong_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 9433, - "name": "Tales of Berseria", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9434, - "name": "Tales of Zestiria", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9435, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9436, - "name": "Tasty Planet: Back for Seconds", - "site": "spong_com", - "genres": [] - }, - { - "id": 9437, - "name": "Tekken 7", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9438, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "spong_com", - "genres": [] - }, - { - "id": 9439, - "name": "Tesla vs Lovecraft", - "site": "spong_com", - "genres": [] - }, - { - "id": 9440, - "name": "Teslagrad", - "site": "spong_com", - "genres": [] - }, - { - "id": 9441, - "name": "The Banner Saga", - "site": "spong_com", - "genres": [] - }, - { - "id": 9442, - "name": "The Banner Saga 2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9443, - "name": "The Bard’s Tale (2004)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9444, - "name": "The Beginner's Guide", - "site": "spong_com", - "genres": [] - }, - { - "id": 9445, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9446, - "name": "The Binding of Isaac: Rebirth", - "site": "spong_com", - "genres": [] - }, - { - "id": 9447, - "name": "The Bunker", - "site": "spong_com", - "genres": [] - }, - { - "id": 9448, - "name": "The Cat Lady", - "site": "spong_com", - "genres": [] - }, - { - "id": 9449, - "name": "The Council", - "site": "spong_com", - "genres": [] - }, - { - "id": 9450, - "name": "The Curse of Blackwater", - "site": "spong_com", - "genres": [] - }, - { - "id": 9451, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "spong_com", - "genres": [] - }, - { - "id": 9452, - "name": "The Darkness II", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure: Survival Horror" - ] - }, - { - "id": 9453, - "name": "The Elder Scrolls IV: Oblivion", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9454, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9455, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9456, - "name": "The Elder Scrolls V: Skyrim", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9457, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack" - ] - }, - { - "id": 9458, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9459, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9460, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9461, - "name": "The Evil Within", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9462, - "name": "The Evil Within 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9463, - "name": "The Evil Within: The Assignment (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9464, - "name": "The Evil Within: The Consequence (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9465, - "name": "The Evil Within: The Executioner (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9466, - "name": "The Expendabros", - "site": "spong_com", - "genres": [] - }, - { - "id": 9467, - "name": "The Final Station", - "site": "spong_com", - "genres": [] - }, - { - "id": 9468, - "name": "The First Tree", - "site": "spong_com", - "genres": [] - }, - { - "id": 9469, - "name": "The Forest", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9470, - "name": "The Hat Man Shadow Ward", - "site": "spong_com", - "genres": [] - }, - { - "id": 9471, - "name": "The Incredible Adventures of Van Helsing", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9472, - "name": "The Incredible Adventures of Van Helsing II", - "site": "spong_com", - "genres": [] - }, - { - "id": 9473, - "name": "The Incredible Adventures of Van Helsing III", - "site": "spong_com", - "genres": [] - }, - { - "id": 9474, - "name": "The Last Remnant", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9475, - "name": "The Mask Man", - "site": "spong_com", - "genres": [] - }, - { - "id": 9476, - "name": "The Punisher (2005)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9477, - "name": "The Secret of Monkey Island: Special Edition", - "site": "spong_com", - "genres": [ - "Adventure: Graphic" - ] - }, - { - "id": 9478, - "name": "The Slippery Slope", - "site": "spong_com", - "genres": [] - }, - { - "id": 9479, - "name": "The Stanley Parable", - "site": "spong_com", - "genres": [] - }, - { - "id": 9480, - "name": "The Walking Dead: A New Frontier", - "site": "spong_com", - "genres": [] - }, - { - "id": 9481, - "name": "The Walking Dead: Michonne", - "site": "spong_com", - "genres": [] - }, - { - "id": 9482, - "name": "The Walking Dead: Season One", - "site": "spong_com", - "genres": [] - }, - { - "id": 9483, - "name": "The Walking Dead: Season Two", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9484, - "name": "The Witcher", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9485, - "name": "The Witcher 2: Assassins of Kings", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9486, - "name": "The Witcher 3: Wild Hunt", - "site": "spong_com", - "genres": [] - }, - { - "id": 9487, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9488, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9489, - "name": "The Wolf Among Us", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9490, - "name": "Thief", - "site": "spong_com", - "genres": [] - }, - { - "id": 9491, - "name": "This Is The Police", - "site": "spong_com", - "genres": [] - }, - { - "id": 9492, - "name": "TimeShift", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9493, - "name": "Titan Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9494, - "name": "Titan Quest Anniversary Edition", - "site": "spong_com", - "genres": [] - }, - { - "id": 9495, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9496, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Combat Game" - ] - }, - { - "id": 9497, - "name": "Titan Souls", - "site": "spong_com", - "genres": [] - }, - { - "id": 9498, - "name": "Titanfall 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9499, - "name": "To The Moon", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Adventure: Point and Click" - ] - }, - { - "id": 9500, - "name": "Tomb Raider", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9501, - "name": "Torchlight", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 9502, - "name": "Torchlight II", - "site": "spong_com", - "genres": [] - }, - { - "id": 9503, - "name": "Toren", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9504, - "name": "Tormentum: Dark Sorrow", - "site": "spong_com", - "genres": [] - }, - { - "id": 9505, - "name": "Total Overdose", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9506, - "name": "Transistor", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9507, - "name": "Trine", - "site": "spong_com", - "genres": [] - }, - { - "id": 9508, - "name": "Trine 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9509, - "name": "Trine 3: The Artifacts of Power", - "site": "spong_com", - "genres": [] - }, - { - "id": 9510, - "name": "Troll Tale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9511, - "name": "Trollface Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9512, - "name": "Trollface Quest 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9513, - "name": "Trollface Quest 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9514, - "name": "Trollface Quest 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9515, - "name": "Trollface Quest 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 9516, - "name": "Trollface Quest 6", - "site": "spong_com", - "genres": [] - }, - { - "id": 9517, - "name": "Trollface Quest 7", - "site": "spong_com", - "genres": [] - }, - { - "id": 9518, - "name": "Trollface Quest: Video Memes", - "site": "spong_com", - "genres": [] - }, - { - "id": 9519, - "name": "Twin Sector", - "site": "spong_com", - "genres": [] - }, - { - "id": 9520, - "name": "Two Worlds", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9521, - "name": "Typical Nightmare", - "site": "spong_com", - "genres": [] - }, - { - "id": 9522, - "name": "Ultimate Epic Battle Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9523, - "name": "Ultra Street Fighter IV", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9524, - "name": "Undertale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9525, - "name": "Unfair Mario", - "site": "spong_com", - "genres": [] - }, - { - "id": 9526, - "name": "Unmechanical", - "site": "spong_com", - "genres": [] - }, - { - "id": 9527, - "name": "Unmechanical: Extended (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9528, - "name": "Until the last", - "site": "spong_com", - "genres": [] - }, - { - "id": 9529, - "name": "Valiant Hearts: The Great War", - "site": "spong_com", - "genres": [] - }, - { - "id": 9530, - "name": "Vanish", - "site": "spong_com", - "genres": [] - }, - { - "id": 9531, - "name": "Victor Vran", - "site": "spong_com", - "genres": [] - }, - { - "id": 9532, - "name": "Vikings: Wolves of Midgard", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9533, - "name": "Visage", - "site": "spong_com", - "genres": [] - }, - { - "id": 9534, - "name": "Wanted: Weapons of Fate", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9535, - "name": "Warcraft III: Reign of Chaos", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9536, - "name": "Warcraft III: The Frozen Throne", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9537, - "name": "Warhammer 40.000: Space Marine", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9538, - "name": "Warhammer: Chaosbane", - "site": "spong_com", - "genres": [] - }, - { - "id": 9539, - "name": "We Happy Few", - "site": "spong_com", - "genres": [] - }, - { - "id": 9540, - "name": "Whack The Thief", - "site": "spong_com", - "genres": [] - }, - { - "id": 9541, - "name": "Whack Your Boss", - "site": "spong_com", - "genres": [] - }, - { - "id": 9542, - "name": "Whack Your Ex", - "site": "spong_com", - "genres": [] - }, - { - "id": 9543, - "name": "Whack Your Neighbour", - "site": "spong_com", - "genres": [] - }, - { - "id": 9544, - "name": "Whack Your Teacher", - "site": "spong_com", - "genres": [] - }, - { - "id": 9545, - "name": "What Remains of Edith Finch", - "site": "spong_com", - "genres": [] - }, - { - "id": 9546, - "name": "Will Rock", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9547, - "name": "Willy-Nilly Knight", - "site": "spong_com", - "genres": [] - }, - { - "id": 9548, - "name": "Wizard of Legend", - "site": "spong_com", - "genres": [] - }, - { - "id": 9549, - "name": "Wolfenstein", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9550, - "name": "Wolfenstein: The New Order", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9551, - "name": "Wolfenstein: The Old Blood", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9552, - "name": "Woolfe - The Red Hood Diaries", - "site": "spong_com", - "genres": [] - }, - { - "id": 9553, - "name": "Worms 2", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Shoot 'Em Up" - ] - }, - { - "id": 9554, - "name": "X-Blades / Ониблэйд", - "site": "spong_com", - "genres": [] - }, - { - "id": 9555, - "name": "X-Morph: Defense", - "site": "spong_com", - "genres": [] - }, - { - "id": 9556, - "name": "Yandere School", - "site": "spong_com", - "genres": [] - }, - { - "id": 9557, - "name": "Year Walk", - "site": "spong_com", - "genres": [] - }, - { - "id": 9558, - "name": "ZOMBI", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9559, - "name": "Zero Escape: Zero Time Dilemma", - "site": "spong_com", - "genres": [] - }, - { - "id": 9560, - "name": "Ziggurat", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9561, - "name": "Zombie Driver HD", - "site": "spong_com", - "genres": [] - }, - { - "id": 9562, - "name": "Zombie Party", - "site": "spong_com", - "genres": [ - "Various: Party Game" - ] - }, - { - "id": 9563, - "name": "Zombie Vikings", - "site": "spong_com", - "genres": [] - }, - { - "id": 9564, - "name": "Zuma Deluxe", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 9565, - "name": "Zuma's Revenge!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9566, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "spong_com", - "genres": [] - }, - { - "id": 9567, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "spong_com", - "genres": [] - }, - { - "id": 9568, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "spong_com", - "genres": [] - }, - { - "id": 9569, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9570, - "name": "Невероятные Приключения Трояна", - "site": "spong_com", - "genres": [] - }, - { - "id": 9571, - "name": "Невероятные Приключения Трояна 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9572, - "name": "Невероятные Приключения Трояна 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9573, - "name": "Предатор", - "site": "spong_com", - "genres": [] - }, - { - "id": 9574, - "name": "Стоп-Гоп", - "site": "spong_com", - "genres": [] - }, - { - "id": 9575, - "name": "Стоп-Гоп 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9576, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9577, - "name": "Detention", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9578, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9579, - "name": "Devil May Cry 4", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "Action Adventure", - "General", - "Linear", - "Fantasy", - "Action" - ] - }, - { - "id": 9580, - "name": "Devil May Cry 5", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9581, - "name": "Diablo II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9582, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9583, - "name": "Diablo III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9584, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9585, - "name": "Dig or Die", - "site": "metacritic_com", - "genres": [ - "Sandbox", - "General", - "Action", - "Action Adventure" - ] - }, - { - "id": 9586, - "name": "Disciples II: Dark Prophecy", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9587, - "name": "Disciples III: Renaissance", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9588, - "name": "Dishonored", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Action Adventure" - ] - }, - { - "id": 9589, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9590, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9591, - "name": "Distraint", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9592, - "name": "Disturbed", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9593, - "name": "DmC: Devil May Cry", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "General", - "Linear", - "Fantasy", - "Action" - ] - }, - { - "id": 9594, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9595, - "name": "Doki Doki Literature Club", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 9596, - "name": "Don't Knock Twice", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9597, - "name": "Doom 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9598, - "name": "Double Dragon Neon", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9599, - "name": "Down Stairs", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9600, - "name": "Downfall", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9601, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9602, - "name": "Dragon Age II", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9603, - "name": "Dragon Age: Inquisition", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG", - "Western-Style" - ] - }, - { - "id": 9604, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9605, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9606, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9607, - "name": "Dragon Age: Origins", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9608, - "name": "Dragon Age: Origins - Awakening", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9609, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9610, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9611, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9612, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9613, - "name": "Dragon's Dogma: Dark Arisen", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9614, - "name": "Draw a Stickman: Episode 1", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9615, - "name": "Draw a Stickman: Episode 2", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9616, - "name": "DreadOut", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9617, - "name": "Dropsy", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9618, - "name": "Dude, Stop", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Party / Minigame" - ] - }, - { - "id": 9619, - "name": "Duke Nukem Forever", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9620, - "name": "Dungeon Nightmares", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "3D", - "General", - "Action" - ] - }, - { - "id": 9621, - "name": "Dying Light", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9622, - "name": "Dying Light: The Following (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9623, - "name": "Emily Wants To Play", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9624, - "name": "Escape Dead Island", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Survival", - "Adventure", - "3D", - "Third-Person", - "Modern" - ] - }, - { - "id": 9625, - "name": "Evil Defenders", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 9626, - "name": "Evoland", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 9627, - "name": "Evoland 2", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9628, - "name": "Eyes The Horror Game", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9629, - "name": "F.E.A.R.", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9630, - "name": "F.E.A.R. 2: Project Origin", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9631, - "name": "F.E.A.R. 2: Reborn", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9632, - "name": "F.E.A.R. 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Modern", - "Action" - ] - }, - { - "id": 9633, - "name": "F.E.A.R. Extraction Point", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9634, - "name": "F.E.A.R. Perseus Mandate", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9635, - "name": "Fable (2004)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9636, - "name": "Fable III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9637, - "name": "Fahrenheit", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9638, - "name": "Failman", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9639, - "name": "Fallout 3", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9640, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9641, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9642, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9643, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9644, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9645, - "name": "Fallout 4", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9646, - "name": "Fallout: New Vegas", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Action RPG", - "Western-Style" - ] - }, - { - "id": 9647, - "name": "Family Trouble", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9648, - "name": "Far Cry", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9649, - "name": "Far Cry 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9650, - "name": "Far Cry 3: Blood Dragon", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9651, - "name": "FarSky", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action", - "Action Adventure" - ] - }, - { - "id": 9652, - "name": "Final Fantasy III (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9653, - "name": "Final Fantasy IV (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9654, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9655, - "name": "Final Fantasy X HD Remaster", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Console-style RPG" - ] - }, - { - "id": 9656, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9657, - "name": "Final Fantasy XIII", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9658, - "name": "Final Fantasy XIII-2", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9659, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9660, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9661, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9662, - "name": "Final Fantasy XV", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9663, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9664, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9665, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9666, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9667, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9668, - "name": "Firewatch", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9669, - "name": "First Winter", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9670, - "name": "Five Minutes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9671, - "name": "Five Nights At Freddy's", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9672, - "name": "Five Nights At Freddy's 2", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9673, - "name": "Five Nights At Freddy's 3", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9674, - "name": "Five Nights At Freddy's 4", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action", - "Action Adventure" - ] - }, - { - "id": 9675, - "name": "Five Nights at Freddy's 3D", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9676, - "name": "Five Nights in Anime v3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9677, - "name": "Fran Bow", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9678, - "name": "Front Mission Evolved", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9679, - "name": "GUN", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9680, - "name": "Gemini Rue", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Sci-Fi", - "Third-Person", - "General", - "Point-and-Click" - ] - }, - { - "id": 9681, - "name": "Geometry Dash", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Music", - "Rhythm", - "Action" - ] - }, - { - "id": 9682, - "name": "Ghost of a Tale", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9683, - "name": "Goat Simulator", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation", - "Adventure" - ] - }, - { - "id": 9684, - "name": "Grand Theft Auto III", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9685, - "name": "Grand Theft Auto: San Andreas", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9686, - "name": "Grand Theft Auto: Vice City", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9687, - "name": "Grandpa", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9688, - "name": "Grey", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9689, - "name": "Grim Fandango Remastered", - "site": "metacritic_com", - "genres": [ - "Adventure", - "3D", - "Third-Person", - "General", - "Fantasy" - ] - }, - { - "id": 9690, - "name": "Grow Home", - "site": "metacritic_com", - "genres": [ - "Sandbox", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9691, - "name": "Grow Up", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9692, - "name": "Guns, Gore & Cannoli", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9693, - "name": "Guns, Gore & Cannoli 2", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9694, - "name": "Half-Life", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9695, - "name": "Half-Life 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9696, - "name": "Half-Life 2: Episode One", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9697, - "name": "Half-Life 2: Episode Two", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9698, - "name": "Half-Life: Blue Shift", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9699, - "name": "Half-Life: Opposing Force", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9700, - "name": "Half-Life: Paranoia", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9701, - "name": "Half-Life: The Xeno Project", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9702, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9703, - "name": "Halo: Combat Evolved", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Sci-Fi" - ] - }, - { - "id": 9704, - "name": "Hand of Fate", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Role-Playing", - "Board / Card Game" - ] - }, - { - "id": 9705, - "name": "Hand of Fate 2", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Board / Card Game" - ] - }, - { - "id": 9706, - "name": "Happy Room", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9707, - "name": "Happy Wheels", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9708, - "name": "Hard Reset", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9709, - "name": "Hard Reset: Exile (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9710, - "name": "Harts", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9711, - "name": "Hatred", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shoot-'Em-Up", - "Top-Down", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9712, - "name": "Hellblade: Senua's Sacrifice", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9713, - "name": "Hellgate: London", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9714, - "name": "Her Story", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 9715, - "name": "Hero Academy", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9716, - "name": "Heroes of Might and Magic III", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9717, - "name": "Heroes of Might and Magic IV", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9718, - "name": "Heroes of Might and Magic V", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9719, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9720, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9721, - "name": "Homefront", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9722, - "name": "Hotel Remorse", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9723, - "name": "Hotline Miami", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action", - "Action Adventure" - ] - }, - { - "id": 9724, - "name": "Hotline Miami 2: Wrong Number", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action", - "Action Adventure" - ] - }, - { - "id": 9725, - "name": "House Flipper", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Virtual Life", - "Virtual" - ] - }, - { - "id": 9726, - "name": "I Am Alive", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9727, - "name": "I am Setsuna", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style" - ] - }, - { - "id": 9728, - "name": "I hate this game", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9729, - "name": "INSIDE", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9730, - "name": "Indivisible", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9731, - "name": "Injustice: Gods Among Us", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9732, - "name": "Into the Breach", - "site": "metacritic_com", - "genres": [ - "Tactics", - "General", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 9733, - "name": "Jade Empire", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9734, - "name": "Jotun", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9735, - "name": "Just Cause", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9736, - "name": "Just Cause 2", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9737, - "name": "Kane & Lynch 2: Dog Days", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9738, - "name": "Killer is Dead: Nightmare Edition", - "site": "metacritic_com", - "genres": [ - "General", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9739, - "name": "King's Bounty: Armored Princess", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 9740, - "name": "King's Bounty: Crossworlds", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9741, - "name": "King's Bounty: Dark Side", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9742, - "name": "King's Bounty: The Legend", - "site": "metacritic_com", - "genres": [ - "Tactics", - "Turn-Based", - "Strategy", - "General", - "Fantasy" - ] - }, - { - "id": 9743, - "name": "King's Bounty: Warriors of the North", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9744, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9745, - "name": "Kingdom Rush", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Defense", - "Action" - ] - }, - { - "id": 9746, - "name": "Kingdom: Classic", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9747, - "name": "Kingdoms of Amalur: Reckoning", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9748, - "name": "Knock-Knock", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General" - ] - }, - { - "id": 9749, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9750, - "name": "Lara Croft and the Guardian of Light", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9751, - "name": "Lara Croft and the Temple of Osiris", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action Adventure", - "Linear", - "Action" - ] - }, - { - "id": 9752, - "name": "Late Shift", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9753, - "name": "Layers of Fear", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9754, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9755, - "name": "Left 4 Dead", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9756, - "name": "Left 4 Dead 2", - "site": "metacritic_com", - "genres": [ - "Tactical", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9757, - "name": "Legend Hand of God", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9758, - "name": "Legend of Grimrock", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9759, - "name": "Legendary", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9760, - "name": "Life After Us: Shipwrecked", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9761, - "name": "Life Goes On: Done to Death", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9762, - "name": "Life Is Strange", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure", - "Sci-Fi" - ] - }, - { - "id": 9763, - "name": "Limbo", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9764, - "name": "Little Nightmares", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 9765, - "name": "Little Nightmares - The Depths (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9766, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9767, - "name": "Loki: Heroes of Mythology", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9768, - "name": "Lost Planet: Colonies", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9769, - "name": "Lost: Via Domus", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Action Adventure" - ] - }, - { - "id": 9770, - "name": "Love Is Strange (DEMO)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9771, - "name": "METAL SLUG", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9772, - "name": "METAL SLUG 3", - "site": "metacritic_com", - "genres": [ - "Horizontal", - "Vertical", - "Shoot-'Em-Up", - "Shooter", - "Action" - ] - }, - { - "id": 9773, - "name": "MINERVA: Metastasis", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9774, - "name": "MOTHERGUNSHIP", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9775, - "name": "Machinarium", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Sci-Fi", - "Third-Person", - "General", - "Point-and-Click" - ] - }, - { - "id": 9776, - "name": "Magibot", - "site": "metacritic_com", - "genres": [ - "Action", - "2D", - "Platformer", - "Puzzle" - ] - }, - { - "id": 9777, - "name": "Magicka", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9778, - "name": "Magicka 2", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9779, - "name": "Manual Samuel", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 9780, - "name": "Mario.EXE", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9781, - "name": "Marvel vs. Capcom: Infinite", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9782, - "name": "Masochisia", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9783, - "name": "Masters of Anima", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "MOBA", - "Strategy" - ] - }, - { - "id": 9784, - "name": "Max Payne", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9785, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9786, - "name": "Max Payne 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9787, - "name": "Max: The Curse Of Brotherhood", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9788, - "name": "McPixel", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9789, - "name": "Metal Gear Rising: Revengeance", - "site": "metacritic_com", - "genres": [ - "Tactical", - "Shooter", - "Action Adventure", - "Third-Person", - "General", - "Modern", - "Action" - ] - }, - { - "id": 9790, - "name": "Metal Wolf Chaos XD", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Combat", - "Vehicle" - ] - }, - { - "id": 9791, - "name": "Metro 2033", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9792, - "name": "Metro 2033 Redux", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9793, - "name": "Metro Exodus", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9794, - "name": "Metro Last Light", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9795, - "name": "Metro Last Light Redux", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9796, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9797, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9798, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9799, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9800, - "name": "Middle-earth: Shadow of Mordor", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9801, - "name": "Might & Magic Heroes VI", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Turn-Based", - "Strategy", - "General", - "Fantasy" - ] - }, - { - "id": 9802, - "name": "Minecraft Story Mode", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9803, - "name": "Mini Ninjas", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9804, - "name": "Mirror's Edge", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action Adventure", - "General", - "Modern", - "Action" - ] - }, - { - "id": 9805, - "name": "Monst", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9806, - "name": "Mortal Kombat IX", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9807, - "name": "Mortal Kombat X", - "site": "metacritic_com", - "genres": [ - "3D", - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9808, - "name": "Mother Russia Bleeds", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9809, - "name": "Mount Your Friends", - "site": "metacritic_com", - "genres": [ - "General", - "Sports" - ] - }, - { - "id": 9810, - "name": "Mr. Robot", - "site": "metacritic_com", - "genres": [ - "General", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9811, - "name": "Mr.President!", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9812, - "name": "Murdered: Soul Suspect", - "site": "metacritic_com", - "genres": [ - "Horror", - "Survival", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9813, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9814, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9815, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9816, - "name": "Need for Speed: Underground", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Automobile", - "GT / Street", - "Racing", - "Driving" - ] - }, - { - "id": 9817, - "name": "Never Alone", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9818, - "name": "Neverending Nightmares", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9819, - "name": "Nevermind", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Adventure", - "First-Person", - "3D", - "Linear" - ] - }, - { - "id": 9820, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9821, - "name": "Neverwinter Nights", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9822, - "name": "Neverwinter Nights 2", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9823, - "name": "NieR: Automata", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9824, - "name": "Nightmare House 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9825, - "name": "Nosferatu: The Wrath of Malachi", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9826, - "name": "Nox Timore", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9827, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9828, - "name": "Omensight", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9829, - "name": "One Hand Clapping (DEMO)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9830, - "name": "One Night at Flumpty's", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9831, - "name": "One Night at Flumpty's 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9832, - "name": "One Piece: Burning Blood", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9833, - "name": "Orcs Must Die!", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Defense", - "Action" - ] - }, - { - "id": 9834, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9835, - "name": "Ori and the Blind Forest", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9836, - "name": "Outlast", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9837, - "name": "Outlast: Whistleblower", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9838, - "name": "Overlord", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9839, - "name": "Overlord: Raising Hell", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9840, - "name": "Oxenfree", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9841, - "name": "Painkiller", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9842, - "name": "Painkiller: Battle Out of Hell", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9843, - "name": "Painkiller: Overdose", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9844, - "name": "Paint the Town Red", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9845, - "name": "Panty Party", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9846, - "name": "Papers, Please", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9847, - "name": "Peace, Death!", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Career", - "Virtual" - ] - }, - { - "id": 9848, - "name": "Pillars of Eternity", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9849, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9850, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9851, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9852, - "name": "Pinstripe", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General" - ] - }, - { - "id": 9853, - "name": "Pirates of the Caribbean: At World’s End", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9854, - "name": "Pizza Delivery 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9855, - "name": "Plague Inc: Evolved", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Sci-Fi", - "Strategy" - ] - }, - { - "id": 9856, - "name": "Plants vs. Zombies", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Fantasy", - "Defense" - ] - }, - { - "id": 9857, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "metacritic_com", - "genres": [ - "Tactical", - "Shooter", - "Third-Person", - "General", - "Action" - ] - }, - { - "id": 9858, - "name": "Pony Island", - "site": "metacritic_com", - "genres": [ - "General", - "Puzzle" - ] - }, - { - "id": 9859, - "name": "Portal", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9860, - "name": "Portal 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9861, - "name": "Prey (2006)", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9862, - "name": "Prince of Persia (2008)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9863, - "name": "Prince of Persia: The Two Thrones", - "site": "metacritic_com", - "genres": [ - "Linear", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9864, - "name": "Prospekt", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9865, - "name": "Prototype", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9866, - "name": "Prototype 2", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9867, - "name": "Psychonauts", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 9868, - "name": "PuniTy", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9869, - "name": "P·O·L·L·E·N", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9870, - "name": "Quake 4", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9871, - "name": "Quantum Break", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9872, - "name": "Raft", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9873, - "name": "Rage", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9874, - "name": "Rampage Knights", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9875, - "name": "Rayman Legends", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9876, - "name": "Rayman Origins", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action Adventure", - "Fantasy", - "Action" - ] - }, - { - "id": 9877, - "name": "Rayman Raving Rabbids", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Miscellaneous", - "Party", - "3D", - "Party / Minigame", - "Action" - ] - }, - { - "id": 9878, - "name": "Rayman Raving Rabbids 2", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Party / Minigame", - "Party" - ] - }, - { - "id": 9879, - "name": "Real Horror Stories", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9880, - "name": "Redeemer", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 9881, - "name": "Reign Of Kings", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9882, - "name": "Reigns", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation" - ] - }, - { - "id": 9883, - "name": "Reigns: Her Majesty", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation" - ] - }, - { - "id": 9884, - "name": "Resident Evil", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9885, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9886, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9887, - "name": "Resident Evil 4", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9888, - "name": "Resident Evil 5", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9889, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9890, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9891, - "name": "Resident Evil 6", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9892, - "name": "Resident Evil 7: Biohazard", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9893, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9894, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9895, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9896, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9897, - "name": "Resident Evil: Operation Raccoon City", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Sci-Fi", - "Horror", - "Shooter", - "Action Adventure", - "Third-Person", - "Action" - ] - }, - { - "id": 9898, - "name": "Resident Evil: Revelations", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9899, - "name": "Resident Evil: Revelations 2", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9900, - "name": "Reus", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9901, - "name": "Rezrog", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Roguelike" - ] - }, - { - "id": 9902, - "name": "Rise of the Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 9903, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9904, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9905, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9906, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9907, - "name": "Risen", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9908, - "name": "Rock of Ages", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Action" - ] - }, - { - "id": 9909, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9910, - "name": "Root Of Evil: The Tailor", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9911, - "name": "Runic Rampage", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9912, - "name": "Rust", - "site": "metacritic_com", - "genres": [ - "Historic", - "Survival", - "Action Adventure" - ] - }, - { - "id": 9913, - "name": "Ryse: Son of Rome", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9914, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9915, - "name": "SCP-087", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9916, - "name": "SCP-087-B", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9917, - "name": "SINNER: Sacrifice for Redemption", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9918, - "name": "SWAT 4", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9919, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9920, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9921, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9922, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9923, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9924, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9925, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9926, - "name": "Sacred", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9927, - "name": "Sacred 3", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9928, - "name": "Sacred Citadel", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9929, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9930, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9931, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9932, - "name": "Saints Row IV", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9933, - "name": "Saints Row: Gat out of Hell", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9934, - "name": "Salt and Sanctuary", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9935, - "name": "Samsara Room", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9936, - "name": "Saw: The Video Game", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9937, - "name": "Scratches", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9938, - "name": "Seasons after Fall", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9939, - "name": "Serena", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Modern", - "Adventure" - ] - }, - { - "id": 9940, - "name": "Serious Sam 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9941, - "name": "Serious Sam 3: BFE", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9942, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9943, - "name": "Serious Sam Classic: The First Encounter", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9944, - "name": "Serious Sam Classic: The Second Encounter", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9945, - "name": "Shadow Warrior", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9946, - "name": "Shadow of the Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9947, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9948, - "name": "Shantae: Half-Genie Hero", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9949, - "name": "Shoot Your Nightmare Halloween Special", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9950, - "name": "Shovel Knight: Shovel of Hope", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9951, - "name": "Shrek 2: The Game", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9952, - "name": "Silence on The Line", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9953, - "name": "Silent Hill: Alchemilla", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9954, - "name": "Silverfall", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9955, - "name": "Silverfall: Earth Awakening", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9956, - "name": "Singularity", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9957, - "name": "Slap-Man", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9958, - "name": "Slay the Spire", - "site": "metacritic_com", - "genres": [ - "Turn-Based", - "Card Battle", - "Strategy" - ] - }, - { - "id": 9959, - "name": "Sleeping Dogs", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9960, - "name": "Slender: The Arrival", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9961, - "name": "Slendytubbies", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9962, - "name": "Slime Rancher", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9963, - "name": "Sniper Elite", - "site": "metacritic_com", - "genres": [ - "Historic", - "Tactical", - "Shooter", - "Third-Person", - "Action" - ] - }, - { - "id": 9964, - "name": "Sniper Elite 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9965, - "name": "Sniper Elite V2", - "site": "metacritic_com", - "genres": [ - "Historic", - "Tactical", - "Shooter", - "Third-Person", - "Action" - ] - }, - { - "id": 9966, - "name": "Sophie's Curse", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9967, - "name": "South Park: The Fractured But Whole", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9968, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9969, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9970, - "name": "South Park: The Stick of Truth", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9971, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "metacritic_com", - "genres": [ - "General", - "Hidden Object", - "Puzzle", - "Adventure" - ] - }, - { - "id": 9972, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9973, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9974, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9975, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9976, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9977, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9978, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9979, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9980, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9981, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9982, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9983, - "name": "Spooky's House of Jump Scares", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9984, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9985, - "name": "Star Wars: The Force Unleashed", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "General", - "Action" - ] - }, - { - "id": 9986, - "name": "Star Wars: The Force Unleashed II", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9987, - "name": "SteamWorld Dig", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9988, - "name": "SteamWorld Dig 2", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9989, - "name": "SteamWorld Heist", - "site": "metacritic_com", - "genres": [ - "Strategy", - "Turn-Based", - "General", - "Sci-Fi" - ] - }, - { - "id": 9990, - "name": "Steampunk Tower 2", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 9991, - "name": "Steel Rats", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Combat", - "Vehicle" - ] - }, - { - "id": 9992, - "name": "Stories: The Path of Destinies", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9993, - "name": "Street Fighter X Tekken", - "site": "metacritic_com", - "genres": [ - "3D", - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9995, - "name": "Stupidella", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9996, - "name": "Sudeki", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9997, - "name": "Superhot", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Tactical", - "Action" - ] - }, - { - "id": 9998, - "name": "Surgeon Simulator", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Virtual", - "Career", - "General", - "Action" - ] - }, - { - "id": 9999, - "name": "Tales from the Borderlands", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10000, - "name": "Tales of Berseria", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG", - "Japanese-Style" - ] - }, - { - "id": 10001, - "name": "Tales of Zestiria", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG", - "Japanese-Style" - ] - }, - { - "id": 10002, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10003, - "name": "Tasty Planet: Back for Seconds", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Puzzle", - "Adventure" - ] - }, - { - "id": 10004, - "name": "Tekken 7", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 10005, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10006, - "name": "Tesla vs Lovecraft", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Top-Down", - "Shoot-'Em-Up", - "Action" - ] - }, - { - "id": 10007, - "name": "Teslagrad", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10008, - "name": "The Banner Saga", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Tactics", - "Turn-Based", - "Strategy", - "General" - ] - }, - { - "id": 10009, - "name": "The Banner Saga 2", - "site": "metacritic_com", - "genres": [ - "Tactics", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 10010, - "name": "The Bard’s Tale (2004)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10011, - "name": "The Beginner's Guide", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10012, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10013, - "name": "The Binding of Isaac: Rebirth", - "site": "metacritic_com", - "genres": [ - "Horror", - "Shoot-'Em-Up", - "Top-Down", - "Survival", - "Shooter", - "Action Adventure", - "Action" - ] - }, - { - "id": 10014, - "name": "The Bunker", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 10015, - "name": "The Cat Lady", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10016, - "name": "The Council", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10017, - "name": "The Curse of Blackwater", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10018, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10019, - "name": "The Darkness II", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 10020, - "name": "The Elder Scrolls IV: Oblivion", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 10021, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10022, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10023, - "name": "The Elder Scrolls V: Skyrim", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 10024, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10025, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10026, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10027, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10028, - "name": "The Evil Within", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10029, - "name": "The Evil Within 2", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10030, - "name": "The Evil Within: The Assignment (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10031, - "name": "The Evil Within: The Consequence (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10032, - "name": "The Evil Within: The Executioner (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10033, - "name": "The Expendabros", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10034, - "name": "The Final Station", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 10035, - "name": "The First Tree", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10036, - "name": "The Forest", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10037, - "name": "The Hat Man Shadow Ward", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10038, - "name": "The Incredible Adventures of Van Helsing", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10039, - "name": "The Incredible Adventures of Van Helsing II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10040, - "name": "The Incredible Adventures of Van Helsing III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10041, - "name": "The Last Remnant", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 10042, - "name": "The Mask Man", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10043, - "name": "The Punisher (2005)", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 10044, - "name": "The Secret of Monkey Island: Special Edition", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 10045, - "name": "The Slippery Slope", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10046, - "name": "The Stanley Parable", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "3D", - "General", - "Modern", - "Action" - ] - }, - { - "id": 10047, - "name": "The Walking Dead: A New Frontier", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10048, - "name": "The Walking Dead: Michonne", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10049, - "name": "The Walking Dead: Season One", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10050, - "name": "The Walking Dead: Season Two", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10051, - "name": "The Witcher", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10052, - "name": "The Witcher 2: Assassins of Kings", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10053, - "name": "The Witcher 3: Wild Hunt", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10054, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10055, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10056, - "name": "The Wolf Among Us", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Adventure", - "General", - "Fantasy", - "Point-and-Click" - ] - }, - { - "id": 10057, - "name": "Thief", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 10058, - "name": "This Is The Police", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Career", - "Virtual" - ] - }, - { - "id": 10059, - "name": "TimeShift", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 10060, - "name": "Titan Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10061, - "name": "Titan Quest Anniversary Edition", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10062, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10063, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10064, - "name": "Titan Souls", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10065, - "name": "Titanfall 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 10066, - "name": "To The Moon", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10067, - "name": "Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Modern", - "Action Adventure" - ] - }, - { - "id": 10068, - "name": "Torchlight", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10069, - "name": "Torchlight II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10070, - "name": "Toren", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 10071, - "name": "Tormentum: Dark Sorrow", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Point-and-Click", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10072, - "name": "Total Overdose", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10073, - "name": "Transistor", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10074, - "name": "Trine", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Fantasy", - "Action" - ] - }, - { - "id": 10075, - "name": "Trine 2", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10076, - "name": "Trine 3: The Artifacts of Power", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10077, - "name": "Troll Tale", - "site": "metacritic_com", - "genres": [ - "General", - "Puzzle" - ] - }, - { - "id": 10078, - "name": "Trollface Quest", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10079, - "name": "Trollface Quest 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10080, - "name": "Trollface Quest 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10081, - "name": "Trollface Quest 4", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10082, - "name": "Trollface Quest 5", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10083, - "name": "Trollface Quest 6", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10084, - "name": "Trollface Quest 7", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10085, - "name": "Trollface Quest: Video Memes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10086, - "name": "Twin Sector", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Modern", - "Action Adventure" - ] - }, - { - "id": 10087, - "name": "Two Worlds", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "General", - "Role-Playing" - ] - }, - { - "id": 10088, - "name": "Typical Nightmare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10089, - "name": "Ultimate Epic Battle Simulator", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 10090, - "name": "Ultra Street Fighter IV", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 10091, - "name": "Undertale", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style" - ] - }, - { - "id": 10092, - "name": "Unfair Mario", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10093, - "name": "Unmechanical", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Puzzle", - "Miscellaneous", - "General", - "Action" - ] - }, - { - "id": 10094, - "name": "Unmechanical: Extended (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10095, - "name": "Until the last", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10096, - "name": "Valiant Hearts: The Great War", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10097, - "name": "Vanish", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10098, - "name": "Victor Vran", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10099, - "name": "Vikings: Wolves of Midgard", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10100, - "name": "Visage", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10101, - "name": "Wanted: Weapons of Fate", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 10102, - "name": "Warcraft III: Reign of Chaos", - "site": "metacritic_com", - "genres": [ - "General", - "Real-Time", - "Fantasy", - "Strategy" - ] - }, - { - "id": 10103, - "name": "Warcraft III: The Frozen Throne", - "site": "metacritic_com", - "genres": [ - "General", - "Real-Time", - "Fantasy", - "Strategy" - ] - }, - { - "id": 10104, - "name": "Warhammer 40.000: Space Marine", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Role-Playing", - "Shooter", - "Action RPG", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 10105, - "name": "Warhammer: Chaosbane", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10106, - "name": "We Happy Few", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 10107, - "name": "Whack The Thief", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10108, - "name": "Whack Your Boss", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10109, - "name": "Whack Your Ex", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10110, - "name": "Whack Your Neighbour", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10111, - "name": "Whack Your Teacher", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10112, - "name": "What Remains of Edith Finch", - "site": "metacritic_com", - "genres": [ - "First-Person", - "General", - "3D", - "Adventure" - ] - }, - { - "id": 10113, - "name": "Will Rock", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 10114, - "name": "Willy-Nilly Knight", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Tactics", - "Turn-Based", - "Action RPG", - "Strategy" - ] - }, - { - "id": 10115, - "name": "Wizard of Legend", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10116, - "name": "Wolfenstein", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10117, - "name": "Wolfenstein: The New Order", - "site": "metacritic_com", - "genres": [ - "Historic", - "Arcade", - "First-Person", - "Shooter", - "General", - "Action" - ] - }, - { - "id": 10118, - "name": "Wolfenstein: The Old Blood", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 10119, - "name": "Woolfe - The Red Hood Diaries", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10120, - "name": "Worms 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10121, - "name": "X-Blades / Ониблэйд", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10122, - "name": "X-Morph: Defense", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 10123, - "name": "Yandere School", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10124, - "name": "Year Walk", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Miscellaneous", - "First-Person", - "3D", - "General", - "Modern" - ] - }, - { - "id": 10125, - "name": "ZOMBI", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Survival", - "First-Person", - "Shooter", - "Action Adventure", - "Action" - ] - }, - { - "id": 10126, - "name": "Zero Escape: Zero Time Dilemma", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 10127, - "name": "Ziggurat", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 10128, - "name": "Zombie Driver HD", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10129, - "name": "Zombie Party", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Top-Down", - "Shoot-'Em-Up", - "Action" - ] - }, - { - "id": 10130, - "name": "Zombie Vikings", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 10131, - "name": "Zuma Deluxe", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Matching", - "Puzzle" - ] - }, - { - "id": 10132, - "name": "Zuma's Revenge!", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Action", - "Puzzle" - ] - }, - { - "id": 10133, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10134, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10135, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10136, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10137, - "name": "Невероятные Приключения Трояна", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10138, - "name": "Невероятные Приключения Трояна 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10139, - "name": "Невероятные Приключения Трояна 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10140, - "name": "Предатор", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10141, - "name": "Стоп-Гоп", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10142, - "name": "Стоп-Гоп 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10143, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10144, - "name": "Darksiders Genesis", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 10145, - "name": "Darksiders Genesis", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 10146, - "name": "Darksiders Genesis", - "site": "gameguru_ru", - "genres": [ - "Слэшер", - "Экшен", - "Шутер" - ] - }, - { - "id": 10147, - "name": "Darksiders Genesis", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения", - "РПГ" - ] - }, - { - "id": 10148, - "name": "Darksiders Genesis", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 10149, - "name": "Darksiders Genesis", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 10150, - "name": "Darksiders Genesis", - "site": "playground_ru", - "genres": [ - "Экшен", - "Вид сверху" - ] - }, - { - "id": 10151, - "name": "Darksiders Genesis", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 10152, - "name": "Darksiders Genesis", - "site": "gamespot_com", - "genres": [ - "Role-Playing", - "Action" - ] - }, - { - "id": 10153, - "name": "Darksiders Genesis", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10154, - "name": "Darksiders Genesis", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10155, - "name": "Darksiders Genesis", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10156, - "name": "Darksiders Genesis", - "site": "spong_com", - "genres": [] - }, - { - "id": 10157, - "name": "Wolfenstein II: The New Colossus", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10158, - "name": "Wolfenstein II: The New Colossus", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10159, - "name": "Wolfenstein II: The New Colossus", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10160, - "name": "Wolfenstein II: The New Colossus", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10161, - "name": "Wolfenstein II: The New Colossus", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 10162, - "name": "Wolfenstein II: The New Colossus", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10163, - "name": "Wolfenstein II: The New Colossus", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10164, - "name": "Wolfenstein II: The New Colossus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10165, - "name": "Wolfenstein II: The New Colossus", - "site": "gamespot_com", - "genres": [ - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 10166, - "name": "Wolfenstein II: The New Colossus", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 10167, - "name": "Wolfenstein II: The New Colossus", - "site": "spong_com", - "genres": [] - }, - { - "id": 10168, - "name": "Wolfenstein II: The New Colossus", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 10169, - "name": "Wolfenstein II: The New Colossus", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10170, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10171, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10172, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10173, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10174, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10175, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10176, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10177, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10178, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10179, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10180, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10181, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10182, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10183, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10184, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10185, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10186, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10187, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10188, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10189, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10190, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10191, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10192, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10193, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10194, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10195, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10196, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10197, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10198, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10199, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10200, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10201, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10202, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10203, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10204, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10205, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10206, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10207, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10208, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10209, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10210, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10211, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10212, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10213, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10214, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10215, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10216, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10217, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10218, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10219, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10220, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10221, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10222, - "name": "Witch trainer", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10223, - "name": "Witch trainer", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10224, - "name": "Witch trainer", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10225, - "name": "Witch trainer", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10226, - "name": "Witch trainer", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10227, - "name": "Witch trainer", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10228, - "name": "Witch trainer", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10229, - "name": "Witch trainer", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10230, - "name": "Witch trainer", - "site": "spong_com", - "genres": [] - }, - { - "id": 10231, - "name": "Witch trainer", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10232, - "name": "Witch trainer", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10233, - "name": "Witch trainer", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10234, - "name": "Witch trainer", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10235, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10236, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10237, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 10238, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10239, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 10240, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10241, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10242, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10243, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10244, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 10245, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10246, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10247, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Sci-Fi" - ] - }, - { - "id": 10248, - "name": "Control", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены", - "Приключения" - ] - }, - { - "id": 10249, - "name": "Crysis 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10250, - "name": "Control", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10251, - "name": "Control", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 10252, - "name": "Control", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10253, - "name": "Control", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица" - ] - }, - { - "id": 10254, - "name": "Control", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "От третьего лица", - "Экшен" - ] - }, - { - "id": 10255, - "name": "Crysis 3", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10256, - "name": "Crysis 3", - "site": "gameguru_ru", - "genres": [ - "Шутер", - "Экшен" - ] - }, - { - "id": 10257, - "name": "Control", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10258, - "name": "Control", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10259, - "name": "Crysis 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 10260, - "name": "Control", - "site": "iwantgames_ru", - "genres": [ - "Приключения", - "Экшен" - ] - }, - { - "id": 10261, - "name": "Control", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10262, - "name": "Control", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10263, - "name": "Control", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 10264, - "name": "Crysis 3", - "site": "playground_ru", - "genres": [ - "Открытый мир", - "Шутер", - "Экшен", - "От первого лица", - "Научная фантастика" - ] - }, - { - "id": 10265, - "name": "Control", - "site": "spong_com", - "genres": [] - }, - { - "id": 10266, - "name": "Crysis 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10267, - "name": "Crysis 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10268, - "name": "Crysis 3", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Action", - "Shooter" - ] - }, - { - "id": 10269, - "name": "Crysis 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10270, - "name": "Crysis 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 10271, - "name": "Crysis 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10272, - "name": "Crysis 3", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 10273, - "name": "Crysis 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10274, - "name": "Devil May Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10275, - "name": "Devil May Cry", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10276, - "name": "Devil May Cry", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10277, - "name": "Devil May Cry", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер", - "Боевик-приключения" - ] - }, - { - "id": 10278, - "name": "Devil May Cry", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10279, - "name": "Devil May Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10280, - "name": "Devil May Cry", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен", - "От третьего лица" - ] - }, - { - "id": 10281, - "name": "Devil May Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10282, - "name": "Devil May Cry", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10283, - "name": "Devil May Cry", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10284, - "name": "Devil May Cry", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10285, - "name": "Devil May Cry", - "site": "gamespot_com", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10286, - "name": "Devil May Cry", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy" - ] - }, - { - "id": 10287, - "name": "Devil May Cry 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10288, - "name": "Devil May Cry 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер" - ] - }, - { - "id": 10289, - "name": "Devil May Cry 2", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10290, - "name": "Devil May Cry 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10291, - "name": "Devil May Cry 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10292, - "name": "Devil May Cry 2", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен", - "От третьего лица" - ] - }, - { - "id": 10293, - "name": "Devil May Cry 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10294, - "name": "Devil May Cry 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10295, - "name": "Devil May Cry 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10296, - "name": "Devil May Cry HD Collection", - "site": "gameguru_ru", - "genres": [ - "Слэшер", - "Экшен", - "Ремастер" - ] - }, - { - "id": 10297, - "name": "Devil May Cry 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10298, - "name": "Devil May Cry 2", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10299, - "name": "Devil May Cry 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10300, - "name": "Devil May Cry HD Collection", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10301, - "name": "Devil May Cry HD Collection", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Приключения" - ] - }, - { - "id": 10302, - "name": "Devil May Cry HD Collection", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10303, - "name": "Devil May Cry HD Collection", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен" - ] - }, - { - "id": 10304, - "name": "Devil May Cry HD Collection", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10305, - "name": "Devil May Cry HD Collection", - "site": "iwantgames_ru", - "genres": [ - "Аркады", - "Экшен", - "Ужасы" - ] - }, - { - "id": 10306, - "name": "Devil May Cry HD Collection", - "site": "spong_com", - "genres": [] - }, - { - "id": 10307, - "name": "Devil May Cry HD Collection", - "site": "ag_ru", - "genres": [ - "Приключения", - "Экшены" - ] - }, - { - "id": 10308, - "name": "Devil May Cry 2", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy" - ] - }, - { - "id": 10309, - "name": "Devil May Cry HD Collection", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 10310, - "name": "Devil May Cry HD Collection", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10311, - "name": "Devil May Cry HD Collection", - "site": "gamespot_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 10312, - "name": "Devil May Cry HD Collection", - "site": "metacritic_com", - "genres": [ - "Compilation", - "Miscellaneous" - ] - }, - { - "id": 10313, - "name": "Terminator: Resistance", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10314, - "name": "Terminator: Resistance", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 10315, - "name": "Terminator: Resistance", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10316, - "name": "Terminator: Resistance", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10317, - "name": "Terminator: Resistance", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10318, - "name": "Terminator: Resistance", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 10319, - "name": "Terminator: Resistance", - "site": "playground_ru", - "genres": [ - "Научная фантастика", - "Шутер", - "От первого лица" - ] - }, - { - "id": 10320, - "name": "Terminator: Resistance", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10321, - "name": "Terminator: Resistance", - "site": "spong_com", - "genres": [] - }, - { - "id": 10322, - "name": "Terminator: Resistance", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 10323, - "name": "Terminator: Resistance", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10324, - "name": "Terminator: Resistance", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "First-Person", - "Shooter" - ] - }, - { - "id": 10325, - "name": "Terminator: Resistance", - "site": "metacritic_com", - "genres": [ - "Action", - "First-Person", - "Arcade", - "Shooter" - ] - }, - { - "id": 10326, - "name": "Kholat", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 10327, - "name": "Kholat", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "инди" - ] - }, - { - "id": 10328, - "name": "Kholat", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 10329, - "name": "Kholat", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10330, - "name": "Kholat", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 10331, - "name": "Kholat", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ужасы" - ] - }, - { - "id": 10332, - "name": "Kholat", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ужасы", - "Инди", - "Экшен", - "От первого лица" - ] - }, - { - "id": 10333, - "name": "Kholat", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 10334, - "name": "Kholat", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10335, - "name": "Kholat", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 10336, - "name": "Kholat", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10337, - "name": "Kholat", - "site": "gamespot_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10338, - "name": "Kholat", - "site": "metacritic_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10339, - "name": "The Old City: Leviathan", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10340, - "name": "The Old City: Leviathan", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 10341, - "name": "The Old City: Leviathan", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10342, - "name": "The Old City: Leviathan", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 10343, - "name": "The Old City: Leviathan", - "site": "gamer_info_com", - "genres": [ - "приключения", - "инди" - ] - }, - { - "id": 10344, - "name": "The Old City: Leviathan", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10345, - "name": "The Old City: Leviathan", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 10346, - "name": "The Old City: Leviathan", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 10347, - "name": "The Old City: Leviathan", - "site": "spong_com", - "genres": [] - }, - { - "id": 10348, - "name": "The Old City: Leviathan", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 10349, - "name": "The Old City: Leviathan", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10350, - "name": "The Old City: Leviathan", - "site": "gamespot_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10351, - "name": "The Old City: Leviathan", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Adventure", - "Fantasy" - ] - }, - { - "id": 10352, - "name": "Nihilumbra", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 10353, - "name": "Nihilumbra", - "site": "gameguru_ru", - "genres": [ - "Платформер", - "Аркада", - "Инди" - ] - }, - { - "id": 10354, - "name": "Nihilumbra", - "site": "gamer_info_com", - "genres": [ - "логические", - "головоломки", - "платформеры", - "инди" - ] - }, - { - "id": 10355, - "name": "Nihilumbra", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10356, - "name": "Nihilumbra", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Платформер" - ] - }, - { - "id": 10357, - "name": "Nihilumbra", - "site": "ag_ru", - "genres": [ - "Инди", - "Экшены", - "Приключения", - "Головоломки", - "Казуальные" - ] - }, - { - "id": 10358, - "name": "Nihilumbra", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 10359, - "name": "Nihilumbra", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10360, - "name": "Nihilumbra", - "site": "spong_com", - "genres": [] - }, - { - "id": 10361, - "name": "Nihilumbra", - "site": "gamespot_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 10362, - "name": "Nihilumbra", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10363, - "name": "Nihilumbra", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 10364, - "name": "Nihilumbra", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 10365, - "name": "Samorost 3", - "site": "gameguru_ru", - "genres": [ - "Квест", - "Point-and-click" - ] - }, - { - "id": 10366, - "name": "Samorost 3", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл", - "Приключения" - ] - }, - { - "id": 10367, - "name": "Samorost 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10368, - "name": "Samorost 3", - "site": "gamer_info_com", - "genres": [ - "логические", - "головоломки", - "квесты" - ] - }, - { - "id": 10369, - "name": "Samorost 3", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 10370, - "name": "Samorost 3", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 10371, - "name": "Samorost 3", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 10372, - "name": "Samorost 3", - "site": "ag_ru", - "genres": [ - "Семейные", - "Инди", - "Приключения", - "Казуальные", - "Головоломки" - ] - }, - { - "id": 10373, - "name": "Samorost 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 10374, - "name": "Samorost 3", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10375, - "name": "Samorost 3", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10376, - "name": "Samorost 3", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 10377, - "name": "Samorost 3", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure", - "General" - ] - }, - { - "id": 10378, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10379, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10380, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10381, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Экшены", - "Шутеры" - ] - }, - { - "id": 10382, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10383, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10384, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10385, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10386, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10387, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10388, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10389, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 10390, - "name": "Unreal Tournament", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10391, - "name": "Unreal Tournament", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 10392, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 10393, - "name": "Unreal Tournament", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от первого лица" - ] - }, - { - "id": 10394, - "name": "Unreal Tournament", - "site": "ag_ru", - "genres": [ - "Экшены", - "Шутеры" - ] - }, - { - "id": 10395, - "name": "Unreal Tournament", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10396, - "name": "Unreal Tournament", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10397, - "name": "Unreal Tournament", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10398, - "name": "Unreal Tournament", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10399, - "name": "Unreal Tournament", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10400, - "name": "Unreal Tournament", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Шутер", - "Космос", - "От первого лица", - "Научная фантастика", - "Экшен" - ] - }, - { - "id": 10401, - "name": "Unreal Tournament", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10402, - "name": "Unreal Tournament", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10403, - "name": "Unreal Tournament", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Action", - "Arcade", - "First-Person" - ] - }, - { - "id": 10404, - "name": "Prey", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10405, - "name": "Prey", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10406, - "name": "Prey", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10407, - "name": "Prey", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10408, - "name": "Prey", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от первого лица" - ] - }, - { - "id": 10409, - "name": "Prey", - "site": "spong_com", - "genres": [] - }, - { - "id": 10410, - "name": "Prey", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "От первого лица" - ] - }, - { - "id": 10411, - "name": "Prey", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 10412, - "name": "Prey", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10413, - "name": "Prey", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10414, - "name": "Prey", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10415, - "name": "Prey", - "site": "gamespot_com", - "genres": [ - "3D", - "Shooter", - "First-Person", - "Action" - ] - }, - { - "id": 10416, - "name": "Prey", - "site": "metacritic_com", - "genres": [ - "General", - "Shooter", - "Action", - "Arcade", - "First-Person" - ] - }, - { - "id": 10417, - "name": "007: Quantum of Solace", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10418, - "name": "007: Quantum of Solace", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10419, - "name": "007: Quantum of Solace", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10420, - "name": "007: Quantum of Solace", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10421, - "name": "007: Quantum of Solace", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10422, - "name": "007: Quantum of Solace", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10423, - "name": "007: Quantum of Solace", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10424, - "name": "007: Quantum of Solace", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10425, - "name": "007: Quantum of Solace", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10426, - "name": "007: Quantum of Solace", - "site": "spong_com", - "genres": [] - }, - { - "id": 10427, - "name": "007: Quantum of Solace", - "site": "gamespot_com", - "genres": [ - "Shooter", - "3D", - "First-Person", - "Action" - ] - }, - { - "id": 10428, - "name": "007: Quantum of Solace", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10429, - "name": "007: Quantum of Solace", - "site": "metacritic_com", - "genres": [ - "Modern", - "Arcade", - "First-Person", - "General", - "Shooter", - "Action" - ] - }, - { - "id": 10430, - "name": "Constantine", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10431, - "name": "Constantine", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн", - "Приключения" - ] - }, - { - "id": 10432, - "name": "Constantine", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10433, - "name": "Constantine", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10434, - "name": "Constantine", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10435, - "name": "Constantine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10436, - "name": "Constantine", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10437, - "name": "Constantine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10438, - "name": "Constantine", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ужасы", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 10439, - "name": "Constantine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10440, - "name": "Constantine", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10441, - "name": "Constantine", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 10442, - "name": "Constantine", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10443, - "name": "You Are Empty", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 10444, - "name": "You Are Empty", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10445, - "name": "You Are Empty", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10446, - "name": "You Are Empty", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 10447, - "name": "You Are Empty", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10448, - "name": "You Are Empty", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10449, - "name": "You Are Empty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10450, - "name": "You Are Empty", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10451, - "name": "You Are Empty", - "site": "playground_ru", - "genres": [ - "Экшен", - "Постапокалипсис", - "От первого лица" - ] - }, - { - "id": 10452, - "name": "You Are Empty", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10453, - "name": "You Are Empty", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10454, - "name": "You Are Empty", - "site": "gamespot_com", - "genres": [ - "First-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 10455, - "name": "You Are Empty", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 10456, - "name": "007: Quantum of Solace", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10457, - "name": "35MM", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10458, - "name": "60 Seconds!", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Simulation", - "Strategy" - ] - }, - { - "id": 10459, - "name": "A Bird Story", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10460, - "name": "A Plague Tale: Innocence", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10461, - "name": "A Story About My Uncle", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10462, - "name": "Adam Wolfe", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10463, - "name": "Alan Wake", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10464, - "name": "Alan Wake's American Nightmare", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "TPS", - "Action" - ] - }, - { - "id": 10465, - "name": "Alice: Madness Returns", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Horror", - "Beat 'em up" - ] - }, - { - "id": 10466, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10467, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10468, - "name": "Alien Rage - Unlimited", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10469, - "name": "Alien: Isolation", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Survival Horror" - ] - }, - { - "id": 10470, - "name": "Aliens vs. Predator (2010)", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10471, - "name": "Aliens: Colonial Marines", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10472, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10473, - "name": "Alone in the Dark (2008)", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10474, - "name": "Amnesia: A Machine for Pigs", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Action Adventure" - ] - }, - { - "id": 10475, - "name": "Amnesia: The Dark Descent", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Action Adventure" - ] - }, - { - "id": 10476, - "name": "Among The Sleep", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "First-Person" - ] - }, - { - "id": 10477, - "name": "Angry Birds", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10478, - "name": "Angry Birds Rio", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10479, - "name": "Angry Birds Seasons", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10480, - "name": "Angry Birds Space", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade" - ] - }, - { - "id": 10481, - "name": "Angry Birds Star Wars", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10482, - "name": "Angry Birds Star Wars II", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10483, - "name": "Armored Kitten", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10484, - "name": "Ashes", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10485, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10486, - "name": "BLACK ROSE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10487, - "name": "Bad Dream: Bridge", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10488, - "name": "Bad Dream: Butcher", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10489, - "name": "Bad Dream: Coma", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10490, - "name": "Bad Dream: Cyclops", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10491, - "name": "Bad Dream: Graveyard", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10492, - "name": "Bad Dream: Hospital", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10493, - "name": "Bad Dream: Memories", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10494, - "name": "Baldur's Gate", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10495, - "name": "Bastion", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action Adventure" - ] - }, - { - "id": 10496, - "name": "Batman: The Telltale Series", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie" - ] - }, - { - "id": 10497, - "name": "Battlefield 1", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS", - "FPS" - ] - }, - { - "id": 10498, - "name": "Battlefield 1942", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10499, - "name": "Battlefield 4", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10500, - "name": "Battlefield Hardline", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10501, - "name": "Battlefield V", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10502, - "name": "Battlefield Vietnam", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10503, - "name": "Bayonetta", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10504, - "name": "Beholder", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Strategy" - ] - }, - { - "id": 10505, - "name": "Beholder (Beta)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10506, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10507, - "name": "Ben and Ed", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10508, - "name": "Ben and Ed: Blood Party", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Platform Game", - "Action" - ] - }, - { - "id": 10509, - "name": "Besiege", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10510, - "name": "Beyond Good and Evil", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10511, - "name": "Binary Domain", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10512, - "name": "BioShock", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10513, - "name": "BioShock 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10514, - "name": "BioShock Infinite", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10515, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10516, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10517, - "name": "Bionic Commando", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10518, - "name": "Black Mesa", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10519, - "name": "BlackSite: Area 51", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10520, - "name": "Blades of Time", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Beat 'em up" - ] - }, - { - "id": 10521, - "name": "Blades of Time: Dismal Swamp", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10522, - "name": "Blood Omen 2: Legacy of Kain", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10523, - "name": "BloodRayne", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action", - "Third-Person" - ] - }, - { - "id": 10524, - "name": "BloodRayne 2", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action", - "Third-Person" - ] - }, - { - "id": 10525, - "name": "BloodRayne: Betrayal", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Beat 'em up" - ] - }, - { - "id": 10526, - "name": "Bloodbath kavkaz", - "site": "squarefaction_ru", - "genres": [ - "Shooter" - ] - }, - { - "id": 10527, - "name": "Bloodline: Линия крови", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10528, - "name": "Blue Estate", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10529, - "name": "Blues and Bullets: Episode 1", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10530, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10531, - "name": "Boogeyman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10532, - "name": "Borderlands", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "FPS" - ] - }, - { - "id": 10533, - "name": "Borderlands 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "FPS" - ] - }, - { - "id": 10534, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10535, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10536, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10537, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10538, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10539, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10540, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10541, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10542, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10543, - "name": "Borderlands: The Pre-Sequel!", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10544, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10545, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10546, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10547, - "name": "Brink", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10548, - "name": "Broforce", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10549, - "name": "Broken Age", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Third-Person" - ] - }, - { - "id": 10550, - "name": "Brothers: A Tale Of Two Sons", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10551, - "name": "Brutal Legend", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "RTS" - ] - }, - { - "id": 10552, - "name": "Buff Knight Advanced", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10553, - "name": "Bulb Boy", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10554, - "name": "Bulletstorm", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10555, - "name": "Call of Cthulhu (2018)", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10556, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10557, - "name": "Call of Duty", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10558, - "name": "Call of Duty 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10559, - "name": "Call of Duty 4: Modern Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10560, - "name": "Call of Duty: Advanced Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10561, - "name": "Call of Duty: Black Ops", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10562, - "name": "Call of Duty: Black Ops II", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10563, - "name": "Call of Duty: Black Ops III", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10564, - "name": "Call of Duty: Ghosts", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10565, - "name": "Call of Duty: Infinite Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10566, - "name": "Call of Duty: Modern Warfare 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10567, - "name": "Call of Duty: Modern Warfare 3", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10568, - "name": "Call of Duty: United Offensive", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10569, - "name": "Call of Duty: WWII", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10570, - "name": "Call of Duty: World at War", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10571, - "name": "Canabalt", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10572, - "name": "Castlevania: Lords of Shadow", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Slasher" - ] - }, - { - "id": 10573, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10574, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10575, - "name": "Castlevania: Lords of Shadow 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10576, - "name": "Cat Mario", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10577, - "name": "Cat Quest", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "RPG" - ] - }, - { - "id": 10578, - "name": "Child Of Light", - "site": "squarefaction_ru", - "genres": [ - "RPG", - "Platform Game" - ] - }, - { - "id": 10579, - "name": "Children of Morta", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10580, - "name": "Choice Chamber", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10581, - "name": "Claire", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10582, - "name": "Clustertruck", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10583, - "name": "Cold Fear", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10584, - "name": "Constantine", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10585, - "name": "Control", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10586, - "name": "Costume Quest", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10587, - "name": "Costume Quest 2", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10588, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10589, - "name": "Cross of the Dutchman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10590, - "name": "Cry Of Fear", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10591, - "name": "Crysis", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10592, - "name": "Crysis 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10593, - "name": "Crysis 3", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10594, - "name": "Crysis Warhead", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10595, - "name": "Cube Escape: Arles", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10596, - "name": "Cube Escape: Case 23", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10597, - "name": "Cube Escape: Harvey's Box", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10598, - "name": "Cube Escape: Seasons", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10599, - "name": "Cube Escape: The Lake", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10600, - "name": "Cube Escape: The Mill", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10601, - "name": "DOOM (2016)", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10602, - "name": "Dark Deception", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10603, - "name": "Dark Messiah of Might and Magic", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10604, - "name": "Dark Sector", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10605, - "name": "Dark Souls II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10606, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10607, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10608, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10609, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10610, - "name": "Dark Souls III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10611, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10612, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10613, - "name": "Dark Souls: Prepare to Die Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10614, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10615, - "name": "Dark Souls: Remastered", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10616, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10617, - "name": "Darksiders", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10618, - "name": "Darksiders Genesis", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10619, - "name": "Darksiders II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action Adventure" - ] - }, - { - "id": 10620, - "name": "Darksiders III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Slasher" - ] - }, - { - "id": 10621, - "name": "Daylight", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 10622, - "name": "Dead Island", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "FPS" - ] - }, - { - "id": 10623, - "name": "Dead Island: Riptide", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action", - "Beat 'em up" - ] - }, - { - "id": 10624, - "name": "Dead Island: Ryder White", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10625, - "name": "Dead Rising", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10626, - "name": "Dead Space", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS", - "Action" - ] - }, - { - "id": 10627, - "name": "Dead Space 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10628, - "name": "Dead Space 3", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10629, - "name": "Dead Space 3: Awakened (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10630, - "name": "Dead by Daylight", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action" - ] - }, - { - "id": 10631, - "name": "Deadlight", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Platform Game", - "Action", - "Puzzle" - ] - }, - { - "id": 10632, - "name": "Deadpool", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10633, - "name": "DeathSpank", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10634, - "name": "DeathSpank: Thongs of Virtue", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10635, - "name": "Deathtrap", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10636, - "name": "Demigod", - "site": "squarefaction_ru", - "genres": [ - "RTS", - "Action" - ] - }, - { - "id": 10637, - "name": "Descenders", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10638, - "name": "Detention", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10639, - "name": "Devil May Cry", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10640, - "name": "Devil May Cry 2", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10641, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10642, - "name": "Devil May Cry 4", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10643, - "name": "Devil May Cry 5", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10644, - "name": "Devil May Cry HD Collection", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10645, - "name": "Diablo II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10646, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10647, - "name": "Diablo III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10648, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10649, - "name": "Dig or Die", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10650, - "name": "Disciples II: Dark Prophecy", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10651, - "name": "Disciples III: Renaissance", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10652, - "name": "Dishonored", - "site": "squarefaction_ru", - "genres": [ - "Stealth", - "Action Adventure" - ] - }, - { - "id": 10653, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10654, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10655, - "name": "Distraint", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10656, - "name": "Disturbed", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10657, - "name": "DmC: Devil May Cry", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10658, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10659, - "name": "Doki Doki Literature Club", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Visual Novel" - ] - }, - { - "id": 10660, - "name": "Don't Knock Twice", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10661, - "name": "Doom 3", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10662, - "name": "Double Dragon Neon", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Action" - ] - }, - { - "id": 10663, - "name": "Down Stairs", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10664, - "name": "Downfall", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10665, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10666, - "name": "Dragon Age II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10667, - "name": "Dragon Age: Inquisition", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10668, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10669, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10670, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10671, - "name": "Dragon Age: Origins", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10672, - "name": "Dragon Age: Origins - Awakening", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10673, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10674, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10675, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10676, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10677, - "name": "Dragon's Dogma: Dark Arisen", - "site": "squarefaction_ru", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 10678, - "name": "Draw a Stickman: Episode 1", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10679, - "name": "Draw a Stickman: Episode 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10680, - "name": "DreadOut", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10681, - "name": "Dropsy", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10682, - "name": "Dude, Stop", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10683, - "name": "Duke Nukem Forever", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10684, - "name": "Dungeon Nightmares", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10685, - "name": "Dying Light", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action" - ] - }, - { - "id": 10686, - "name": "Dying Light: The Following (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10687, - "name": "Emily Wants To Play", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10688, - "name": "Escape Dead Island", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10689, - "name": "Evil Defenders", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10690, - "name": "Evoland", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10691, - "name": "Evoland 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10692, - "name": "Eyes The Horror Game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10693, - "name": "F.E.A.R.", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Horror" - ] - }, - { - "id": 10694, - "name": "F.E.A.R. 2: Project Origin", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10695, - "name": "F.E.A.R. 2: Reborn", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10696, - "name": "F.E.A.R. 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10697, - "name": "F.E.A.R. Extraction Point", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10698, - "name": "F.E.A.R. Perseus Mandate", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10699, - "name": "Fable (2004)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10700, - "name": "Fable III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10701, - "name": "Fahrenheit", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10702, - "name": "Failman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10703, - "name": "Fallout 3", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "RPG" - ] - }, - { - "id": 10704, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10705, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10706, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10707, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10708, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10709, - "name": "Fallout 4", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10710, - "name": "Fallout: New Vegas", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10711, - "name": "Family Trouble", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10712, - "name": "Far Cry", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10713, - "name": "Far Cry 2", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10714, - "name": "Far Cry 3: Blood Dragon", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "FPS" - ] - }, - { - "id": 10715, - "name": "FarSky", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10716, - "name": "Final Fantasy III (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10717, - "name": "Final Fantasy IV (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10718, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10719, - "name": "Final Fantasy X HD Remaster", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10720, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10721, - "name": "Final Fantasy XIII", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10722, - "name": "Final Fantasy XIII-2", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10723, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10724, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10725, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10726, - "name": "Final Fantasy XV", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10727, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10728, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10729, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10730, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10731, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10732, - "name": "Firewatch", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 10733, - "name": "First Winter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10734, - "name": "Five Minutes", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10735, - "name": "Five Nights At Freddy's", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10736, - "name": "Five Nights At Freddy's 2", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10737, - "name": "Five Nights At Freddy's 3", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10738, - "name": "Five Nights At Freddy's 4", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10739, - "name": "Five Nights at Freddy's 3D", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10740, - "name": "Five Nights in Anime v3", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10741, - "name": "Fran Bow", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10742, - "name": "Front Mission Evolved", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10743, - "name": "GUN", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10744, - "name": "Gemini Rue", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10745, - "name": "Geometry Dash", - "site": "squarefaction_ru", - "genres": [ - "Music", - "Platform Game", - "Action" - ] - }, - { - "id": 10746, - "name": "Ghost of a Tale", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Stealth", - "RPG" - ] - }, - { - "id": 10747, - "name": "Goat Simulator", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10748, - "name": "Grand Theft Auto III", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Sandbox", - "TPS" - ] - }, - { - "id": 10749, - "name": "Grand Theft Auto: San Andreas", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10750, - "name": "Grand Theft Auto: Vice City", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10751, - "name": "Grandpa", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10752, - "name": "Grey", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10753, - "name": "Grim Fandango Remastered", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10754, - "name": "Grow Home", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Sandbox" - ] - }, - { - "id": 10755, - "name": "Grow Up", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10756, - "name": "Guns, Gore & Cannoli", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade" - ] - }, - { - "id": 10757, - "name": "Guns, Gore & Cannoli 2", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade" - ] - }, - { - "id": 10758, - "name": "Half-Life", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10759, - "name": "Half-Life 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10760, - "name": "Half-Life 2: Episode One", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10761, - "name": "Half-Life 2: Episode Two", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10762, - "name": "Half-Life: Blue Shift", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10763, - "name": "Half-Life: Opposing Force", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10764, - "name": "Half-Life: Paranoia", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10765, - "name": "Half-Life: The Xeno Project", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10766, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10767, - "name": "Halo: Combat Evolved", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10768, - "name": "Hand of Fate", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10769, - "name": "Hand of Fate 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10770, - "name": "Happy Room", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10771, - "name": "Happy Wheels", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10772, - "name": "Hard Reset", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10773, - "name": "Hard Reset: Exile (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10774, - "name": "Harts", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10775, - "name": "Hatred", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10776, - "name": "Hellblade: Senua's Sacrifice", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10777, - "name": "Hellgate: London", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10778, - "name": "Her Story", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10779, - "name": "Hero Academy", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 10780, - "name": "Heroes of Might and Magic III", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10781, - "name": "Heroes of Might and Magic IV", - "site": "squarefaction_ru", - "genres": [ - "TBS", - "Tactical RPG" - ] - }, - { - "id": 10782, - "name": "Heroes of Might and Magic V", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10783, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10784, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10785, - "name": "Homefront", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10786, - "name": "Hotel Remorse", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10787, - "name": "Hotline Miami", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Stealth" - ] - }, - { - "id": 10788, - "name": "Hotline Miami 2: Wrong Number", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Stealth" - ] - }, - { - "id": 10789, - "name": "House Flipper", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10790, - "name": "I Am Alive", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10791, - "name": "I am Setsuna", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10792, - "name": "I hate this game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10793, - "name": "INSIDE", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10794, - "name": "Indivisible", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10795, - "name": "Injustice: Gods Among Us", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10796, - "name": "Into the Breach", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "TBS", - "Simulation" - ] - }, - { - "id": 10797, - "name": "Jade Empire", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10798, - "name": "Jotun", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10799, - "name": "Just Cause", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Racing", - "Arcade" - ] - }, - { - "id": 10800, - "name": "Just Cause 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Driving", - "TPS" - ] - }, - { - "id": 10801, - "name": "Kane & Lynch 2: Dog Days", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10802, - "name": "Kholat", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "First-Person", - "Adventure" - ] - }, - { - "id": 10803, - "name": "Killer is Dead: Nightmare Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10804, - "name": "King's Bounty: Armored Princess", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10805, - "name": "King's Bounty: Crossworlds", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10806, - "name": "King's Bounty: Dark Side", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10807, - "name": "King's Bounty: The Legend", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10808, - "name": "King's Bounty: Warriors of the North", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10809, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10810, - "name": "Kingdom Rush", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Tower Defense" - ] - }, - { - "id": 10811, - "name": "Kingdom: Classic", - "site": "squarefaction_ru", - "genres": [ - "Simulation", - "Strategy", - "Adventure", - "City Building", - "Tower Defense", - "Action" - ] - }, - { - "id": 10812, - "name": "Kingdoms of Amalur: Reckoning", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10813, - "name": "Knock-Knock", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10814, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10815, - "name": "Lara Croft and the Guardian of Light", - "site": "squarefaction_ru", - "genres": [ - "Arcade" - ] - }, - { - "id": 10816, - "name": "Lara Croft and the Temple of Osiris", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10817, - "name": "Late Shift", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10818, - "name": "Layers of Fear", - "site": "squarefaction_ru", - "genres": [ - "Horror" - ] - }, - { - "id": 10819, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10820, - "name": "Left 4 Dead", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10821, - "name": "Left 4 Dead 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action", - "FPS" - ] - }, - { - "id": 10822, - "name": "Legend Hand of God", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10823, - "name": "Legend of Grimrock", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure", - "RPG" - ] - }, - { - "id": 10824, - "name": "Legendary", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10825, - "name": "Life After Us: Shipwrecked", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10826, - "name": "Life Goes On: Done to Death", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10827, - "name": "Life Is Strange", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10828, - "name": "Limbo", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game" - ] - }, - { - "id": 10829, - "name": "Little Nightmares", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10830, - "name": "Little Nightmares - The Depths (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10831, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10832, - "name": "Loki: Heroes of Mythology", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10833, - "name": "Lost Planet: Colonies", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10834, - "name": "Lost: Via Domus", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10835, - "name": "Love Is Strange (DEMO)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10836, - "name": "METAL SLUG", - "site": "squarefaction_ru", - "genres": [ - "Scrolling Shooter" - ] - }, - { - "id": 10837, - "name": "METAL SLUG 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Scrolling Shooter" - ] - }, - { - "id": 10838, - "name": "MINERVA: Metastasis", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10839, - "name": "MOTHERGUNSHIP", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10840, - "name": "Machinarium", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10841, - "name": "Magibot", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10842, - "name": "Magicka", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10843, - "name": "Magicka 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10844, - "name": "Manual Samuel", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10845, - "name": "Mario.EXE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10846, - "name": "Marvel vs. Capcom: Infinite", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10847, - "name": "Masochisia", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10848, - "name": "Masters of Anima", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Strategy", - "RPG" - ] - }, - { - "id": 10849, - "name": "Max Payne", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10850, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 10851, - "name": "Max Payne 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 10852, - "name": "Max: The Curse Of Brotherhood", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Platform Game" - ] - }, - { - "id": 10853, - "name": "McPixel", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Puzzle" - ] - }, - { - "id": 10854, - "name": "Metal Gear Rising: Revengeance", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10855, - "name": "Metal Wolf Chaos XD", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10856, - "name": "Metro 2033", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10857, - "name": "Metro 2033 Redux", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10858, - "name": "Metro Exodus", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10859, - "name": "Metro Last Light", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10860, - "name": "Metro Last Light Redux", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10861, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10862, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10863, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10864, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10865, - "name": "Middle-earth: Shadow of Mordor", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10866, - "name": "Might & Magic Heroes VI", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10867, - "name": "Minecraft Story Mode", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10868, - "name": "Mini Ninjas", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade", - "Adventure" - ] - }, - { - "id": 10869, - "name": "Mirror's Edge", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10870, - "name": "Monst", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10871, - "name": "Mortal Kombat IX", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10872, - "name": "Mortal Kombat X", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10873, - "name": "Mother Russia Bleeds", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up" - ] - }, - { - "id": 10874, - "name": "Mount Your Friends", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10875, - "name": "Mr. Robot", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10876, - "name": "Mr.President!", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10877, - "name": "Murdered: Soul Suspect", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10878, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10879, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Fighting" - ] - }, - { - "id": 10880, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10881, - "name": "Need for Speed: Underground", - "site": "squarefaction_ru", - "genres": [ - "Racing" - ] - }, - { - "id": 10882, - "name": "Never Alone", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10883, - "name": "Neverending Nightmares", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10884, - "name": "Nevermind", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10885, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10886, - "name": "Neverwinter Nights", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10887, - "name": "Neverwinter Nights 2", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10888, - "name": "NieR: Automata", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10889, - "name": "Nightmare House 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10890, - "name": "Nihilumbra", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game", - "Adventure" - ] - }, - { - "id": 10891, - "name": "Nosferatu: The Wrath of Malachi", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10892, - "name": "Nox Timore", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10893, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Puzzle" - ] - }, - { - "id": 10894, - "name": "Omensight", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10895, - "name": "One Hand Clapping (DEMO)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10896, - "name": "One Night at Flumpty's", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10897, - "name": "One Night at Flumpty's 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10898, - "name": "One Piece: Burning Blood", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10899, - "name": "Orcs Must Die!", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action", - "Tower Defense" - ] - }, - { - "id": 10900, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10901, - "name": "Ori and the Blind Forest", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10902, - "name": "Outlast", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10903, - "name": "Outlast: Whistleblower", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10904, - "name": "Overlord", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action", - "TBS", - "Adventure" - ] - }, - { - "id": 10905, - "name": "Overlord: Raising Hell", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10906, - "name": "Oxenfree", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Adventure" - ] - }, - { - "id": 10907, - "name": "Painkiller", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10908, - "name": "Painkiller: Battle Out of Hell", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10909, - "name": "Painkiller: Overdose", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10910, - "name": "Paint the Town Red", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10911, - "name": "Panty Party", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10912, - "name": "Papers, Please", - "site": "squarefaction_ru", - "genres": [ - "Management", - "Text Game", - "Adventure" - ] - }, - { - "id": 10913, - "name": "Peace, Death!", - "site": "squarefaction_ru", - "genres": [ - "Simulation", - "Management" - ] - }, - { - "id": 10914, - "name": "Pillars of Eternity", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10915, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10916, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10917, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10918, - "name": "Pinstripe", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 10919, - "name": "Pirates of the Caribbean: At World’s End", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10920, - "name": "Pizza Delivery 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10921, - "name": "Plague Inc: Evolved", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10922, - "name": "Plants vs. Zombies", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Tower Defense" - ] - }, - { - "id": 10923, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10924, - "name": "Pony Island", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Horror" - ] - }, - { - "id": 10925, - "name": "Portal", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "FPS" - ] - }, - { - "id": 10926, - "name": "Portal 2", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "FPS" - ] - }, - { - "id": 10927, - "name": "Prey", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10928, - "name": "Prey (2006)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10929, - "name": "Prince of Persia (2008)", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10930, - "name": "Prince of Persia: The Two Thrones", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10931, - "name": "Prospekt", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10932, - "name": "Prototype", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10933, - "name": "Prototype 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10934, - "name": "Psychonauts", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Adventure" - ] - }, - { - "id": 10935, - "name": "PuniTy", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10936, - "name": "P·O·L·L·E·N", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10937, - "name": "Quake 4", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10938, - "name": "Quantum Break", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "TPS" - ] - }, - { - "id": 10939, - "name": "Raft", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10940, - "name": "Rage", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10941, - "name": "Rampage Knights", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10942, - "name": "Rayman Legends", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10943, - "name": "Rayman Origins", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10944, - "name": "Rayman Raving Rabbids", - "site": "squarefaction_ru", - "genres": [ - "Party", - "Platform Game" - ] - }, - { - "id": 10945, - "name": "Rayman Raving Rabbids 2", - "site": "squarefaction_ru", - "genres": [ - "Party" - ] - }, - { - "id": 10946, - "name": "Real Horror Stories", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10947, - "name": "Redeemer", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10948, - "name": "Reign Of Kings", - "site": "squarefaction_ru", - "genres": [ - "Sandbox", - "RPG" - ] - }, - { - "id": 10949, - "name": "Reigns", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10950, - "name": "Reigns: Her Majesty", - "site": "squarefaction_ru", - "genres": [ - "Card Game", - "Simulation" - ] - }, - { - "id": 10951, - "name": "Resident Evil", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10952, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10953, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10954, - "name": "Resident Evil 4", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action", - "TPS" - ] - }, - { - "id": 10955, - "name": "Resident Evil 5", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10956, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10957, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10958, - "name": "Resident Evil 6", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10959, - "name": "Resident Evil 7: Biohazard", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10960, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10961, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10962, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10963, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10964, - "name": "Resident Evil: Operation Raccoon City", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10965, - "name": "Resident Evil: Revelations", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10966, - "name": "Resident Evil: Revelations 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 10967, - "name": "Reus", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Simulation" - ] - }, - { - "id": 10968, - "name": "Rezrog", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10969, - "name": "Rise of the Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10970, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10971, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10972, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10973, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10974, - "name": "Risen", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10975, - "name": "Rock of Ages", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade", - "TBS" - ] - }, - { - "id": 10976, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10977, - "name": "Root Of Evil: The Tailor", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10978, - "name": "Runic Rampage", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10979, - "name": "Rust", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10980, - "name": "Ryse: Son of Rome", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10981, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10982, - "name": "SCP-087", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10983, - "name": "SCP-087-B", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10984, - "name": "SINNER: Sacrifice for Redemption", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10985, - "name": "SWAT 4", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10986, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10987, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10988, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10989, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10990, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10991, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10992, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10993, - "name": "Sacred", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10994, - "name": "Sacred 3", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10995, - "name": "Sacred Citadel", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Platform Game", - "Arcade", - "Action" - ] - }, - { - "id": 10996, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10997, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10998, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10999, - "name": "Saints Row IV", - "site": "squarefaction_ru", - "genres": [ - "Sandbox", - "TPS" - ] - }, - { - "id": 11000, - "name": "Saints Row: Gat out of Hell", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11001, - "name": "Salt and Sanctuary", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11002, - "name": "Samorost 3", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11003, - "name": "Samsara Room", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11004, - "name": "Saw: The Video Game", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11005, - "name": "Scratches", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11006, - "name": "Seasons after Fall", - "site": "squarefaction_ru", - "genres": [ - "Arcade", - "Puzzle", - "Platform Game" - ] - }, - { - "id": 11007, - "name": "Serena", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11008, - "name": "Serious Sam 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11009, - "name": "Serious Sam 3: BFE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11010, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11011, - "name": "Serious Sam Classic: The First Encounter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11012, - "name": "Serious Sam Classic: The Second Encounter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11013, - "name": "Shadow Warrior", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "First-Person" - ] - }, - { - "id": 11014, - "name": "Shadow of the Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11015, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11016, - "name": "Shantae: Half-Genie Hero", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11017, - "name": "Shoot Your Nightmare Halloween Special", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11018, - "name": "Shovel Knight: Shovel of Hope", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11019, - "name": "Shrek 2: The Game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11020, - "name": "Silence on The Line", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11021, - "name": "Silent Hill: Alchemilla", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11022, - "name": "Silverfall", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11023, - "name": "Silverfall: Earth Awakening", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11024, - "name": "Singularity", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11025, - "name": "Slap-Man", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11026, - "name": "Slay the Spire", - "site": "squarefaction_ru", - "genres": [ - "Card Game", - "Strategy", - "Arcade", - "TBS" - ] - }, - { - "id": 11027, - "name": "Sleeping Dogs", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11028, - "name": "Slender: The Arrival", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11029, - "name": "Slendytubbies", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Horror" - ] - }, - { - "id": 11030, - "name": "Slime Rancher", - "site": "squarefaction_ru", - "genres": [ - "Action", - "First-Person", - "Sandbox" - ] - }, - { - "id": 11031, - "name": "Sniper Elite", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11032, - "name": "Sniper Elite 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11033, - "name": "Sniper Elite V2", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11034, - "name": "Sophie's Curse", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11035, - "name": "South Park: The Fractured But Whole", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11036, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11037, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11038, - "name": "South Park: The Stick of Truth", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 11039, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11040, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11041, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11042, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11043, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11044, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11045, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11046, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11047, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11048, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11049, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11050, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11051, - "name": "Spooky's House of Jump Scares", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11052, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS", - "TPS" - ] - }, - { - "id": 11053, - "name": "Star Wars: The Force Unleashed", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 11054, - "name": "Star Wars: The Force Unleashed II", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 11055, - "name": "SteamWorld Dig", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Arcade" - ] - }, - { - "id": 11056, - "name": "SteamWorld Dig 2", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Arcade" - ] - }, - { - "id": 11057, - "name": "SteamWorld Heist", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 11058, - "name": "Steampunk Tower 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11059, - "name": "Steel Rats", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Racing", - "Arcade", - "Action" - ] - }, - { - "id": 11060, - "name": "Stories: The Path of Destinies", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11061, - "name": "Street Fighter X Tekken", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 11062, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 11063, - "name": "Stupidella", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11064, - "name": "Sudeki", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11065, - "name": "Superhot", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS" - ] - }, - { - "id": 11066, - "name": "Surgeon Simulator", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11067, - "name": "Tales from the Borderlands", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11068, - "name": "Tales of Berseria", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11069, - "name": "Tales of Zestiria", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11070, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11071, - "name": "Tasty Planet: Back for Seconds", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11072, - "name": "Tekken 7", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Fighting" - ] - }, - { - "id": 11073, - "name": "Terminator: Resistance", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11074, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11075, - "name": "Tesla vs Lovecraft", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11076, - "name": "Teslagrad", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game" - ] - }, - { - "id": 11077, - "name": "The Banner Saga", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11078, - "name": "The Banner Saga 2", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11079, - "name": "The Bard’s Tale (2004)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11080, - "name": "The Beginner's Guide", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11081, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11082, - "name": "The Binding of Isaac: Rebirth", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Arcade" - ] - }, - { - "id": 11083, - "name": "The Bunker", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11084, - "name": "The Cat Lady", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 11085, - "name": "The Council", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "RPG" - ] - }, - { - "id": 11086, - "name": "The Curse of Blackwater", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11087, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11088, - "name": "The Darkness II", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11089, - "name": "The Elder Scrolls IV: Oblivion", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Sandbox" - ] - }, - { - "id": 11090, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11091, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11092, - "name": "The Elder Scrolls V: Skyrim", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Sandbox" - ] - }, - { - "id": 11093, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11094, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11095, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11096, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11097, - "name": "The Evil Within", - "site": "squarefaction_ru", - "genres": [ - "TPS", - "First-Person", - "Stealth", - "Slasher", - "Survival Horror" - ] - }, - { - "id": 11098, - "name": "The Evil Within 2", - "site": "squarefaction_ru", - "genres": [ - "TPS", - "First-Person", - "Stealth", - "Slasher", - "Survival Horror" - ] - }, - { - "id": 11099, - "name": "The Evil Within: The Assignment (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11100, - "name": "The Evil Within: The Consequence (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11101, - "name": "The Evil Within: The Executioner (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11102, - "name": "The Expendabros", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11103, - "name": "The Final Station", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11104, - "name": "The First Tree", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11105, - "name": "The Forest", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11106, - "name": "The Hat Man Shadow Ward", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11107, - "name": "The Incredible Adventures of Van Helsing", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11108, - "name": "The Incredible Adventures of Van Helsing II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11109, - "name": "The Incredible Adventures of Van Helsing III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11110, - "name": "The Last Remnant", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11111, - "name": "The Mask Man", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11112, - "name": "The Old City: Leviathan", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11113, - "name": "The Punisher (2005)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11114, - "name": "The Secret of Monkey Island: Special Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11115, - "name": "The Slippery Slope", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11116, - "name": "The Stanley Parable", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11117, - "name": "The Walking Dead: A New Frontier", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11118, - "name": "The Walking Dead: Michonne", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11119, - "name": "The Walking Dead: Season One", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11120, - "name": "The Walking Dead: Season Two", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11121, - "name": "The Witcher", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11122, - "name": "The Witcher 2: Assassins of Kings", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11123, - "name": "The Witcher 3: Wild Hunt", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11124, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11125, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11126, - "name": "The Wolf Among Us", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Action Adventure" - ] - }, - { - "id": 11127, - "name": "Thief", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Stealth" - ] - }, - { - "id": 11128, - "name": "This Is The Police", - "site": "squarefaction_ru", - "genres": [ - "Management", - "Simulation", - "Adventure" - ] - }, - { - "id": 11129, - "name": "TimeShift", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11130, - "name": "Titan Quest", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11131, - "name": "Titan Quest Anniversary Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11132, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11133, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11134, - "name": "Titan Souls", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 11135, - "name": "Titanfall 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11136, - "name": "To The Moon", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11137, - "name": "Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Platform Game", - "Third-Person" - ] - }, - { - "id": 11138, - "name": "Torchlight", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11139, - "name": "Torchlight II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11140, - "name": "Toren", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 11141, - "name": "Tormentum: Dark Sorrow", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 11142, - "name": "Total Overdose", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11143, - "name": "Transistor", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11144, - "name": "Trine", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11145, - "name": "Trine 2", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 11146, - "name": "Trine 3: The Artifacts of Power", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11147, - "name": "Troll Tale", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11148, - "name": "Trollface Quest", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11149, - "name": "Trollface Quest 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11150, - "name": "Trollface Quest 3", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11151, - "name": "Trollface Quest 4", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11152, - "name": "Trollface Quest 5", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11153, - "name": "Trollface Quest 6", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11154, - "name": "Trollface Quest 7", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11155, - "name": "Trollface Quest: Video Memes", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Point&Click" - ] - }, - { - "id": 11156, - "name": "Twin Sector", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11157, - "name": "Two Worlds", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11158, - "name": "Typical Nightmare", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11159, - "name": "Ultimate Epic Battle Simulator", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11160, - "name": "Ultra Street Fighter IV", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 11161, - "name": "Undertale", - "site": "squarefaction_ru", - "genres": [ - "JRPG", - "Adventure" - ] - }, - { - "id": 11162, - "name": "Unfair Mario", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11163, - "name": "Unmechanical", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade" - ] - }, - { - "id": 11164, - "name": "Unmechanical: Extended (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11165, - "name": "Unreal Tournament", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11166, - "name": "Until the last", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11167, - "name": "Valiant Hearts: The Great War", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade", - "Adventure" - ] - }, - { - "id": 11168, - "name": "Vanish", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11169, - "name": "Victor Vran", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11170, - "name": "Vikings: Wolves of Midgard", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11171, - "name": "Visage", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "First-Person" - ] - }, - { - "id": 11172, - "name": "Wanted: Weapons of Fate", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11173, - "name": "Warcraft III: Reign of Chaos", - "site": "squarefaction_ru", - "genres": [ - "RTS" - ] - }, - { - "id": 11174, - "name": "Warcraft III: The Frozen Throne", - "site": "squarefaction_ru", - "genres": [ - "RTS" - ] - }, - { - "id": 11175, - "name": "Warhammer 40.000: Space Marine", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11176, - "name": "Warhammer: Chaosbane", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11177, - "name": "We Happy Few", - "site": "squarefaction_ru", - "genres": [ - "Action", - "First-Person" - ] - }, - { - "id": 11178, - "name": "Whack The Thief", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11179, - "name": "Whack Your Boss", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11180, - "name": "Whack Your Ex", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11181, - "name": "Whack Your Neighbour", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11182, - "name": "Whack Your Teacher", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11183, - "name": "What Remains of Edith Finch", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11184, - "name": "Will Rock", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11185, - "name": "Willy-Nilly Knight", - "site": "squarefaction_ru", - "genres": [ - "RPG", - "Tactical RPG" - ] - }, - { - "id": 11186, - "name": "Witch trainer", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11187, - "name": "Wizard of Legend", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11188, - "name": "Wolfenstein", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11189, - "name": "Wolfenstein II: The New Colossus", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11190, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11191, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11192, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11193, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11194, - "name": "Wolfenstein: The New Order", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11195, - "name": "Wolfenstein: The Old Blood", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11196, - "name": "Woolfe - The Red Hood Diaries", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Platform Game" - ] - }, - { - "id": 11197, - "name": "Worms 2", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 11198, - "name": "X-Blades / Ониблэйд", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11199, - "name": "X-Morph: Defense", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11200, - "name": "Yandere School", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11201, - "name": "Year Walk", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11202, - "name": "You Are Empty", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 11203, - "name": "ZOMBI", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11204, - "name": "Zero Escape: Zero Time Dilemma", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11205, - "name": "Ziggurat", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11206, - "name": "Zombie Driver HD", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11207, - "name": "Zombie Party", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11208, - "name": "Zombie Vikings", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Arcade" - ] - }, - { - "id": 11209, - "name": "Zuma Deluxe", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 11210, - "name": "Zuma's Revenge!", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11211, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11212, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11213, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11214, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11215, - "name": "Невероятные Приключения Трояна", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11216, - "name": "Невероятные Приключения Трояна 2", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11217, - "name": "Невероятные Приключения Трояна 3", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11218, - "name": "Предатор", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11219, - "name": "Стоп-Гоп", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11220, - "name": "Стоп-Гоп 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11221, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11222, - "name": "Rogue Warrior", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 11223, - "name": "Rogue Warrior", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 11224, - "name": "Rogue Warrior", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 11225, - "name": "Rogue Warrior", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 11226, - "name": "Rogue Warrior", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 11227, - "name": "Rogue Warrior", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11228, - "name": "Rogue Warrior", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Stealth" - ] - }, - { - "id": 11229, - "name": "Rogue Warrior", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11230, - "name": "Rogue Warrior", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 11231, - "name": "Rogue Warrior", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 11232, - "name": "Rogue Warrior", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 11233, - "name": "Rogue Warrior", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 11234, - "name": "Rogue Warrior", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 11235, - "name": "Rogue Warrior", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Arcade", - "Tactical", - "Shooter", - "Action", - "Modern" - ] - }, - { - "id": 11236, - "name": "Counter-Strike", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 11237, - "name": "Counter-Strike", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 11238, - "name": "Counter-Strike", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 11239, - "name": "Counter-Strike", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 11240, - "name": "Counter-Strike", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS" - ] - }, - { - "id": 11241, - "name": "Counter-Strike", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 11242, - "name": "Counter-Strike", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Strategy: Combat" - ] - }, - { - "id": 11243, - "name": "Counter-Strike", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11244, - "name": "Counter-Strike", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 11245, - "name": "Counter-Strike", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Мультиплеер", - "Тактика", - "Шутер" - ] - }, - { - "id": 11246, - "name": "Counter-Strike", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 11247, - "name": "Counter-Strike", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 11248, - "name": "Counter-Strike", - "site": "gamespot_com", - "genres": [ - "Tactical", - "Shooter", - "Action", - "First-Person" - ] - }, - { - "id": 11249, - "name": "Counter-Strike", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Tactical", - "Shooter", - "Action", - "Modern" - ] - }, - { - "id": 11250, - "name": "Dark Void", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11251, - "name": "Dark Void", - "site": "ag_ru", - "genres": [ - "Приключения", - "Шутеры", - "Экшены" - ] - }, - { - "id": 11252, - "name": "Dark Void", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 11253, - "name": "Dark Void", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 11254, - "name": "Dark Void", - "site": "gamer_info_com", - "genres": [ - "action", - "шутеры от 3-го лица" - ] - }, - { - "id": 11255, - "name": "Dark Void", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11256, - "name": "Dark Void", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game: Flying" - ] - }, - { - "id": 11257, - "name": "Dark Void", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 11258, - "name": "Dark Void", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 11259, - "name": "Dark Void", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11260, - "name": "Dark Void", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 11261, - "name": "Dark Void", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 11262, - "name": "Dark Void", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 11263, - "name": "Dark Void", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - } -] \ No newline at end of file diff --git a/html_parsing/get_game_genres/export_import/export.py b/html_parsing/get_game_genres/export_import/export.py deleted file mode 100644 index 0739d386f..000000000 --- a/html_parsing/get_game_genres/export_import/export.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -from pathlib import Path - -import sys -sys.path.append('..') - -from db import Dump - -from playhouse.shortcuts import model_to_dict - - -DIR = Path(__file__).parent.resolve() / 'data' -DIR.mkdir(parents=True, exist_ok=True) - -FILE_NAME_EXPORT_JSON = DIR / 'games.json' - - -if __name__ == '__main__': - items = [model_to_dict(dump) for dump in Dump.select()] - print(len(items)) - - json.dump( - items, - open(FILE_NAME_EXPORT_JSON, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) diff --git a/html_parsing/get_game_genres/export_import/import.py b/html_parsing/get_game_genres/export_import/import.py deleted file mode 100644 index f576e0e19..000000000 --- a/html_parsing/get_game_genres/export_import/import.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json - -import sys -sys.path.append('..') - -from db import Dump - -import peewee -from playhouse.shortcuts import dict_to_model - -from export import FILE_NAME_EXPORT_JSON - - -items = json.load( - open(FILE_NAME_EXPORT_JSON, encoding='utf-8') -) -print('items:', len(items)) -print('Dump count before import:', Dump.select().count()) - -for x in items: - try: - dump = dict_to_model(Dump, x) - dump.save(force_insert=True) - print(f'Import {x}') - - except peewee.IntegrityError as e: - # Ignore error "UNIQUE constraint failed: dump.id" - pass - -print('Current dump count:', Dump.select().count()) diff --git a/html_parsing/get_game_genres/generate_games/game_by_genres.json b/html_parsing/get_game_genres/generate_games/game_by_genres.json deleted file mode 100644 index 95205b10a..000000000 --- a/html_parsing/get_game_genres/generate_games/game_by_genres.json +++ /dev/null @@ -1,4370 +0,0 @@ -{ - "35MM": [ - "Action-adventure", - "FPS", - "Post apocalypse", - "Quest", - "Survival horror" - ], - "60 Seconds!": [ - "Action-adventure", - "Arcade", - "Casual", - "Post apocalypse", - "Quest", - "Simulation", - "Strategy", - "Survival" - ], - "A Bird Story": [ - "Action-adventure", - "Action/RPG", - "Retro" - ], - "A Plague Tale: Innocence": [ - "Action-adventure", - "Action/RPG", - "Stealth", - "Third-person" - ], - "A Story About My Uncle": [ - "Action-adventure", - "Arcade", - "Casual", - "Platformer", - "Puzzle" - ], - "Adam Wolfe": [ - "Adventure", - "Casual", - "Hidden Object", - "Puzzle" - ], - "Alan Wake": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Alan Wake's American Nightmare": [ - "Action-adventure", - "Horror", - "Shooter", - "Survival horror", - "Third-person" - ], - "Alice: Madness Returns": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Survival horror" - ], - "Alien Isolation: Crew Expendable (DLC)": [ - "DLC" - ], - "Alien Isolation: Last Survivor (DLC)": [ - "DLC" - ], - "Alien Rage - Unlimited": [ - "Action", - "Shooter" - ], - "Alien: Isolation": [ - "Action-adventure", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "Aliens vs. Predator (2010)": [ - "Action", - "FPS" - ], - "Aliens: Colonial Marines": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactical" - ], - "Aliens: Colonial Marines - Stasis Interrupted (DLC)": [ - "DLC" - ], - "Alone in the Dark (2008)": [ - "Action-adventure", - "First-person", - "Survival horror", - "Third-person" - ], - "Amnesia: A Machine for Pigs": [ - "Action-adventure", - "First-person", - "Puzzle", - "Quest", - "Survival horror" - ], - "Amnesia: The Dark Descent": [ - "Action-adventure", - "First-person", - "Quest", - "Survival horror" - ], - "Among The Sleep": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Angry Birds": [ - "Action", - "Arcade", - "Logic", - "Puzzle", - "Strategy" - ], - "Angry Birds Rio": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Seasons": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Space": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Star Wars": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Star Wars II": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy" - ], - "Armored Kitten": [ - "2D", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Ashes": [ - "Action-adventure", - "Arcade", - "Casual" - ], - "Attack on Titan / A.O.T. Wings of Freedom": [ - "Action" - ], - "BLACK ROSE": [ - "Adventure", - "First-person" - ], - "Bad Dream: Bridge": [], - "Bad Dream: Butcher": [ - "Adventure" - ], - "Bad Dream: Coma": [ - "Adventure", - "Arcade", - "Horror", - "Point-and-click" - ], - "Bad Dream: Cyclops": [], - "Bad Dream: Graveyard": [], - "Bad Dream: Hospital": [], - "Bad Dream: Memories": [], - "Baldur's Gate": [ - "Fantasy", - "RPG", - "Strategy" - ], - "Bastion": [ - "Action/RPG", - "Arcade", - "Logic", - "Platformer" - ], - "Batman: The Telltale Series": [ - "Adventure", - "Point-and-click" - ], - "Battlefield 1": [ - "Action", - "FPS", - "MMOFPS", - "Tactic" - ], - "Battlefield 1942": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Wargame" - ], - "Battlefield 4": [ - "Action", - "Arcade", - "FPS", - "MMOFPS", - "Shoot 'em up" - ], - "Battlefield Hardline": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Tactic" - ], - "Battlefield V": [ - "Action-adventure", - "FPS", - "MMO", - "MMOFPS", - "Tactic" - ], - "Battlefield Vietnam": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Wargame" - ], - "Bayonetta": [ - "Action-adventure", - "Shooter", - "Slasher", - "Third-person", - "Wargame" - ], - "Beholder": [ - "Adventure", - "Life simulation", - "Quest", - "RPG", - "Simulation", - "Strategy", - "Tactic" - ], - "Beholder (Beta)": [], - "Beholder: Blissful Sleep (DLC)": [ - "Add-on", - "Adventure", - "DLC", - "Quest" - ], - "Ben and Ed": [ - "Action-adventure", - "Arcade", - "Platformer" - ], - "Ben and Ed: Blood Party": [ - "Action", - "Arcade", - "Platformer" - ], - "Besiege": [ - "Action", - "Logic", - "RTS", - "Simulation", - "Strategy", - "Wargame" - ], - "Beyond Good and Evil": [ - "Action-adventure" - ], - "Binary Domain": [ - "Action", - "Action-adventure", - "Cyberpunk", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Tactic", - "Tactical", - "Third-person" - ], - "BioShock": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Steampunk" - ], - "BioShock 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Steampunk" - ], - "BioShock Infinite": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Steampunk" - ], - "BioShock Infinite: Burial at Sea - Episode One (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "BioShock Infinite: Burial at Sea - Episode Two (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Bionic Commando": [ - "2D", - "Action-adventure", - "Platformer", - "Quest", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Black Mesa": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi" - ], - "BlackSite: Area 51": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Blades of Time": [ - "Action-adventure", - "Beat 'em up", - "Hack and slash", - "Slasher", - "Third-person" - ], - "Blades of Time: Dismal Swamp": [ - "Action", - "DLC" - ], - "Blood Omen 2: Legacy of Kain": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "BloodRayne": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Horror", - "Shooter", - "Third-person" - ], - "BloodRayne 2": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Shooter", - "Third-person" - ], - "BloodRayne: Betrayal": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Fantasy", - "Platformer", - "Shooter", - "Slasher", - "Third-person" - ], - "Bloodbath kavkaz": [ - "2D", - "Action-adventure", - "Shoot 'em up", - "Shooter" - ], - "Bloodline: Линия крови": [ - "Action", - "Shooter" - ], - "Blue Estate": [ - "Action", - "Arcade", - "FPS" - ], - "Blues and Bullets: Episode 1": [], - "Blues and Bullets: Episode 2 (DLC)": [], - "Boogeyman": [ - "Action-adventure", - "First-person", - "Strategy", - "Survival" - ], - "Borderlands": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Borderlands 2": [ - "Action/RPG", - "Arcade", - "FPS", - "Open-World", - "Racing", - "Sci-fi" - ], - "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)": [], - "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)": [], - "Borderlands 2: Headhunter 3: Mercenary Day (DLC)": [], - "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands: Claptrap's New Robot Revolution (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: Mad Moxxi's Underdome Riot (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: The Pre-Sequel!": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)": [ - "DLC" - ], - "Borderlands: The Secret Armory of General Knoxx (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: The Zombie Island of Dr. Ned (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Brink": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Broforce": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Platformer", - "Retro", - "Shooter" - ], - "Broken Age": [ - "Adventure", - "Casual", - "Point-and-click", - "Puzzle", - "Quest", - "Third-person" - ], - "Brothers: A Tale Of Two Sons": [ - "Action-adventure", - "Fantasy", - "Puzzle" - ], - "Brutal Legend": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "RTS", - "Slasher", - "Strategy", - "Tactic" - ], - "Buff Knight Advanced": [ - "Action/RPG", - "Arcade", - "Casual", - "Runner" - ], - "Bulb Boy": [ - "Adventure", - "Horror", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Bulletstorm": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Call of Cthulhu (2018)": [], - "Call of Cthulhu: Dark Corners of the Earth": [ - "Action-adventure", - "FPS", - "Quest", - "Survival horror" - ], - "Call of Duty": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Simulation" - ], - "Call of Duty 2": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty 4: Modern Warfare": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Advanced Warfare": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactic" - ], - "Call of Duty: Black Ops": [ - "Action", - "Arcade", - "FPS", - "Robot War", - "Zombie" - ], - "Call of Duty: Black Ops II": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Black Ops III": [ - "Action-adventure", - "Arcade", - "FPS", - "MMO" - ], - "Call of Duty: Ghosts": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Call of Duty: Infinite Warfare": [ - "Action", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactic" - ], - "Call of Duty: Modern Warfare 2": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Modern Warfare 3": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Call of Duty: United Offensive": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Shoot 'em up", - "Tactic", - "Third-person", - "Wargame" - ], - "Call of Duty: WWII": [ - "Action", - "Arcade", - "FPS", - "MMOFPS" - ], - "Call of Duty: World at War": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Canabalt": [ - "2D", - "Action", - "Arcade", - "Casual", - "Platformer", - "Runner" - ], - "Castlevania: Lords of Shadow": [ - "Action-adventure", - "Fantasy", - "Quest", - "Slasher", - "Third-person" - ], - "Castlevania: Lords of Shadow - Resurrection (DLC)": [ - "Action", - "Quest" - ], - "Castlevania: Lords of Shadow - Reverie (DLC)": [ - "Action" - ], - "Castlevania: Lords of Shadow 2": [ - "Action-adventure", - "Fantasy", - "Quest", - "Slasher", - "Third-person" - ], - "Cat Mario": [], - "Cat Quest": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Child Of Light": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "JRPG", - "Platformer" - ], - "Children of Morta": [ - "Action-adventure", - "Action/RPG", - "Retro" - ], - "Choice Chamber": [ - "Action", - "Arcade", - "MMO" - ], - "Claire": [ - "Action-adventure" - ], - "Clustertruck": [ - "Action", - "Arcade", - "First-person", - "Platformer", - "Racing", - "Runner" - ], - "Cold Fear": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Costume Quest": [ - "Action-adventure", - "Action/RPG", - "Casual", - "Fantasy", - "Humor" - ], - "Costume Quest 2": [ - "Action-adventure", - "Action/RPG", - "Casual" - ], - "Costume Quest: Grubbins on Ice Part": [], - "Cross of the Dutchman": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Cry Of Fear": [ - "Action-adventure", - "FPS", - "Survival horror" - ], - "Crysis": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Crysis 2": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Crysis Warhead": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Cube Escape: Arles": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Case 23": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Harvey's Box": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Seasons": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: The Lake": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: The Mill": [ - "Adventure", - "Logic", - "Puzzle" - ], - "DOOM (2016)": [ - "Action", - "FPS", - "Horror" - ], - "Dark Deception": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Messiah of Might and Magic": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Fantasy", - "Strategy" - ], - "Dark Sector": [ - "Action", - "Arcade", - "Cyberpunk", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Dark Souls II": [ - "Action/RPG" - ], - "Dark Souls II: Scholar of the First Sin": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)": [], - "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)": [], - "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)": [], - "Dark Souls III": [ - "Action/RPG" - ], - "Dark Souls III: Ashes of Ariandel (DLC)": [ - "Action/RPG", - "DLC" - ], - "Dark Souls III: The Ringed City (DLC)": [ - "Action/RPG", - "DLC" - ], - "Dark Souls: Prepare to Die Edition": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)": [], - "Dark Souls: Remastered": [ - "Action/RPG", - "Horror" - ], - "Dark Souls: Remastered - Artorias of the Abyss (DLC)": [], - "Darksiders": [ - "Action-adventure", - "Fantasy", - "Open-World", - "Slasher", - "Third-person" - ], - "Darksiders Genesis": [ - "Action-adventure", - "Action/RPG", - "Hack and slash", - "Shooter", - "Slasher" - ], - "Darksiders II": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World" - ], - "Darksiders III": [ - "Action-adventure", - "Action/RPG", - "Open-World" - ], - "Daylight": [ - "Action-adventure", - "Casual", - "Quest", - "Survival horror" - ], - "Dead Island": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Survival horror" - ], - "Dead Island: Riptide": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Racing", - "Survival horror" - ], - "Dead Island: Ryder White": [ - "Action", - "Action-adventure", - "DLC", - "Survival horror" - ], - "Dead Rising": [ - "Action-adventure", - "Open-World", - "Sandbox", - "Survival horror", - "Third-person", - "Zombie" - ], - "Dead Space": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 2": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 3": [ - "Action-adventure", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 3: Awakened (DLC)": [ - "Action", - "DLC" - ], - "Dead by Daylight": [ - "Action-adventure", - "First-person", - "Survival horror", - "Third-person" - ], - "Deadlight": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Survival horror", - "Zombie" - ], - "Deadpool": [ - "Action-adventure", - "Arcade", - "Beat 'em up", - "Humor", - "Shooter", - "Third-person" - ], - "DeathSpank": [ - "Action-adventure", - "Action/RPG", - "Fighting", - "Third-person" - ], - "DeathSpank: Thongs of Virtue": [ - "Action-adventure", - "Action/RPG", - "Puzzle", - "Racing", - "Third-person" - ], - "Deathtrap": [ - "Action-adventure", - "Action/RPG", - "RTS", - "Strategy", - "Tactic" - ], - "Demigod": [ - "Action-adventure", - "Action/RPG", - "Beat 'em up", - "Fantasy", - "MOBA", - "RTS", - "Strategy", - "Tactic" - ], - "Descenders": [ - "Action", - "Arcade", - "Cycle", - "Motorcycle", - "Racing", - "Simulation", - "Sport" - ], - "Detention": [ - "Adventure", - "Point-and-click", - "Puzzle", - "Quest", - "Survival horror" - ], - "Devil May Cry 3: Dante’s Awakening": [ - "Action-adventure", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry 4": [ - "Action-adventure", - "Beat 'em up", - "Fantasy", - "Slasher", - "Third-person" - ], - "Devil May Cry 5": [ - "Action-adventure", - "Slasher", - "Third-person" - ], - "Diablo II": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Diablo II: Lord of Destruction (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Diablo III": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Diablo III: Reaper of Souls (DLC)": [ - "Action-adventure", - "Action/RPG", - "DLC" - ], - "Dig or Die": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Platformer", - "Sandbox", - "Strategy" - ], - "Disciples II: Dark Prophecy": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Disciples III: Renaissance": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "Dishonored": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Stealth", - "Steampunk" - ], - "Dishonored: The Brigmore Witches (DLC)": [ - "Action", - "DLC" - ], - "Dishonored: The Knife of Dunwall (DLC)": [ - "Action", - "DLC" - ], - "Distraint": [ - "Adventure", - "Arcade", - "Casual", - "Horror" - ], - "Disturbed": [ - "Adventure", - "Arcade", - "Casual", - "Horror", - "Point-and-click", - "Simulation", - "Visual Novel" - ], - "DmC: Devil May Cry": [ - "Action-adventure", - "Beat 'em up", - "Fantasy", - "Slasher", - "Third-person" - ], - "DmC: Devil May Cry - Vergil's Downfall (DLC)": [ - "Action", - "Add-on" - ], - "Doki Doki Literature Club": [ - "Adventure", - "Casual", - "Visual Novel" - ], - "Don't Knock Twice": [ - "Action-adventure", - "First-person", - "Horror" - ], - "Doom 3": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi" - ], - "Double Dragon Neon": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Down Stairs": [], - "Downfall": [ - "Adventure", - "Arcade", - "Logic", - "Point-and-click", - "Puzzle", - "Quest", - "Strategy", - "Tactic" - ], - "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist": [ - "Adventure", - "First-person", - "Quest" - ], - "Dragon Age II": [ - "Action-adventure", - "Action/RPG" - ], - "Dragon Age: Inquisition": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Dragon Age: Inquisition - Jaws of Hakkon (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "Dragon Age: Inquisition - The Descent (DLC)": [ - "DLC" - ], - "Dragon Age: Inquisition - Trespasser (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Dragon Age: Origins": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Tactic", - "Third-person", - "Wargame" - ], - "Dragon Age: Origins - Awakening": [ - "Adventure", - "DLC", - "RPG" - ], - "Dragon Age: Origins - Leliana's Song (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - The Darkspawn Chronicles (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - The Golems of Amgarrak (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - Witch Hunt (DLC)": [ - "DLC" - ], - "Dragon's Dogma: Dark Arisen": [ - "Action-adventure", - "Action/RPG" - ], - "Draw a Stickman: Episode 1": [], - "Draw a Stickman: Episode 2": [ - "Adventure" - ], - "DreadOut": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Dropsy": [ - "Adventure", - "Arcade", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Dude, Stop": [ - "Action-adventure", - "Arcade", - "Casual", - "Logic" - ], - "Duke Nukem Forever": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Third-person" - ], - "Dungeon Nightmares": [ - "Action-adventure", - "First-person" - ], - "Dying Light": [ - "Action-adventure", - "FPS", - "Open-World", - "Survival horror", - "Zombie" - ], - "Dying Light: The Following (DLC)": [ - "Action", - "Add-on", - "Open-World" - ], - "Emily Wants To Play": [ - "Action-adventure", - "Casual", - "First-person", - "Puzzle", - "Quest", - "Simulation", - "Strategy", - "Survival horror" - ], - "Escape Dead Island": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Evil Defenders": [ - "Adventure", - "Arcade", - "Casual", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Evoland": [ - "Action-adventure", - "Action/RPG", - "JRPG", - "Retro" - ], - "Evoland 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fighting", - "RTS" - ], - "Eyes The Horror Game": [ - "Adventure" - ], - "F.E.A.R.": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Shoot 'em up", - "Wargame" - ], - "F.E.A.R. 2: Project Origin": [ - "Action-adventure", - "Arcade", - "FPS", - "Horror", - "Shoot 'em up", - "Survival horror" - ], - "F.E.A.R. 2: Reborn": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Horror" - ], - "F.E.A.R. 3": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi" - ], - "F.E.A.R. Extraction Point": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Horror", - "Shoot 'em up", - "Wargame" - ], - "F.E.A.R. Perseus Mandate": [ - "Action-adventure", - "Arcade", - "FPS", - "Horror", - "Survival horror" - ], - "Fable (2004)": [], - "Fable III": [ - "Action-adventure", - "Action/RPG" - ], - "Fahrenheit": [ - "Action-adventure", - "Interactive movie", - "Quest" - ], - "Failman": [], - "Fallout 3": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Fallout 3 - Broken Steel (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 3 - Mothership Zeta (DLC)": [ - "Action/RPG", - "DLC" - ], - "Fallout 3 - Operation: Anchorage (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Fallout 3 - Point Lookout (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 3 - The Pitt (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 4": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Fallout: New Vegas": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Family Trouble": [ - "Adventure" - ], - "Far Cry": [ - "Action-adventure", - "FPS", - "Open-World", - "Robot War", - "Sandbox", - "Shoot 'em up" - ], - "Far Cry 2": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Open-World", - "Sandbox", - "Shoot 'em up" - ], - "Far Cry 3: Blood Dragon": [ - "Action-adventure", - "Add-on", - "Arcade", - "FPS", - "Open-World", - "Sandbox", - "Shoot 'em up" - ], - "FarSky": [ - "Action-adventure", - "Survival" - ], - "Final Fantasy III (Remake)": [], - "Final Fantasy IV (Remake)": [], - "Final Fantasy IV: The After Years (Remake)": [], - "Final Fantasy X HD Remaster": [ - "JRPG", - "RPG" - ], - "Final Fantasy X HD Remaster: Eternal Calm (DLC)": [], - "Final Fantasy XIII": [ - "Adventure", - "JRPG", - "RPG" - ], - "Final Fantasy XIII-2": [ - "Adventure", - "JRPG", - "RPG" - ], - "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)": [], - "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)": [], - "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)": [], - "Final Fantasy XV": [ - "Action-adventure", - "Action/RPG", - "JRPG" - ], - "Final Fantasy XV: Ardyn (DLC)": [], - "Final Fantasy XV: Comrades (DLC)": [], - "Final Fantasy XV: Episode Gladiolus (DLC)": [ - "Action-adventure", - "Action/RPG" - ], - "Final Fantasy XV: Episode Ignis (DLC)": [ - "Action-adventure", - "Action/RPG" - ], - "Final Fantasy XV: Episode Prompto (DLC)": [ - "Action-adventure", - "Action/RPG", - "Shooter" - ], - "Firewatch": [ - "Adventure", - "First-person", - "Logic", - "Puzzle", - "Quest", - "Simulation" - ], - "First Winter": [ - "Adventure", - "First-person" - ], - "Five Minutes": [ - "Puzzle" - ], - "Five Nights At Freddy's": [ - "Action-adventure", - "First-person", - "Quest", - "Simulation", - "Sport", - "Strategy", - "Survival horror" - ], - "Five Nights At Freddy's 2": [ - "Action-adventure", - "Arcade", - "First-person", - "Quest", - "Simulation", - "Strategy", - "Survival horror" - ], - "Five Nights At Freddy's 3": [ - "Action-adventure", - "Arcade", - "First-person", - "Horror", - "Quest", - "Simulation" - ], - "Five Nights At Freddy's 4": [ - "Action-adventure", - "First-person", - "Quest", - "Strategy", - "Survival horror" - ], - "Five Nights at Freddy's 3D": [], - "Five Nights in Anime v3": [], - "Fran Bow": [ - "Adventure", - "Horror", - "Platformer", - "Point-and-click", - "Quest" - ], - "Front Mission Evolved": [ - "Action", - "Action-adventure", - "Arcade", - "Robot War", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Simulation", - "Third-person" - ], - "GUN": [ - "Action-adventure", - "FPS", - "Quest", - "Racing", - "Third-person", - "Western" - ], - "Gemini Rue": [ - "Adventure", - "Point-and-click", - "Quest", - "Sci-fi", - "Third-person" - ], - "Geometry Dash": [ - "Action", - "Arcade", - "Casual", - "Music", - "Music/Rhythm" - ], - "Ghost of a Tale": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Goat Simulator": [ - "Action-adventure", - "Arcade", - "Casual", - "Humor", - "Open-World", - "Sandbox", - "Simulation" - ], - "Grand Theft Auto III": [ - "Action-adventure", - "Racing", - "Shooter", - "Simulation", - "Third-person" - ], - "Grand Theft Auto: San Andreas": [ - "Action-adventure", - "Arcade", - "Open-World", - "Racing", - "Sandbox", - "Shooter", - "Simulation", - "TPS", - "Third-person", - "Wargame" - ], - "Grand Theft Auto: Vice City": [ - "Action-adventure", - "Arcade", - "Open-World", - "Racing", - "Sandbox", - "Shooter", - "Simulation", - "TPS", - "Third-person", - "Wargame" - ], - "Grandpa": [ - "Action-adventure", - "Survival" - ], - "Grey": [ - "Action" - ], - "Grim Fandango Remastered": [ - "Action-adventure", - "Fantasy", - "Puzzle", - "Quest", - "Third-person" - ], - "Grow Home": [ - "Action-adventure", - "Arcade", - "Casual", - "Open-World", - "Platformer", - "Quest", - "Sandbox" - ], - "Grow Up": [ - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Open-World", - "Platformer", - "Quest", - "Third-person" - ], - "Guns, Gore & Cannoli": [ - "Action", - "Arcade", - "Platformer", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Guns, Gore & Cannoli 2": [ - "Action", - "Arcade", - "Platformer", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Half-Life": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Puzzle", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2: Episode One": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2: Episode Two": [ - "Action", - "Arcade", - "FPS", - "Logic", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life: Blue Shift": [ - "Action", - "Arcade", - "FPS", - "Puzzle", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life: Opposing Force": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Sci-fi" - ], - "Half-Life: Paranoia": [], - "Half-Life: The Xeno Project": [], - "Halloween Stories: Invitation (Collector's Edition)": [], - "Halo: Combat Evolved": [ - "Action-adventure", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Hand of Fate": [ - "Action-adventure", - "Action/RPG", - "Board game", - "Cardgame", - "Logic", - "Strategy" - ], - "Hand of Fate 2": [ - "Action-adventure", - "Action/RPG", - "Board game", - "Cardgame", - "Logic", - "Strategy", - "Wargame" - ], - "Happy Room": [ - "Action", - "Arcade", - "Simulation" - ], - "Happy Wheels": [ - "2D", - "Action", - "Arcade", - "Platformer", - "Racing" - ], - "Hard Reset": [ - "Action", - "Arcade", - "Cyberpunk", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Hard Reset: Exile (DLC)": [ - "DLC" - ], - "Harts": [ - "Adventure", - "First-person" - ], - "Hatred": [ - "2D", - "Action", - "Arcade", - "Open-World", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Hellblade: Senua's Sacrifice": [ - "Action-adventure", - "Fantasy", - "Open-World", - "Slasher", - "Third-person" - ], - "Hellgate: London": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Fantasy", - "MMORPG", - "Survival horror", - "Third-person" - ], - "Her Story": [ - "Adventure", - "Logic", - "Puzzle", - "Quest", - "Simulation", - "Visual Novel" - ], - "Hero Academy": [ - "Board game", - "Fantasy", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic III": [ - "Adventure", - "Strategy", - "Wargame" - ], - "Heroes of Might and Magic IV": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic V": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic V: Hammers of Fate": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "Strategy", - "TBS" - ], - "Heroes of Might and Magic V: Tribes of the East": [ - "Fantasy", - "God game", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Homefront": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Hotel Remorse": [ - "Action-adventure", - "First-person" - ], - "Hotline Miami": [ - "Action-adventure", - "Arcade", - "Retro", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Hotline Miami 2: Wrong Number": [ - "Action-adventure", - "Arcade", - "Retro", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "House Flipper": [ - "Adventure", - "First-person", - "Simulation", - "Strategy" - ], - "I Am Alive": [ - "Action-adventure", - "Open-World", - "Platformer", - "Post apocalypse", - "Quest", - "Shooter", - "Survival horror", - "Third-person" - ], - "I am Setsuna": [ - "Adventure", - "JRPG", - "RPG" - ], - "I hate this game": [ - "Action", - "Arcade" - ], - "INSIDE": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Horror", - "Logic", - "Platformer", - "Puzzle" - ], - "Indivisible": [ - "Action/RPG", - "Platformer" - ], - "Injustice: Gods Among Us": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Into the Breach": [ - "RPG", - "Sci-fi", - "Simulation", - "Strategy", - "TBS", - "Tactic" - ], - "Jade Empire": [ - "Action-adventure", - "Action/RPG", - "Fighting" - ], - "Jotun": [ - "Action-adventure", - "Arcade" - ], - "Just Cause": [ - "Action-adventure", - "Arcade", - "Open-World", - "Quest", - "Racing", - "Robot War", - "Sandbox", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Just Cause 2": [ - "Action-adventure", - "Open-World", - "Quest", - "Racing", - "Robot War", - "Sandbox", - "Shooter", - "Third-person" - ], - "Kane & Lynch 2: Dog Days": [ - "Action", - "Arcade", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Killer is Dead: Nightmare Edition": [ - "Action-adventure", - "First-person", - "Sci-fi" - ], - "King's Bounty: Armored Princess": [ - "Adventure", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Crossworlds": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "Strategy", - "TBS" - ], - "King's Bounty: Dark Side": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "King's Bounty: The Legend": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Warriors of the North": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Warriors of the North - Ice and Fire (DLC)": [ - "DLC" - ], - "Kingdom Rush": [ - "Action", - "Arcade", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Kingdom: Classic": [ - "Action-adventure", - "Simulation", - "Strategy" - ], - "Kingdoms of Amalur: Reckoning": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "Knock-Knock": [ - "Action-adventure", - "Arcade" - ], - "Land of the Dead: Road to Fiddler's Green": [ - "Action", - "Arcade", - "FPS" - ], - "Lara Croft and the Guardian of Light": [ - "Action-adventure", - "Arcade", - "Platformer", - "Puzzle", - "Shoot 'Em Up", - "Shoot 'em up" - ], - "Lara Croft and the Temple of Osiris": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle", - "Shoot 'em up" - ], - "Late Shift": [ - "Adventure", - "Interactive movie", - "Quest" - ], - "Layers of Fear": [ - "Action-adventure", - "First-person", - "Horror", - "Quest", - "Simulation" - ], - "Layers of Fear: Inheritance (DLC)": [ - "Add-on", - "Adventure", - "DLC", - "Quest" - ], - "Left 4 Dead": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi", - "Zombie" - ], - "Left 4 Dead 2": [ - "Action", - "FPS", - "Horror", - "Sci-fi", - "TPS", - "Tactic", - "Zombie" - ], - "Legend Hand of God": [ - "Action/RPG", - "Fantasy" - ], - "Legend of Grimrock": [ - "Adventure", - "RPG", - "Wargame" - ], - "Legendary": [ - "Action", - "FPS", - "Shoot 'em up" - ], - "Life After Us: Shipwrecked": [], - "Life Goes On: Done to Death": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle" - ], - "Life Is Strange": [ - "Action-adventure", - "Puzzle", - "Quest", - "Sci-fi", - "Third-person" - ], - "Limbo": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Little Nightmares": [ - "Action-adventure", - "Arcade", - "Horror", - "Platformer", - "Puzzle" - ], - "Little Nightmares - The Depths (DLC)": [ - "Action-adventure", - "Add-on", - "Arcade", - "DLC", - "Platformer" - ], - "Little Nightmares - The Hideaway (DLC)": [ - "Action-adventure", - "Add-on", - "Arcade", - "DLC", - "Platformer" - ], - "Loki: Heroes of Mythology": [ - "Action/RPG", - "Hack and slash" - ], - "Lost Planet: Colonies": [ - "Action", - "Shooter" - ], - "Lost: Via Domus": [ - "Action-adventure", - "Arcade", - "Puzzle", - "Quest" - ], - "Love Is Strange (DEMO)": [], - "METAL SLUG": [ - "2D", - "Action", - "Arcade", - "Casual", - "Fighting", - "Platformer", - "Shoot 'em up", - "Shooter" - ], - "METAL SLUG 3": [ - "2D", - "Action", - "Arcade", - "Casual", - "Fighting", - "Platformer", - "Shoot 'em up", - "Shooter" - ], - "MINERVA: Metastasis": [ - "Shooter" - ], - "MOTHERGUNSHIP": [ - "Action", - "FPS", - "Sci-fi" - ], - "Machinarium": [ - "Adventure", - "Logic", - "Point-and-click", - "Puzzle", - "Quest", - "Sci-fi", - "Steampunk", - "Third-person" - ], - "Magibot": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Logic", - "Platformer", - "Puzzle", - "Strategy" - ], - "Magicka": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy", - "Strategy" - ], - "Magicka 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy" - ], - "Manual Samuel": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Humor", - "Quest" - ], - "Mario.EXE": [], - "Marvel vs. Capcom: Infinite": [ - "2D", - "Action", - "Arcade", - "Fighting" - ], - "Masochisia": [ - "Adventure", - "Point-and-click", - "Quest" - ], - "Masters of Anima": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy", - "MOBA", - "RTS", - "Strategy", - "Tactic" - ], - "Max Payne": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max Payne 2: The Fall of Max Payne": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max Payne 3": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max: The Curse Of Brotherhood": [ - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "McPixel": [ - "Action-adventure", - "Arcade", - "Logic", - "Puzzle" - ], - "Metal Gear Rising: Revengeance": [ - "Action-adventure", - "Sci-fi", - "Shooter", - "Slasher", - "Tactic", - "Third-person" - ], - "Metal Wolf Chaos XD": [ - "Action", - "Robots", - "Sci-fi", - "Shooter", - "Simulation", - "Wargame" - ], - "Metro 2033": [ - "Action-adventure", - "Arcade", - "FPS", - "Post apocalypse", - "Sci-fi", - "Stealth", - "Survival horror" - ], - "Metro 2033 Redux": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Metro Exodus": [ - "Action-adventure", - "Arcade", - "FPS", - "Open-World", - "Post apocalypse", - "Stealth", - "Survival horror" - ], - "Metro Last Light": [ - "Action", - "Arcade", - "FPS", - "Post apocalypse", - "Sci-fi", - "Stealth" - ], - "Metro Last Light Redux": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Metro Last Light Redux: Chronicles Pack (DLC)": [], - "Metro Last Light Redux: Developer Pack (DLC)": [], - "Metro Last Light Redux: Faction Pack (DLC)": [], - "Metro Last Light Redux: Tower Pack (DLC)": [], - "Middle-earth: Shadow of Mordor": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "Might & Magic Heroes VI": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "Minecraft Story Mode": [ - "Adventure", - "Fantasy", - "Third-person" - ], - "Mini Ninjas": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Quest", - "Third-person" - ], - "Mirror's Edge": [ - "Action-adventure", - "FPS", - "Platformer", - "Simulation" - ], - "Monst": [], - "Mortal Kombat IX": [], - "Mortal Kombat X": [ - "2D", - "Action", - "Arcade", - "Fighting" - ], - "Mother Russia Bleeds": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting", - "Platformer", - "Retro" - ], - "Mount Your Friends": [ - "Action", - "Arcade", - "Simulation", - "Sport" - ], - "Mr. Robot": [ - "Action-adventure", - "Action/RPG", - "Logic", - "Platformer", - "Puzzle", - "Sci-fi", - "Strategy", - "Tactic" - ], - "Mr.President!": [ - "Action-adventure", - "Simulation" - ], - "Murdered: Soul Suspect": [ - "Action-adventure", - "Action/RPG", - "Quest", - "Survival horror", - "Third-person" - ], - "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst": [ - "Action-adventure", - "Beat 'em up", - "Fighting" - ], - "Naruto Shippuden: Ultimate Ninja Storm 4": [ - "Action-adventure", - "Arcade", - "Fighting" - ], - "Naruto Shippuden: Ultimate Ninja Storm Revolution": [ - "Action-adventure", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Need for Speed: Underground": [ - "Arcade", - "Racing", - "Simulation" - ], - "Never Alone": [ - "2D", - "Action", - "Arcade", - "Platformer", - "Puzzle" - ], - "Neverending Nightmares": [ - "Action-adventure", - "Quest", - "Survival horror" - ], - "Nevermind": [ - "Action-adventure", - "Adventure", - "First-person", - "Horror", - "Platformer" - ], - "Nevertales 8: The Abomination (Collector's Edition)": [], - "Neverwinter Nights": [ - "Adventure", - "Fantasy", - "RPG" - ], - "Neverwinter Nights 2": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "NieR: Automata": [ - "Action/RPG", - "Post apocalypse", - "Robots", - "Sci-fi", - "Slasher" - ], - "Nightmare House 2": [], - "Nosferatu: The Wrath of Malachi": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Horror" - ], - "Nox Timore": [], - "Oceanhorn Monster of Uncharted Seas": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "JRPG", - "Open-World", - "Quest" - ], - "Omensight": [ - "Action-adventure", - "Action/RPG", - "First-person" - ], - "One Hand Clapping (DEMO)": [], - "One Night at Flumpty's": [ - "Adventure", - "Quest" - ], - "One Night at Flumpty's 2": [], - "One Piece: Burning Blood": [ - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Orcs Must Die!": [ - "Action-adventure", - "RTS", - "Strategy", - "Tactic", - "Third-person", - "Tower Defence", - "Wargame" - ], - "Orcs Must Die! - Lost Adventures (DLC)": [ - "DLC" - ], - "Ori and the Blind Forest": [ - "2D", - "Action", - "Arcade", - "Platformer" - ], - "Outlast": [ - "Action-adventure", - "First-person", - "Survival horror" - ], - "Outlast: Whistleblower": [ - "Action-adventure", - "Add-on", - "DLC", - "First-person", - "Survival horror" - ], - "Overlord": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Quest", - "Strategy" - ], - "Overlord: Raising Hell": [ - "Action-adventure", - "Action/RPG", - "DLC", - "Fantasy", - "Strategy" - ], - "Oxenfree": [ - "Action-adventure", - "Adventure", - "Horror", - "Platformer", - "Quest", - "RPG" - ], - "Painkiller": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up", - "Survival horror" - ], - "Painkiller: Battle Out of Hell": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Painkiller: Overdose": [ - "Action", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Paint the Town Red": [ - "Action", - "First-person", - "Survival" - ], - "Panty Party": [ - "Action-adventure", - "Arcade", - "Casual", - "Humor", - "Puzzle", - "Shooter", - "Simulation", - "Third-person" - ], - "Papers, Please": [ - "Action-adventure", - "Adventure", - "Logic", - "Puzzle", - "RPG", - "Retro", - "Simulation" - ], - "Peace, Death!": [ - "Arcade", - "Casual", - "Humor", - "Retro", - "Simulation" - ], - "Pillars of Eternity": [ - "Adventure", - "Fantasy", - "RPG", - "Third-person" - ], - "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)": [ - "DLC" - ], - "Pillars of Eternity: The White March Part I (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Pillars of Eternity: The White March Part II (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Pinstripe": [ - "Action-adventure", - "Casual", - "Platformer", - "Puzzle", - "Quest" - ], - "Pirates of the Caribbean: At World’s End": [ - "Action-adventure", - "Quest" - ], - "Pizza Delivery 2": [], - "Plague Inc: Evolved": [ - "Casual", - "RTS", - "Sci-fi", - "Simulation", - "Strategy", - "TBS", - "Tactic" - ], - "Plants vs. Zombies": [ - "Action", - "Arcade", - "Fantasy", - "Puzzle", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense", - "Wargame", - "Zombie" - ], - "Plants vs. Zombies: Garden Warfare 2": [ - "Action", - "MMOFPS", - "Shoot 'em up", - "Shooter", - "TPS", - "Tactic", - "Third-person" - ], - "Pony Island": [ - "Action-adventure", - "Arcade", - "Logic", - "Puzzle" - ], - "Portal": [ - "Action-adventure", - "Arcade", - "FPS", - "Logic", - "Platformer", - "Puzzle", - "Sci-fi" - ], - "Portal 2": [ - "Action-adventure", - "Arcade", - "FPS", - "Logic", - "Puzzle", - "Sci-fi" - ], - "Prey (2006)": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi" - ], - "Prince of Persia (2008)": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Open-World", - "Platformer", - "Third-person" - ], - "Prince of Persia: The Two Thrones": [ - "Action-adventure", - "Fantasy", - "Platformer", - "Third-person" - ], - "Prospekt": [ - "Action", - "Arcade", - "FPS" - ], - "Prototype": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Sci-fi" - ], - "Prototype 2": [ - "Action-adventure", - "First-person", - "Open-World", - "Sci-fi" - ], - "Psychonauts": [ - "Action-adventure", - "Arcade", - "Humor", - "Platformer", - "Third-person" - ], - "PuniTy": [], - "P·O·L·L·E·N": [ - "Action-adventure", - "First-person", - "Quest", - "Space" - ], - "Quake 4": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Quantum Break": [ - "Action-adventure", - "Shooter", - "Third-person" - ], - "Raft": [ - "Action-adventure", - "First-person", - "Open-World", - "Simulation", - "Survival" - ], - "Rage": [ - "Action", - "Arcade", - "Beat 'em up", - "FPS", - "Racing" - ], - "Rampage Knights": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Beat 'em up" - ], - "Rayman Legends": [ - "2D", - "Action-adventure", - "Arcade", - "Humor", - "Platformer" - ], - "Rayman Origins": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Humor", - "Platformer" - ], - "Rayman Raving Rabbids": [ - "Action-adventure", - "Arcade", - "Humor", - "Platformer" - ], - "Rayman Raving Rabbids 2": [ - "Action", - "Arcade", - "Music/Rhythm", - "Platformer" - ], - "Real Horror Stories": [ - "Action-adventure", - "Point-and-click" - ], - "Redeemer": [ - "Action-adventure", - "Fighting" - ], - "Reign Of Kings": [ - "Action-adventure", - "Action/RPG", - "First-person", - "Open-World", - "Simulation", - "Survival" - ], - "Reigns": [ - "Adventure", - "Arcade", - "Cardgame", - "Humor", - "Logic", - "Quest", - "RPG", - "RTS", - "Simulation", - "Strategy" - ], - "Reigns: Her Majesty": [ - "Adventure", - "Arcade", - "Cardgame", - "Casual", - "Humor", - "Quest", - "RPG", - "RTS", - "Simulation", - "Strategy" - ], - "Resident Evil": [ - "Action-adventure", - "Survival horror", - "Third-person", - "Zombie" - ], - "Resident Evil / Biohazard HD REMASTER": [ - "Action-adventure" - ], - "Resident Evil 0 / Biohazard 0 HD REMASTER": [ - "Action-adventure" - ], - "Resident Evil 4": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person" - ], - "Resident Evil 5": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person", - "Zombie" - ], - "Resident Evil 5: Desperate Escape (DLC)": [], - "Resident Evil 5: Lost in Nightmares (DLC)": [ - "Action", - "Shooter" - ], - "Resident Evil 6": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person" - ], - "Resident Evil 7: Biohazard": [ - "Action-adventure", - "Shooter", - "Survival horror" - ], - "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - End of Zoe (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - Not a Hero (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil: Operation Raccoon City": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Survival horror", - "Tactical", - "Third-person", - "Zombie" - ], - "Resident Evil: Revelations": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Resident Evil: Revelations 2": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Reus": [ - "Arcade", - "God game", - "RTS", - "Sandbox", - "Simulation", - "Strategy", - "Tactic" - ], - "Rezrog": [ - "Adventure", - "Logic", - "RPG", - "Roguelike", - "Strategy", - "TBS", - "Tactic" - ], - "Rise of the Tomb Raider": [ - "Action-adventure", - "Open-World", - "Platformer", - "Third-person" - ], - "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)": [ - "Action", - "DLC" - ], - "Rise of the Tomb Raider - Blood Ties (DLC)": [ - "Action" - ], - "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)": [ - "Action", - "DLC" - ], - "Rise of the Tomb Raider - Endurance Mode (DLC)": [ - "DLC" - ], - "Risen": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Third-person" - ], - "Rock of Ages": [ - "Action", - "Arcade", - "Platformer", - "Racing", - "Strategy", - "TBS" - ], - "Rock of Ages 2: Bigger & Boulder": [ - "Action", - "Arcade", - "Humor", - "Racing", - "Strategy", - "Tower Defence" - ], - "Root Of Evil: The Tailor": [ - "Action-adventure", - "Casual", - "First-person", - "Horror", - "Simulation" - ], - "Runic Rampage": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Hack and slash" - ], - "Rust": [ - "Action-adventure", - "Action/RPG", - "MMO", - "MMOFPS", - "Open-World", - "Shooter", - "Survival" - ], - "Ryse: Son of Rome": [ - "Action-adventure", - "Fantasy", - "First-person", - "Slasher", - "Third-person", - "Wargame" - ], - "S.T.A.L.K.E.R.: Shadow of Chernobyl": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Open-World", - "Post apocalypse", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "SCP-087": [ - "Action-adventure" - ], - "SCP-087-B": [ - "Action" - ], - "SINNER: Sacrifice for Redemption": [ - "Action-adventure", - "Action/RPG" - ], - "SWAT 4": [ - "Action", - "Arcade", - "FPS", - "Simulation", - "Strategy", - "Tactic", - "Wargame" - ], - "Sable Maze: Forbidden Garden (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Sable Maze: Nightmare Shadows (Collector's Edition)": [], - "Sable Maze: Norwich Caves (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sable Maze: Sinister Knowledge (Collector's Edition)": [], - "Sable Maze: Soul Catcher (Collector's Edition)": [], - "Sable Maze: Sullivan River (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sable Maze: Twelve Fears (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sacred": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World" - ], - "Sacred 3": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Sacred Citadel": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Platformer", - "Shoot 'Em Up" - ], - "Saga of the Nine Worlds: The Four Stags (Collector's Edition)": [], - "Saga of the Nine Worlds: The Gathering (Collector's Edition)": [], - "Saga of the Nine Worlds: The Hunt (Collector's Edition)": [], - "Saints Row IV": [ - "Action-adventure", - "Fighting", - "Open-World", - "Racing", - "Shooter" - ], - "Saints Row: Gat out of Hell": [ - "Action-adventure", - "Add-on", - "DLC", - "Open-World", - "Racing", - "Shooter", - "Third-person" - ], - "Salt and Sanctuary": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Platformer", - "TBS" - ], - "Samsara Room": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Saw: The Video Game": [ - "Action", - "Horror", - "Third-person" - ], - "Scratches": [ - "Adventure", - "Horror", - "Point-and-click", - "Quest" - ], - "Seasons after Fall": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Serena": [ - "Adventure", - "First-person", - "Horror", - "Quest" - ], - "Serious Sam 2": [ - "Action", - "First-person" - ], - "Serious Sam 3: BFE": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Serious Sam 3: Jewel of the Nile (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Serious Sam Classic: The First Encounter": [ - "Action" - ], - "Serious Sam Classic: The Second Encounter": [ - "Action" - ], - "Shadow Warrior": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Shadow of the Tomb Raider": [ - "Action-adventure", - "Open-World", - "Third-person" - ], - "Shadow of the Tomb Raider - The Forge (DLC)": [ - "Action", - "Add-on", - "DLC", - "Open-World" - ], - "Shantae: Half-Genie Hero": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer" - ], - "Shoot Your Nightmare Halloween Special": [ - "Action", - "Arcade", - "FPS" - ], - "Shovel Knight: Shovel of Hope": [ - "Action-adventure" - ], - "Shrek 2: The Game": [ - "Arcade", - "Third-person" - ], - "Silence on The Line": [], - "Silent Hill: Alchemilla": [], - "Silverfall": [ - "Action-adventure", - "Action/RPG" - ], - "Silverfall: Earth Awakening": [ - "Action/RPG" - ], - "Singularity": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Slap-Man": [], - "Slay the Spire": [ - "Cardgame", - "Logic", - "Strategy", - "TBS", - "Tactic" - ], - "Sleeping Dogs": [ - "Action-adventure", - "Open-World", - "Racing", - "Shooter", - "Third-person" - ], - "Slender: The Arrival": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Slendytubbies": [], - "Slime Rancher": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Logic", - "Open-World", - "Quest", - "Simulation" - ], - "Sniper Elite": [ - "Action", - "FPS", - "Shoot 'em up", - "Shooter", - "Simulation", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sniper Elite 3": [ - "Action-adventure", - "FPS", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sniper Elite V2": [ - "Action", - "FPS", - "Shooter", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sophie's Curse": [ - "Adventure", - "Casual", - "First-person", - "Horror", - "Simulation" - ], - "South Park: The Fractured But Whole": [ - "Adventure", - "Humor", - "RPG" - ], - "South Park: The Fractured But Whole - Bring The Crunch (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "South Park: The Stick of Truth": [ - "Action-adventure", - "Action/RPG", - "Humor" - ], - "Spirits of Mystery: Amber Maiden (Collector's Edition)": [ - "Adventure", - "Casual", - "Hidden Object", - "Puzzle" - ], - "Spirits of Mystery: Chains of Promise (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Spirits of Mystery: Family Lies (Collector's Edition)": [], - "Spirits of Mystery: Illusions (Collector's Edition)": [], - "Spirits of Mystery: Song of the Phoenix (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Spirits of Mystery: The Dark Minotaur (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)": [], - "Spirits of Mystery: The Last Fire Queen (Collector's Edition)": [], - "Spirits of Mystery: The Lost Queen (Collector's Edition)": [], - "Spirits of Mystery: The Moon Crystal (Collector's Edition)": [], - "Spirits of Mystery: The Silver Arrow (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Spirits of Mystery: Whisper of the Past (Collector's Edition)": [], - "Spooky's House of Jump Scares": [ - "Action-adventure", - "Horror" - ], - "Star Wars Jedi Knight: Jedi Academy": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Slasher", - "Third-person" - ], - "Star Wars: The Force Unleashed": [ - "Action-adventure", - "Arcade", - "Quest", - "Shooter", - "Third-person" - ], - "Star Wars: The Force Unleashed II": [ - "Action-adventure", - "Beat 'em up" - ], - "SteamWorld Dig": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Post apocalypse", - "Puzzle", - "Steampunk", - "Western" - ], - "SteamWorld Dig 2": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Steampunk", - "Western" - ], - "SteamWorld Heist": [ - "Action-adventure", - "Action/RPG", - "Platformer", - "Sci-fi", - "Strategy", - "TBS", - "Tactic" - ], - "Steampunk Tower 2": [ - "Action", - "RTS", - "Strategy", - "Tower Defence" - ], - "Steel Rats": [ - "Action", - "Arcade", - "Motorcycle", - "Platformer", - "Racing", - "Simulation", - "Steampunk", - "Wargame" - ], - "Stories: The Path of Destinies": [ - "Action-adventure", - "Action/RPG", - "Platformer" - ], - "Street Fighter X Tekken": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Stubbs the Zombie in Rebel Without a Pulse": [ - "Action-adventure", - "Beat 'em up", - "Sci-fi", - "Shooter", - "Third-person" - ], - "Stupidella": [ - "Arcade", - "Quest" - ], - "Sudeki": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "Superhot": [ - "Action", - "Arcade", - "FPS", - "Logic", - "Puzzle", - "Tactic" - ], - "Surgeon Simulator": [ - "Action/RPG", - "Simulation" - ], - "Tales from the Borderlands": [ - "Adventure" - ], - "Tales of Berseria": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "JRPG", - "Third-person" - ], - "Tales of Zestiria": [ - "Action-adventure", - "Action/RPG", - "JRPG" - ], - "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)": [ - "DLC" - ], - "Tasty Planet: Back for Seconds": [ - "Action-adventure", - "Arcade", - "Casual", - "Puzzle" - ], - "Tekken 7": [ - "Action", - "Arcade", - "Beat 'em up", - "Fighting", - "Sport" - ], - "Tesla Effect: A Tex Murphy Adventure": [ - "Adventure", - "Quest" - ], - "Tesla vs Lovecraft": [ - "2D", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Teslagrad": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "The Banner Saga": [ - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "The Banner Saga 2": [ - "Adventure", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "The Bard’s Tale (2004)": [ - "Action/RPG" - ], - "The Beginner's Guide": [ - "Adventure", - "Humor" - ], - "The Binding of Isaac: Afterbirth (DLC)": [ - "Action/RPG", - "Add-on", - "Arcade", - "DLC" - ], - "The Binding of Isaac: Rebirth": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Roguelike", - "Shoot 'em up", - "Shooter", - "Survival horror" - ], - "The Bunker": [ - "Action-adventure", - "Point-and-click", - "Quest", - "Retro", - "Simulation" - ], - "The Cat Lady": [ - "Adventure", - "Horror", - "Quest" - ], - "The Council": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "The Curse of Blackwater": [ - "Action-adventure", - "Survival" - ], - "The Dark Pictures Anthology: Man of Medan": [ - "Adventure" - ], - "The Darkness II": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "The Elder Scrolls IV: Oblivion": [ - "Action-adventure", - "Adventure", - "First-person", - "RPG" - ], - "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)": [], - "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)": [], - "The Elder Scrolls V: Skyrim": [ - "Action-adventure", - "Action/RPG" - ], - "The Elder Scrolls V: Skyrim - Dawnguard (DLC)": [ - "DLC", - "RPG" - ], - "The Elder Scrolls V: Skyrim - Dragonborn (DLC)": [ - "Action-adventure", - "Action/RPG", - "DLC" - ], - "The Elder Scrolls V: Skyrim - Falskaar (MOD)": [], - "The Elder Scrolls V: Skyrim - Hearthfire (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "The Evil Within": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "The Evil Within 2": [ - "Action-adventure", - "Open-World", - "Shooter", - "Survival horror", - "Third-person" - ], - "The Evil Within: The Assignment (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Evil Within: The Consequence (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Evil Within: The Executioner (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Expendabros": [ - "Action-adventure", - "Arcade", - "Platformer" - ], - "The Final Station": [ - "Action-adventure", - "Platformer", - "Post apocalypse", - "Quest", - "Retro", - "Survival", - "Zombie" - ], - "The First Tree": [ - "Adventure", - "Arcade", - "Quest", - "Simulation", - "Third-person" - ], - "The Forest": [ - "Action-adventure", - "First-person", - "Open-World", - "Platformer", - "Simulation", - "Strategy", - "Survival horror" - ], - "The Hat Man Shadow Ward": [ - "Action-adventure", - "Action/RPG", - "Casual", - "Horror", - "Racing", - "Simulation", - "Sport", - "Strategy" - ], - "The Incredible Adventures of Van Helsing": [ - "Action-adventure", - "Action/RPG", - "Steampunk", - "Third-person" - ], - "The Incredible Adventures of Van Helsing II": [ - "Action-adventure", - "Action/RPG" - ], - "The Incredible Adventures of Van Helsing III": [ - "Action-adventure", - "Action/RPG", - "Hack and slash", - "Third-person" - ], - "The Last Remnant": [ - "Action-adventure", - "Action/RPG", - "JRPG", - "Strategy" - ], - "The Mask Man": [], - "The Punisher (2005)": [ - "Action", - "Shooter", - "Third-person" - ], - "The Secret of Monkey Island: Special Edition": [ - "Adventure", - "Humor", - "Point-and-click", - "Quest" - ], - "The Slippery Slope": [], - "The Stanley Parable": [ - "Action-adventure", - "First-person", - "Humor", - "Quest", - "Visual Novel" - ], - "The Walking Dead: A New Frontier": [ - "Adventure", - "Zombie" - ], - "The Walking Dead: Michonne": [ - "Adventure" - ], - "The Walking Dead: Season One": [], - "The Walking Dead: Season Two": [ - "Adventure", - "Point-and-click" - ], - "The Witcher": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "The Witcher 2: Assassins of Kings": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "The Witcher 3: Wild Hunt": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "The Witcher 3: Wild Hunt - Blood and Wine (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "The Wolf Among Us": [ - "Action-adventure", - "Adventure", - "Fantasy", - "Point-and-click", - "Puzzle" - ], - "Thief": [ - "Action-adventure", - "Fantasy", - "Stealth" - ], - "This Is The Police": [ - "Adventure", - "Quest", - "RTS", - "Simulation", - "Strategy", - "TBS", - "Tactic", - "Time management" - ], - "TimeShift": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Titan Quest": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Hack and slash" - ], - "Titan Quest Anniversary Edition": [ - "Action/RPG" - ], - "Titan Quest: Ragnarök (DLC)": [], - "Titan Quest: The Immortal Throne (DLC)": [ - "Action/RPG", - "DLC", - "Wargame" - ], - "Titan Souls": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Titanfall 2": [ - "Action", - "Arcade", - "FPS", - "MMO", - "MMOFPS", - "Robot War", - "Robots", - "Shoot 'em up" - ], - "To The Moon": [ - "Adventure", - "Point-and-click", - "Quest", - "RPG", - "Visual Novel" - ], - "Tomb Raider": [ - "Action-adventure", - "Platformer", - "Quest" - ], - "Torchlight": [ - "Action/RPG", - "Beat 'em up", - "Fantasy", - "Hack and slash" - ], - "Torchlight II": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Toren": [ - "Action-adventure", - "Arcade", - "Fantasy", - "First-person", - "Platformer", - "Puzzle", - "Quest" - ], - "Tormentum: Dark Sorrow": [ - "Action-adventure", - "Adventure", - "Casual", - "Fantasy", - "Horror", - "Logic", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Total Overdose": [ - "Action-adventure", - "Open-World", - "Shooter", - "Third-person" - ], - "Transistor": [ - "Action-adventure", - "Action/RPG", - "Sci-fi", - "Strategy", - "TBS", - "Third-person" - ], - "Trine": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Trine 2": [ - "2D", - "Action", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Trine 3: The Artifacts of Power": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Troll Tale": [ - "Puzzle" - ], - "Trollface Quest": [], - "Trollface Quest 2": [], - "Trollface Quest 3": [], - "Trollface Quest 4": [], - "Trollface Quest 5": [], - "Trollface Quest 6": [], - "Trollface Quest 7": [], - "Trollface Quest: Video Memes": [], - "Twin Sector": [ - "Action-adventure", - "FPS", - "First-person", - "Logic", - "Puzzle" - ], - "Two Worlds": [ - "Action-adventure", - "Adventure", - "RPG", - "Third-person" - ], - "Typical Nightmare": [ - "Action-adventure", - "First-person", - "Simulation" - ], - "Ultimate Epic Battle Simulator": [ - "Action-adventure", - "RTS", - "Simulation", - "Strategy", - "Tactic" - ], - "Ultra Street Fighter IV": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Undertale": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Unfair Mario": [ - "2D", - "Action", - "Platformer" - ], - "Unmechanical": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Unmechanical: Extended (DLC)": [ - "Action-adventure", - "Puzzle" - ], - "Until the last": [ - "Action-adventure", - "Survival" - ], - "Valiant Hearts: The Great War": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Vanish": [ - "Action-adventure" - ], - "Victor Vran": [ - "Action-adventure", - "Action/RPG" - ], - "Vikings: Wolves of Midgard": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Visage": [ - "Action-adventure", - "First-person", - "Quest", - "Simulation", - "Survival horror" - ], - "Wanted: Weapons of Fate": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Warcraft III: Reign of Chaos": [ - "Fantasy", - "RTS", - "Strategy", - "Tactic", - "Wargame" - ], - "Warcraft III: The Frozen Throne": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "RTS", - "Strategy", - "Wargame" - ], - "Warhammer 40.000: Space Marine": [ - "Action/RPG", - "Arcade", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Warhammer: Chaosbane": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Hack and slash" - ], - "We Happy Few": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Horror" - ], - "Whack The Thief": [], - "Whack Your Boss": [], - "Whack Your Ex": [ - "Action" - ], - "Whack Your Neighbour": [], - "Whack Your Teacher": [], - "What Remains of Edith Finch": [ - "Adventure", - "First-person", - "Quest" - ], - "Will Rock": [ - "Action", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Willy-Nilly Knight": [ - "Action/RPG", - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Witch trainer": [ - "Adventure" - ], - "Wizard of Legend": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Dungeon", - "Hack and slash", - "Slasher" - ], - "Wolfenstein": [ - "Action", - "FPS", - "Shoot 'em up" - ], - "Wolfenstein II: The New Colossus": [ - "Action", - "Arcade", - "FPS" - ], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)": [], - "Wolfenstein: The New Order": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Wolfenstein: The Old Blood": [ - "Action", - "Add-on", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Woolfe - The Red Hood Diaries": [ - "Action-adventure", - "Arcade", - "Platformer", - "Slasher", - "Survival" - ], - "Worms 2": [ - "Action", - "Arcade", - "Fighting", - "Logic", - "Shoot 'em up", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "X-Blades / Ониблэйд": [], - "X-Morph: Defense": [ - "Action", - "Arcade", - "RTS", - "Shoot 'em up", - "Shooter", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Yandere School": [ - "Action-adventure", - "Simulation", - "Third-person" - ], - "Year Walk": [ - "Adventure", - "First-person", - "Logic", - "Puzzle", - "Quest", - "RPG" - ], - "ZOMBI": [ - "Action-adventure", - "Arcade", - "FPS", - "Point-and-click", - "Survival horror" - ], - "Zero Escape: Zero Time Dilemma": [ - "Adventure", - "Logic", - "Puzzle", - "Quest", - "Visual Novel" - ], - "Ziggurat": [ - "Action/RPG", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Zombie Driver HD": [ - "Action", - "Arcade", - "Racing", - "Shooter" - ], - "Zombie Party": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Zombie Vikings": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Hack and slash" - ], - "Zuma Deluxe": [ - "Arcade", - "Casual", - "Logic", - "Puzzle" - ], - "Zuma's Revenge!": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle" - ], - "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation": [], - "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss": [], - "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet": [], - "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor": [], - "Невероятные Приключения Трояна": [], - "Невероятные Приключения Трояна 2": [], - "Невероятные Приключения Трояна 3": [], - "Предатор": [], - "Стоп-Гоп": [], - "Стоп-Гоп 2": [], - "ШХД: ЗИМА / IT'S WINTER": [ - "Casual", - "Simulation" - ], - "Control": [ - "Action-adventure", - "Shooter", - "Third-person" - ], - "Crysis 3": [ - "Action", - "Arcade", - "FPS", - "MMOFPS", - "Open-World", - "Sci-fi", - "Shoot 'em up" - ], - "007: Quantum of Solace": [ - "Action", - "Arcade", - "FPS" - ], - "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Constantine": [ - "Action-adventure", - "Horror", - "Sci-fi", - "Shooter", - "Third-person" - ], - "Devil May Cry": [ - "Action-adventure", - "Fantasy", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry 2": [ - "Action-adventure", - "Fantasy", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry HD Collection": [ - "Action-adventure", - "Arcade", - "Horror", - "Slasher", - "Third-person" - ], - "Kholat": [ - "Action-adventure", - "First-person", - "Horror", - "Quest", - "Survival horror" - ], - "Nihilumbra": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle" - ], - "Prey": [ - "Action", - "Arcade", - "FPS" - ], - "Samorost 3": [ - "Adventure", - "Casual", - "Logic", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Terminator: Resistance": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi" - ], - "The Old City: Leviathan": [ - "Adventure", - "Fantasy", - "First-person", - "Quest" - ], - "Unreal Tournament": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Space" - ], - "You Are Empty": [ - "Action", - "Arcade", - "FPS", - "Post apocalypse", - "Shoot 'em up" - ], - "Rogue Warrior": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Stealth", - "Tactic" - ], - "Counter-Strike": [ - "Action", - "FPS", - "Shoot 'em up", - "Tactic", - "Tactical FPS", - "Wargame" - ], - "Dark Void": [ - "Action-adventure", - "Flying", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person", - "Wargame" - ] -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/generate_games/generate_games.py b/html_parsing/get_game_genres/generate_games/generate_games.py deleted file mode 100644 index af59d72ee..000000000 --- a/html_parsing/get_game_genres/generate_games/generate_games.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import json -from pathlib import Path -import shutil - -import sys -sys.path.append('..') -sys.path.append('../genre_translate_file') - -from db import Dump -from load import load -from common_utils import get_logger - - -log = get_logger('generate_games.txt') - - -DIR = Path(__file__).parent.resolve() -FILE_NAME_GAMES = str(DIR / 'game_by_genres.json') -FILE_NAME_BACKUP = DIR / 'backup' - -FILE_NAME_BACKUP.mkdir(parents=True, exist_ok=True) - -# Example: "Action", "Adventure" -> "Action-adventure" -GENRE_COMPRESSION = [ - ("Action", "Adventure", "Action-adventure"), - ("Action", "RPG", "Action/RPG"), - ("First-person", "Shooter", "FPS"), - ("Survival", "Horror", "Survival horror"), -] - - -def do_genres_compression(genres: list) -> list: - genres = sorted(set(genres)) - to_remove = set() - - for src_1, src_2, target in GENRE_COMPRESSION: - if src_1 in genres and src_2 in genres: - to_remove.add(src_1) - to_remove.add(src_2) - genres.append(target) - - log.info(f'Compress genres {src_1!r} and {src_2!r} -> {target!r}') - - for x in to_remove: - genres.remove(x) - - return sorted(set(genres)) - - -log.info('Start.') - -if Path(FILE_NAME_GAMES).exists(): - backup_file_name = str( - FILE_NAME_BACKUP / (DT.datetime.today().strftime('%d%m%y-%H%M%S_') + Path(FILE_NAME_GAMES).name) - ) - shutil.copy( - FILE_NAME_GAMES, - backup_file_name - ) - log.info(f'Save backup to: {backup_file_name}') - log.info('') - -log.info('Loading cache...') - -game_by_genres = load(FILE_NAME_GAMES) -log.info(f'game_by_genres ({len(game_by_genres)}): {game_by_genres}') - -new_game_by_genres = Dump.dump() -log.info(f'new_game_by_genres ({len(new_game_by_genres)}): {new_game_by_genres}') - -genre_translate = load() -log.info(f'genre_translate ({len(genre_translate)}): {genre_translate}') - -log.info('Finish loading cache.') -log.info('') - -log.info('Search games...') - -number = 0 - -for game, genres in new_game_by_genres.items(): - if game in game_by_genres: - continue - - log.info(f'Added game {game!r} with genres: {genres}') - number += 1 - - new_genres = [] - - for x in genres: - tr_genres = genre_translate.get(x) - if not tr_genres: # null, [], "" - continue - - if isinstance(tr_genres, str): - new_genres.append(tr_genres) - - elif isinstance(tr_genres, list): - new_genres.extend(tr_genres) - - else: - log.warning(f'Unsupported type genres {tr_genres} from {x!r}') - - new_genres = do_genres_compression(new_genres) - - log.info(f'Successful translate genres: {genres} -> {new_genres}') - game_by_genres[game] = new_genres - - log.info('') - -log.info(f'Finish search games. New games: {number}.') - -log.info(f'Saving to {FILE_NAME_GAMES}') - -json.dump( - game_by_genres, - open(FILE_NAME_GAMES, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 -) - -log.info('Finish!') diff --git a/html_parsing/get_game_genres/genre_translate_file/create.py b/html_parsing/get_game_genres/genre_translate_file/create.py deleted file mode 100644 index 788997d41..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/create.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json - -import sys -sys.path.append('..') - -from db import Dump -from load import FILE_NAME_GENRE_TRANSLATE, load -from common_utils import get_logger -from utils import send_sms - - -log = get_logger('genre_translate.txt') - - -def run(): - log.info('Start load genres.') - - genre_translate = load() - - NEED_SMS = True - new_genres = [] - is_first_run = not genre_translate - - log.info(f'Current genres: {len(genre_translate)}') - - for genre in Dump.get_all_genres(): - if genre not in genre_translate: - log.info(f'Added new genre: {genre!r}') - genre_translate[genre] = None - new_genres.append(genre) - - if new_genres: - text = f"Added genres ({len(new_genres)}): {', '.join(new_genres)}" - log.info(text) - - # Если это первый запуск, то смс не отправляем - if not is_first_run: - if NEED_SMS: - send_sms(text, log=log) - - log.info('Save genres') - - json.dump( - genre_translate, - open(FILE_NAME_GENRE_TRANSLATE, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) - - else: - log.info('No new genres') - - log.info('Finish!') - - -if __name__ == '__main__': - run() diff --git a/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json b/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json deleted file mode 100644 index 693bc1650..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json +++ /dev/null @@ -1,312 +0,0 @@ -{ - "2D": "2D", - "3D": "", - "Action": "Action", - "Action Adventure": "Action-adventure", - "Action RPG": "Action/RPG", - "Add-on pack": "", - "Adventure": "Adventure", - "Adventure: Free Roaming": "Adventure", - "Adventure: Graphic": "Adventure", - "Adventure: Point and Click": [ - "Adventure", - "Point-and-click" - ], - "Adventure: Role Playing": [ - "Adventure", - "RPG" - ], - "Adventure: Survival Horror": [ - "Adventure", - "Survival horror" - ], - "Arcade": "Arcade", - "Automobile": "Racing", - "Beat 'Em Up": "Beat 'em up", - "Beat 'Em Up: Hack and Slash": "Beat 'em up", - "Beat-'Em-Up": "Beat 'em up", - "Beat-em-up": "Beat 'em up", - "Board / Card Game": "Cardgame", - "CCG": "Cardgame", - "Card Battle": "Cardgame", - "Card Game": "Cardgame", - "Career": "", - "Casual": "Casual", - "Combat": "Wargame", - "Combat Game": "Wargame", - "Combat Game: Infantry": "Wargame", - "Compilation": "", - "Console-style RPG": "RPG", - "DLC / add-on": "DLC", - "Defense": "Tower Defence", - "Driving": "Racing", - "Driving/Racing": "Racing", - "Early Access": "", - "FPS": "FPS", - "Fantasy": "Fantasy", - "Fighting": "Fighting", - "First-Person": "First-person", - "Fixed-Screen": "", - "Free to Play": "", - "GT / Street": "Racing", - "General": "", - "Hidden Object": "Hidden Object", - "Historic": "", - "Horizontal": "", - "Horror": "Horror", - "Indie": "", - "Japanese-Style": "", - "Light Gun": "", - "Light-Gun": "", - "Linear": "", - "MMOG": "MMO", - "MOBA": "MOBA", - "Massively Multiplayer": "MMO", - "Matching": "", - "Matching/Stacking": "", - "Miscellaneous": "", - "Modern": "", - "Music": "Music", - "Music/Rhythm": "Music/Rhythm", - "Open-World": "Open-World", - "PC-style RPG": "", - "Party": "", - "Party / Minigame": "", - "Party/Minigame": "", - "Platform": "Platformer", - "Platformer": "Platformer", - "Point-and-Click": "Point-and-click", - "Point-and-click": "Point-and-click", - "Puzzle": "Puzzle", - "Puzzle: Physics": "Puzzle", - "RPG": "RPG", - "RTS": "RTS", - "Racing": "Racing", - "Racing / driving": "Racing", - "Rail": "", - "Real-Time": "RTS", - "Rhythm": "Music/Rhythm", - "Roguelike": "Roguelike", - "Role-Playing": "RPG", - "Role-Playing (RPG)": "RPG", - "Sandbox": "Sandbox", - "Sci-Fi": "Sci-fi", - "Shoot 'Em Up": "Shoot 'em up", - "Shoot-'Em-Up": "Shoot 'em up", - "Shoot-em-up": "Shoot 'em up", - "Shooter": "Shooter", - "Simulation": "Simulation", - "Special edition": "", - "Sports": "Sport", - "Strategy": "Strategy", - "Strategy / tactics": [ - "Strategy", - "Tactic" - ], - "Strategy: Combat": "Wargame", - "Strategy: God game": "God game", - "Strategy: Stealth": "", - "Strategy: Trading": "", - "Survival": "Survival", - "TBS": "TBS", - "Tactical": "Tactic", - "Tactics": "Tactic", - "Third-Person": "Third-person", - "Top-Down": "", - "Tower defence": "Tower Defence", - "Trivia/Board Game": "Board game", - "Turn-Based": "TBS", - "VR": "", - "Various: Party Game": "", - "Vehicle": "", - "Vertical": "", - "Virtual": "", - "Virtual Life": "", - "Visual Novel": "Visual Novel", - "Western-Style": "", - "aRPG": "Action/RPG", - "action": "Action", - "add-on": "", - "adventure": "Adventure", - "arcade": "Arcade", - "beat 'em up": "Beat 'em up", - "cards": "Cardgame", - "fighting": "Fighting", - "for adults": "", - "for kids": "", - "hack & slash": "Hack and slash", - "jRPG": "JRPG", - "logic": "Logic", - "online": "", - "racing": "Racing", - "rpg": "RPG", - "runner": "Runner", - "shoot 'em up": "Shoot 'em up", - "simulator": "Simulation", - "stealth": "Stealth", - "strategy": "Strategy", - "survival": "Survival", - "tower defense": "Tower defense", - "Автомобили": "", - "Адвенчура": "Adventure", - "Аддон": "Add-on", - "Аркада": "Arcade", - "Аркадные": "Arcade", - "Аркады": "Arcade", - "Битвы машин": "Robot War", - "Боевик": "Action", - "Боевик от первого лица": [ - "Action", - "First-person" - ], - "Боевик от третьего лица": [ - "Action", - "Third-person" - ], - "Боевик-приключения": "Action-adventure", - "Боевик/Экшн": "Action", - "В пошаговом режиме": "TBS", - "В реальном времени": "RTS", - "Велоспорт": [ - "Cycle", - "Racing" - ], - "Вестерн": "Western", - "Вид сверху": "", - "Визуальный роман": "Visual Novel", - "Виртуальная реальность": "", - "Выживание": "Survival", - "Головоломка": "Puzzle", - "Головоломки": "Puzzle", - "Головоломки/Пазл": "Puzzle", - "Гонки": "Racing", - "Гонки/вождение": "Racing", - "Гоночные/Рейсинг": "Racing", - "Для детей": "", - "Дополнение": "DLC", - "Драки/Файтинг": "Fighting", - "Другой симулятор": "Simulation", - "Зомби": "Zombie", - "Инди": "", - "Интерактивный фильм": "Interactive movie", - "Казуальные": "Casual", - "Карточная": "Cardgame", - "Карточная игра": "Cardgame", - "Карточные": "Cardgame", - "Карточные игры": "Cardgame", - "Карты": "Cardgame", - "Квест": "Quest", - "Киберпанк": "Cyberpunk", - "Кооператив": "", - "Космос": "Space", - "Логика": "Logic", - "Логические": "Logic", - "Логические игры": "Logic", - "ММО": "MMO", - "ММОРПГ/Многопользоватеские онлайновые": "MMORPG", - "Менеджемент": "Time management", - "Менеджер": "Time management", - "Миниигры": "", - "Многопользоватеский шутер": "MMOFPS", - "Мототехника": "Motorcycle", - "Мотоциклы": "Motorcycle", - "Музыка/Ритм": "Music/Rhythm", - "Мультиплеер": "", - "На логику": "Logic", - "Настольные": "Board game", - "Научная фантастика": "Sci-fi", - "Образовательные": "", - "От первого лица": "First-person", - "От третьего лица": "Third-person", - "Открытый мир": "Open-World", - "Песочница": "Sandbox", - "Платформер": "Platformer", - "Платформеры": "Platformer", - "Подземелья": "Dungeon", - "Постапокалипсис": "Post apocalypse", - "Пошаговая": "TBS", - "Пошаговая стратегия": "TBS", - "Приключение": "Adventure", - "Приключения": "Adventure", - "Пристрели их всех": "Shoot 'Em Up", - "Прочее": "", - "РПГ": "RPG", - "Ремастер": "", - "Ремейк": "", - "Ретро": "Retro", - "Роботы": "Robots", - "Ролевая": "RPG", - "Ролевая игра": "RPG", - "Ролевые": "RPG", - "Ролевые игры": "RPG", - "Самостоятельный": "", - "Семейные": "", - "Симулятор": "Simulation", - "Симулятор жизни": "Life simulation", - "Симуляторы": "Simulation", - "Слэшер": "Slasher", - "Спорт": "Sport", - "Спортивные": "Sport", - "Спортивные игры": "Sport", - "Средневековье": "", - "Стелс": "Stealth", - "Стимпанк": "Steampunk", - "Стратегии": "Strategy", - "Стратегия": "Strategy", - "Стратегия в реальном времени": "RTS", - "Стратегия в реальном времени/РТС": "RTS", - "Строительство": "", - "Тактика": "Tactic", - "Тактический": "Tactic", - "Ужасы": "Horror", - "Условно-бесплатная": "", - "Файтинг": "Fighting", - "Файтинги": "Fighting", - "Фэнтези": "Fantasy", - "Шутер": "Shooter", - "Шутер от первого лица": "FPS", - "Шутеры": "Shooter", - "Экшен": "Action", - "Экшены": "Action", - "Юмор": "Humor", - "аркадные": "Arcade", - "визуальные новеллы": "Visual Novel", - "головоломки": "Puzzle", - "гонки": "Racing", - "детские": "", - "для взрослых": "", - "инди": "", - "карточные": "Cardgame", - "квесты": "Quest", - "командные шутеры": "Tactical Shooter", - "логические": "Logic", - "платформеры": "Platformer", - "приключения": "Adventure", - "симуляторы": "Simulation", - "симуляторы бога": "God game", - "спорт": "Sport", - "тактика": "Tactic", - "ужасы": "Horror", - "файтинги": "Fighting", - "шутеры от 3-го лица": "TPS", - "Beat 'em up": "Beat 'em up", - "Interactive Movie": "Interactive Movie", - "Platform Game": "Platform", - "Slasher": "Slasher", - "Survival Horror": "Survival horror", - "TPS": "TPS", - "Tactical FPS": "Tactical FPS", - "City Building": "City Building", - "JRPG": "JRPG", - "Management": "Time management", - "Point&Click": "Point-and-click", - "Scrolling Shooter": "Scrolling Shooter", - "Stealth": "Stealth", - "Tactical RPG": "TRPG", - "Text Game": "Text Game", - "Tower Defense": "Tower Defense", - "Combat Game: Flying": [ - "Wargame", - "Flying" - ] -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json b/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json deleted file mode 100644 index 20babec16..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - - -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/genre_translate_file/load.py b/html_parsing/get_game_genres/genre_translate_file/load.py deleted file mode 100644 index 1eae3f325..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/load.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -from pathlib import Path - - -FILE_NAME_GENRE_TRANSLATE = str(Path(__file__).parent.resolve() / 'data' / 'genre_translate.json') - - -def load(file_name: str = FILE_NAME_GENRE_TRANSLATE) -> dict: - try: - genre_translate = json.load( - open(file_name, encoding='utf-8') - ) - except: - genre_translate = dict() - - return genre_translate - - -if __name__ == '__main__': - genre_translate = load() - print(f'Genre_translate ({len(genre_translate)}): {genre_translate}') - print() - - # Print all undefined genres without '{' / '}' and indent - genre_null_translate = { - k: v - for k, v in genre_translate.items() - if v is None - } - print(f'Genre null translate ({len(genre_null_translate)}):') - json_text = json.dumps(genre_null_translate, ensure_ascii=False, indent=4) - lines = json_text.splitlines()[1:-1] - for i, line in enumerate(lines): - print(line.strip()) - if i > 0 and i % 40 == 0: - print() diff --git a/html_parsing/get_game_genres/genre_translate_file/merge.py b/html_parsing/get_game_genres/genre_translate_file/merge.py deleted file mode 100644 index 1c9af6711..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/merge.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import json -import shutil -from pathlib import Path - -import sys -sys.path.append('..') - -from load import load, FILE_NAME_GENRE_TRANSLATE -from common_utils import get_logger - - -FILE_NAME_MERGE_GENRE_TRANSLATE = str(Path(__file__).parent.resolve() / 'data' / 'merge_genre_translate.json') - -DIR = Path(__file__).parent.resolve() -FILE_NAME_BACKUP = DIR / 'backup' -FILE_NAME_BACKUP.mkdir(parents=True, exist_ok=True) - -log = get_logger('merge_genre_translate.txt') - -log.info('Start.') - -backup_file_name = ( - FILE_NAME_BACKUP / (DT.datetime.today().strftime('%d%m%y-%H%M%S_') + Path(FILE_NAME_GENRE_TRANSLATE).name) -) -log.info(f'Save backup to: {backup_file_name}') -shutil.copy( - FILE_NAME_GENRE_TRANSLATE, - backup_file_name -) - -log.info('Load genres') - -genre_translate = load() -log.info( - f'Current genres: {len(genre_translate)}. ' - f'Null genres: {sum(1 for v in genre_translate.values() if v is None)}' -) - -if genre_translate: - number = 0 - - log.info('Load merge') - - merge_genre_translate = load(FILE_NAME_MERGE_GENRE_TRANSLATE) - log.info(f'Current merged genres: {len(merge_genre_translate)}') - - for k, v in merge_genre_translate.items(): - if v is not None and k in genre_translate and genre_translate.get(k) is None: - log.info(f'Merge: {k!r} -> {v!r}') - genre_translate[k] = v - number += 1 - - log.info(f'New merged: {number}') - log.info('Save merged genres') - - json.dump( - genre_translate, - open(FILE_NAME_GENRE_TRANSLATE, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) -else: - log.info('Empty genres. Skip.') - -log.info('Finish!') diff --git a/html_parsing/get_game_genres/genre_translate_file/utils.py b/html_parsing/get_game_genres/genre_translate_file/utils.py deleted file mode 100644 index 8967073bc..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/utils.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os - - -SMS_API_ID = os.getenv('SMS_API_ID') -SMS_SEND_TO = os.getenv('SMS_SEND_TO') - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/157b188116ced699b76986420cdc8378e05296d8/Check%20with%20notification/all_common.py#L57 -def send_sms(text: str, log): - if not SMS_API_ID or not SMS_SEND_TO: - log.warning('Переменные окружения SMS_API_ID или SMS_SEND_TO не указаны, отправка СМС невозможна!') - return - - log.info(f'Отправка sms: {text!r}') - - if len(text) > 70: - text = text[:70-3] + '...' - log.info(f'Текст sms будет сокращено, т.к. слишком длинное (больше 70 символов): {text!r}') - - # Отправляю смс на номер - url = 'https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}'.format( - api_id=SMS_API_ID, - to=SMS_SEND_TO, - text=text - ) - log.debug(repr(url)) - - while True: - try: - import requests - rs = requests.get(url) - log.debug(repr(rs.text)) - - break - - except: - log.exception("При отправке sms произошла ошибка:") - log.debug('Через 5 минут попробую снова...') - - # Wait 5 minutes before next attempt - import time - time.sleep(5 * 60) diff --git a/html_parsing/get_game_genres/main.py b/html_parsing/get_game_genres/main.py deleted file mode 100644 index 0e11c5327..000000000 --- a/html_parsing/get_game_genres/main.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from timeit import default_timer -from threading import Thread -import time -import sys - -sys.path.append('genre_translate_file') -import create as create_genre_translate - -from db import db_create_backup, Dump, db -from common_utils import get_parsers, get_games_list, wait, get_logger, AtomicCounter, seconds_to_str, print_parsers - - -IGNORE_SITE_NAMES = ['gamefaqs_gamespot_com'] - -# Test -USE_FAKE_PARSER = False -if USE_FAKE_PARSER: - class FakeParser: - @classmethod - def get_site_name(cls): return "" - - @staticmethod - def get_game_genres(game_name): - if game_name == 'Foo': - raise Exception('Error') - - return ['RGB-bar', 'Action-bar'] - - # Monkey Patch - def get_parsers(): - return [FakeParser] - - def get_games_list(): return ['Foo', 'Bar', 'Zet'] - - -log = get_logger() -counter = AtomicCounter() - - -def run_parser(parser, games: list, max_num_request=5): - try: - pauses = [ - ('15 minutes', 15 * 60), - ('30 minutes', 30 * 60), - ('45 minutes', 45 * 60), - ('1 hour', 60 * 60), - ] - SITE_NAME = parser.get_site_name() - timeout = 3 # 3 seconds - MAX_TIMEOUT = 10 # 10 seconds - TIMEOUT_EVERY_N_GAMES = 50 # Every 50 games - TIMEOUT_BETWEEN_N_GAMES = 3 * 60 # 3 minutes - number = 0 - - for game_name in games: - try: - if Dump.exists(SITE_NAME, game_name): - continue - - number += 1 - - num_request = 0 - - while True: - num_request += 1 - try: - if num_request == 1: - log.info(f'#{number}. Search genres for {game_name!r} ({SITE_NAME})') - else: - log.info(f'#{number}. Search genres for {game_name!r} ({SITE_NAME}). ' - f'Attempts {num_request}/{max_num_request}') - - genres = parser.get_game_genres(game_name) - log.info(f'#{number}. Found genres {game_name!r} ({SITE_NAME}): {genres}') - - Dump.add(SITE_NAME, game_name, genres) - counter.inc() - - time.sleep(timeout) - break - - except: - log.exception(f'#{number}. Error on request {num_request}/{max_num_request} ({SITE_NAME})') - if num_request >= max_num_request: - log.info(f'#{number}. Attempts ended for {game_name!r} ({SITE_NAME})') - break - - pause_text, pause_secs = pauses[num_request - 1] - log.info(f'#{number}. Pause: {pause_text} secs') - time.sleep(pause_secs) - - timeout += 1 - if timeout > MAX_TIMEOUT: - timeout = MAX_TIMEOUT - - if number % TIMEOUT_EVERY_N_GAMES == 0: - log.info( - f'#{number}. Pause for every {TIMEOUT_EVERY_N_GAMES} games: {TIMEOUT_BETWEEN_N_GAMES} secs' - ) - time.sleep(TIMEOUT_BETWEEN_N_GAMES) - - except: - log.exception(f'#{number}. Error by game {game_name!r} ({SITE_NAME})') - - except: - log.exception(f'Error:') - - -if __name__ == "__main__": - parsers = [x for x in get_parsers() if x.get_site_name() not in IGNORE_SITE_NAMES] - print_parsers(parsers, log=lambda *args, **kwargs: log.info(*args, **kwargs)) - - while True: - try: - log.info(f'Started') - t = default_timer() - - db_create_backup() - - games = get_games_list() - log.info(f'Total games: {len(games)}') - - threads = [] - for parser in parsers: - threads.append( - Thread(target=run_parser, args=[parser, games]) - ) - log.info(f'Total parsers/threads: {len(threads)}') - log.info(f'Ignore parsers ({len(IGNORE_SITE_NAMES)}): {", ".join(IGNORE_SITE_NAMES)}') - - counter.value = 0 - - for thread in threads: - thread.start() - - for thread in threads: - thread.join() - - log.info(f'Finished. Added games: {counter.value}. Total games: {Dump.select().count()}. ' - f'Elapsed time: {seconds_to_str(default_timer() - t)}') - - create_genre_translate.run() - - wait(days=1) - - except: - log.exception('') - wait(minutes=15) - - finally: - log.info('') diff --git a/html_parsing/get_game_genres/parsers/__init__.py b/html_parsing/get_game_genres/parsers/__init__.py deleted file mode 100644 index 06334aa61..000000000 --- a/html_parsing/get_game_genres/parsers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - diff --git a/html_parsing/get_game_genres/parsers/ag_ru.py b/html_parsing/get_game_genres/parsers/ag_ru.py deleted file mode 100644 index f0007948c..000000000 --- a/html_parsing/get_game_genres/parsers/ag_ru.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class AgRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'Host': 'ag.ru', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate', - } - - # Для правдоподобности сделаем запрос на страницу с играми - self.send_get('https://ag.ru/games/pc', headers=headers) - - # Заголовки, что отправляются вместе с запросом к API - headers.update({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Referer': 'https://ag.ru/games/pc', - 'X-API-Language': 'ru', - 'X-API-Referer': '%2Fgames', - 'X-API-Client': 'website', - }) - - # По умолчанию, page_size=20, но столько результатов не нужно. По хорошему, можно page_size=1, но есть - # шанс, что игра, что ищем сервером вернется не в первых позициях - rs = self.send_get(f'https://ag.ru/api/games?page_size=5&search={self.game_name}&page=1', headers=headers) - - for item in rs.json()['results']: - title = item['name'] - if not self.is_found_game(title): - continue - - genres = [x['name'] for x in item['genres']] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return AgRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Шутеры', 'Экшены', 'Ролевые'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Экшены', 'Ролевые'] - # - # Search 'Twin Sector'... - # Genres: ['Приключения', 'Экшены'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Шутеры'] diff --git a/html_parsing/get_game_genres/parsers/base_parser.py b/html_parsing/get_game_genres/parsers/base_parser.py deleted file mode 100644 index f42eed482..000000000 --- a/html_parsing/get_game_genres/parsers/base_parser.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from abc import ABCMeta, abstractmethod -from typing import List, Union -from pathlib import Path -import unicodedata - -# For common.py -import sys -sys.path.append('..') - -from bs4 import BeautifulSoup -import requests - -from common import ( - DIR_ERRORS, DIR_LOGS, NEED_LOGS, LOG_FORMAT, USER_AGENT, - pretty_path, get_uniques, get_current_datetime_str, smart_comparing_names, - get_valid_filename -) -import dump - - -class Singleton(ABCMeta): - _instances = dict() - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super().__call__(*args, **kwargs) - - return cls._instances[cls] - - -class BaseParser(metaclass=Singleton): - _site_name = '' - - def __init__(self, need_logs=NEED_LOGS, dir_errors=DIR_ERRORS, dir_logs=DIR_LOGS, log_format=LOG_FORMAT): - self.session = requests.session() - self.session.headers['User-Agent'] = USER_AGENT - - self._dir_errors = pretty_path(dir_errors) - self._dir_logs = pretty_path(dir_logs) - - self.game_name = '' - self._need_logs = need_logs - - self._log = self._get_logger(log_format) - - @classmethod - def instance(cls, *args, **kwargs): - return cls(*args, **kwargs) - - def send_get(self, url: str, return_html=False, **kwargs) -> Union[requests.Response, BeautifulSoup]: - rs = self.session.get(url, **kwargs) - self._on_check_response(rs) - - if return_html: - return BeautifulSoup(rs.content, 'html.parser') - - return rs - - def send_post(self, url: str, data=None, json=None, return_html=False, **kwargs) -> Union[requests.Response, BeautifulSoup]: - rs = self.session.post(url, data=data, json=json, **kwargs) - self._on_check_response(rs) - - if return_html: - return BeautifulSoup(rs.content, 'html.parser') - - return rs - - def _save_error_response(self, rs: requests.Response): - Path(self._dir_errors).mkdir(parents=True, exist_ok=True) - - file_name = pretty_path( - f'{self._dir_errors}/{self.get_site_name()}_{get_valid_filename(self.game_name)}_{get_current_datetime_str()}.dump' - ) - self.log_debug(f'Save dump to {file_name}') - - data = dump.dump_all(rs, request_prefix=b'> ', response_prefix=b'< ') - with open(file_name, 'wb') as f: - f.write(data) - - def _on_check_response(self, rs: requests.Response): - if rs.ok: - return - - self.log_warn(f'Something went wrong...: status_code: {rs.status_code}\n{rs.text}') - self._save_error_response(rs) - - def log_debug(self, msg, *args, **kwargs): - self._need_logs and self._log.debug(msg, *args, **kwargs) - - def log_info(self, msg, *args, **kwargs): - self._need_logs and self._log.info(msg, *args, **kwargs) - - def log_warn(self, msg, *args, **kwargs): - self._need_logs and self._log.warning(msg, *args, **kwargs) - - def log_error(self, msg, *args, **kwargs): - self._need_logs and self._log.error(msg, *args, **kwargs) - - def log_exception(self, msg, *args, **kwargs): - self._need_logs and self._log.exception(msg, *args, **kwargs) - - @classmethod - def get_site_name(cls) -> str: - if not cls._site_name: - import inspect - cls._site_name = Path(inspect.getfile(cls)).stem - return cls._site_name - - @abstractmethod - def _parse(self) -> List[str]: - pass - - def is_found_game(self, game_name: str) -> bool: - return smart_comparing_names(self.game_name, game_name) - - def get_game_genres(self, game_name: str) -> List[str]: - self.game_name = game_name - self.log_info(f'Search {game_name!r}...') - - try: - genres = self._parse() - genres = [x.strip() for x in genres] - genres = get_uniques(genres) - - except BaseException as e: - self.log_exception('Parsing error:') - raise e - - self.log_info(f'Genres: {genres}') - return genres - - # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/163c91d6882b548c904ad40703dac00c0a64e5a2/logger_example.py#L7 - def _get_logger(self, log_format: str, encoding='utf-8'): - Path(self._dir_logs).mkdir(parents=True, exist_ok=True) - - site = self.get_site_name() - - name = 'parser_' + site - file = self._dir_logs + '/' + site + '.txt' - - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter(log_format) - - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10_000_000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - @classmethod - def get_norm_text(cls, node) -> str: - if not node: - return "" - - text = node.get_text(strip=True) - - # NFKD ™ превратит в TM, что исказит текст, лучше удалить - text = text.replace('™', '').replace('©', '').replace('©', '®') - - # https://ru.wikipedia.org/wiki/Юникод#NFKD - # unicodedata.normalize для удаления \xa0 и подобных символов-заменителей - return unicodedata.normalize("NFKD", text) diff --git a/html_parsing/get_game_genres/parsers/dump.py b/html_parsing/get_game_genres/parsers/dump.py deleted file mode 100644 index 23b35e75a..000000000 --- a/html_parsing/get_game_genres/parsers/dump.py +++ /dev/null @@ -1,197 +0,0 @@ -"""This module provides functions for dumping information about responses.""" -import collections - -from requests import compat - - -__all__ = ('dump_response', 'dump_all') - -HTTP_VERSIONS = { - 9: b'0.9', - 10: b'1.0', - 11: b'1.1', -} - -_PrefixSettings = collections.namedtuple('PrefixSettings', - ['request', 'response']) - - -class PrefixSettings(_PrefixSettings): - def __new__(cls, request, response): - request = _coerce_to_bytes(request) - response = _coerce_to_bytes(response) - return super(PrefixSettings, cls).__new__(cls, request, response) - - -def _get_proxy_information(response): - if getattr(response.connection, 'proxy_manager', False): - proxy_info = {} - request_url = response.request.url - if request_url.startswith('https://'): - proxy_info['method'] = 'CONNECT' - - proxy_info['request_path'] = request_url - return proxy_info - return None - - -def _format_header(name, value): - return (_coerce_to_bytes(name) + b': ' + _coerce_to_bytes(value) + - b'\r\n') - - -def _build_request_path(url, proxy_info): - uri = compat.urlparse(url) - proxy_url = proxy_info.get('request_path') - if proxy_url is not None: - request_path = _coerce_to_bytes(proxy_url) - return request_path, uri - - request_path = _coerce_to_bytes(uri.path) - if uri.query: - request_path += b'?' + _coerce_to_bytes(uri.query) - - return request_path, uri - - -def _dump_request_data(request, prefixes, bytearr, proxy_info=None): - if proxy_info is None: - proxy_info = {} - - prefix = prefixes.request - method = _coerce_to_bytes(proxy_info.pop('method', request.method)) - request_path, uri = _build_request_path(request.url, proxy_info) - - # HTTP/1.1 - bytearr.extend(prefix + method + b' ' + request_path + b' HTTP/1.1\r\n') - - # Host: OR host header specified by user - headers = request.headers.copy() - host_header = _coerce_to_bytes(headers.pop('Host', uri.netloc)) - bytearr.extend(prefix + b'Host: ' + host_header + b'\r\n') - - for name, value in headers.items(): - bytearr.extend(prefix + _format_header(name, value)) - - bytearr.extend(prefix + b'\r\n') - if request.body: - if isinstance(request.body, compat.basestring): - bytearr.extend(prefix + _coerce_to_bytes(request.body)) - else: - # In the event that the body is a file-like object, let's not try - # to read everything into memory. - bytearr.extend(b'<< Request body is not a string-like type >>') - bytearr.extend(b'\r\n') - - -def _dump_response_data(response, prefixes, bytearr): - prefix = prefixes.response - # Let's interact almost entirely with urllib3's response - raw = response.raw - - # Let's convert the version int from httplib to bytes - version_str = HTTP_VERSIONS.get(raw.version, b'?') - - # HTTP/ - bytearr.extend(prefix + b'HTTP/' + version_str + b' ' + - str(raw.status).encode('ascii') + b' ' + - _coerce_to_bytes(response.reason) + b'\r\n') - - headers = raw.headers - for name in headers.keys(): - for value in headers.getlist(name): - bytearr.extend(prefix + _format_header(name, value)) - - bytearr.extend(prefix + b'\r\n') - - bytearr.extend(response.content) - - -def _coerce_to_bytes(data): - if not isinstance(data, bytes) and hasattr(data, 'encode'): - data = data.encode('utf-8') - # Don't bail out with an exception if data is None - return data if data is not None else b'' - - -def dump_response(response, request_prefix=b'< ', response_prefix=b'> ', - data_array=None): - """Dump a single request-response cycle's information. - - This will take a response object and dump only the data that requests can - see for that single request-response cycle. - - Example:: - - import requests - from requests_toolbelt.utils import dump - - resp = requests.get('https://api.github.com/users/sigmavirus24') - data = dump.dump_response(resp) - print(data.decode('utf-8')) - - :param response: - The response to format - :type response: :class:`requests.Response` - :param request_prefix: (*optional*) - Bytes to prefix each line of the request data - :type request_prefix: :class:`bytes` - :param response_prefix: (*optional*) - Bytes to prefix each line of the response data - :type response_prefix: :class:`bytes` - :param data_array: (*optional*) - Bytearray to which we append the request-response cycle data - :type data_array: :class:`bytearray` - :returns: Formatted bytes of request and response information. - :rtype: :class:`bytearray` - """ - data = data_array if data_array is not None else bytearray() - prefixes = PrefixSettings(request_prefix, response_prefix) - - if not hasattr(response, 'request'): - raise ValueError('Response has no associated request') - - proxy_info = _get_proxy_information(response) - _dump_request_data(response.request, prefixes, data, - proxy_info=proxy_info) - _dump_response_data(response, prefixes, data) - return data - - -def dump_all(response, request_prefix=b'< ', response_prefix=b'> '): - """Dump all requests and responses including redirects. - - This takes the response returned by requests and will dump all - request-response pairs in the redirect history in order followed by the - final request-response. - - Example:: - - import requests - from requests_toolbelt.utils import dump - - resp = requests.get('https://httpbin.org/redirect/5') - data = dump.dump_all(resp) - print(data.decode('utf-8')) - - :param response: - The response to format - :type response: :class:`requests.Response` - :param request_prefix: (*optional*) - Bytes to prefix each line of the request data - :type request_prefix: :class:`bytes` - :param response_prefix: (*optional*) - Bytes to prefix each line of the response data - :type response_prefix: :class:`bytes` - :returns: Formatted bytes of request and response information. - :rtype: :class:`bytearray` - """ - data = bytearray() - - history = list(response.history[:]) - history.append(response) - - for response in history: - dump_response(response, request_prefix, response_prefix, data) - - return data diff --git a/html_parsing/get_game_genres/parsers/gamebomb_ru.py b/html_parsing/get_game_genres/parsers/gamebomb_ru.py deleted file mode 100644 index cb7c17d36..000000000 --- a/html_parsing/get_game_genres/parsers/gamebomb_ru.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamebombRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'Origin': 'http://gamebomb.ru', - 'Referer': 'http://gamebomb.ru/games', - 'Accept': 'application/json', - } - data = { - 'query': self.game_name, - 'type': '', - } - - url = f'http://gamebomb.ru/base/ajaxSearch' - rs = self.send_post(url, data=data, headers=headers) - - for game_block_preview in rs.json(): - if game_block_preview['type'] != 'игра': - continue - - title = game_block_preview['title'] - if not self.is_found_game(title): - continue - - url_game = urljoin(rs.url, game_block_preview['url']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - # - # Жанры - # - #
- # - # - # Шутер от первого лица - #
- #
- # - # - # Боевик-приключения - #
- game_block = game_block.find('td', text='Жанры') - if not game_block: - continue - - genres = [] - for div in game_block.find_next_sibling('td').find_all('div'): - if not div.select('input[name*="genres"]'): - continue - - genres.append(self.get_norm_text(div)) - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamebombRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Боевик/Экшн', 'Ролевые игры', 'Шутер от первого лица', 'ММОРПГ/Многопользоватеские онлайновые'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Боевик/Экшн', 'Ролевые игры'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Головоломки/Пазл', 'Шутер от первого лица'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Боевик/Экшн', 'Приключения', 'Шутер от первого лица'] diff --git a/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py b/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py deleted file mode 100644 index f2bc18c81..000000000 --- a/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamefaqsGamespotCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://gamefaqs.gamespot.com/search?game={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.search_results_title > .search_result'): - a = game_block_preview.select_one('.sr_name > a.log_search') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - game_info = game_block.select_one('.pod_gameinfo_left') - if not game_info: - return [] - - #
  • Genre: - # Action » - # Shooter » - # Third-Person » - # Arcade - #
  • - genres = [ - self.get_norm_text(a) - for a in game_info.select_one('li > b:contains("Genre:")').find_next_siblings('a') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamefaqsGamespotCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing', 'Western-Style'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Role-Playing', 'Action RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action Adventure', 'Linear'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action Adventure', 'Survival'] diff --git a/html_parsing/get_game_genres/parsers/gameguru_ru.py b/html_parsing/get_game_genres/parsers/gameguru_ru.py deleted file mode 100644 index f0413ae19..000000000 --- a/html_parsing/get_game_genres/parsers/gameguru_ru.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class GameguruRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://gameguru.ru/search/all.html?s={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block in root.select('.jointCard-result-game-unit'): - title = self.get_norm_text(game_block.select_one('.jointCard-result-game-list-title')) - if not self.is_found_game(title): - continue - - genres = [self.get_norm_text(a) for a in game_block.select('a') if '/genre/' in a['href']] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GameguruRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Экшен', 'RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['RPG', 'aRPG'] - # - # Search 'Twin Sector'... - # Genres: ['Экшен'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Экшен', 'Шутер', 'Квест'] diff --git a/html_parsing/get_game_genres/parsers/gamer_info_com.py b/html_parsing/get_game_genres/parsers/gamer_info_com.py deleted file mode 100644 index b75087a2b..000000000 --- a/html_parsing/get_game_genres/parsers/gamer_info_com.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class GamerInfoCom_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - } - form_data = { - 'search-query': self.game_name, - 'search-obl': 'games', - 'page': '1', - } - - url = 'https://gamer-info.com/search-q/' - root = self.send_post(url, headers=headers, data=form_data, return_html=True) - - for game_block in root.select('.games > .c2'): - g = game_block.select_one('.g') - if 'Жанр:' not in g.text: - continue - - title = self.get_norm_text(game_block.select_one('.n')) - if not self.is_found_game(title): - continue - - genres = g.text.replace('Жанр:', '').strip().split(', ') - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamerInfoCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['action', 'RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['action', 'приключения'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['action', 'приключения'] diff --git a/html_parsing/get_game_genres/parsers/gamespot_com.py b/html_parsing/get_game_genres/parsers/gamespot_com.py deleted file mode 100644 index 6ad5639d0..000000000 --- a/html_parsing/get_game_genres/parsers/gamespot_com.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamespotCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.gamespot.com/search/?i=site&q={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.media-body'): - if not game_block_preview.select_one('.media-date'): - continue - - a = game_block_preview.select_one('.media-title a') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - - tag_object_stats = game_block.select_one('#object-stats-wrap') - if not tag_object_stats: - return [] - - genres = [self.get_norm_text(a) for a in tag_object_stats.select('a[href]') if '/genre/' in a['href']] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamespotCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Action', 'Adventure'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Adventure', 'Survival', '3D', 'Action'] diff --git a/html_parsing/get_game_genres/parsers/igromania_ru.py b/html_parsing/get_game_genres/parsers/igromania_ru.py deleted file mode 100644 index de3caf917..000000000 --- a/html_parsing/get_game_genres/parsers/igromania_ru.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class IgromaniaRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - } - form_data = { - 'mode': '11', - 's': '1', - 'p': '1', - 'fg': 'all', - 'fp': 'all', - 'fn': self.game_name - } - - url = 'https://www.igromania.ru/-Engine-/AJAX/games.list.v2/index.php' - root = self.send_post(url, headers=headers, data=form_data, return_html=True) - - for game_block in root.select('.gamebase_box'): - title = self.get_norm_text(game_block.select_one('.release_name')) - if not self.is_found_game(title): - continue - - genres = [self.get_norm_text(a) for a in game_block.select('.genre > a')] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return IgromaniaRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Боевик', 'Боевик от первого лица', 'Боевик от третьего лица', 'Ролевая игра'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Ролевая игра', 'Боевик', 'Боевик от третьего лица'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Боевик', 'Боевик от первого лица'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Боевик', 'Ужасы', 'Боевик от первого лица', 'Приключение'] diff --git a/html_parsing/get_game_genres/parsers/iwantgames_ru.py b/html_parsing/get_game_genres/parsers/iwantgames_ru.py deleted file mode 100644 index 2370778a0..000000000 --- a/html_parsing/get_game_genres/parsers/iwantgames_ru.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class IwantgamesRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://iwantgames.ru/?s={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block in root.select('.game__content'): - title = self.get_norm_text(game_block.h2.a) - if not self.is_found_game(title): - continue - - #
    Жанр:
    - #
    - # РПГ, - # Ужасы, - # Экшен - #
    - # -> ['РПГ', 'Ужасы', 'Экшен'] - dt = game_block.find('dt', text='Жанр:') - if not dt: - continue - - dd = dt.find_next_sibling('dd') - if not dd: - continue - - genres = [self.get_norm_text(a) for a in dd.find_all('a')] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return IwantgamesRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test, TEST_GAMES - TEST_GAMES.append('Dark Souls: Remastered') - - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: [] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: [] - # - # Search 'Dark Souls: Remastered'... - # Genres: ['РПГ', 'Ужасы', 'Экшен'] diff --git a/html_parsing/get_game_genres/parsers/metacritic_com.py b/html_parsing/get_game_genres/parsers/metacritic_com.py deleted file mode 100644 index e63792362..000000000 --- a/html_parsing/get_game_genres/parsers/metacritic_com.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class MetacriticCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.metacritic.com/search/game/{self.game_name}/results' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.result'): - a = game_block_preview.select_one('.product_title > a') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - #
  • - # Genre(s): - # Role-Playing, - # Action RPG - #
  • - genres = [ - self.get_norm_text(a) for a in game_block.select('.summary_detail.product_genre > .data') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return MetacriticCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing', 'First-Person', 'First-Person', 'Western-Style'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action RPG', 'Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Role-Playing', 'Action RPG', 'Action RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action Adventure', 'Modern', 'General', 'Modern', 'Linear'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action Adventure', 'Horror', 'Survival'] diff --git a/html_parsing/get_game_genres/parsers/mobygames_com.py b/html_parsing/get_game_genres/parsers/mobygames_com.py deleted file mode 100644 index 1218c8f19..000000000 --- a/html_parsing/get_game_genres/parsers/mobygames_com.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class MobygamesCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.mobygames.com/search/quick?q={self.game_name}&p=3&search=Go&sFilter=1&sG=on' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.searchTitle > a'): - title = self.get_norm_text(game_block_preview) - if not self.is_found_game(title): - continue - - href = game_block_preview['href'] - url_game = urljoin(url, href) - - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - genres = game_block\ - .select_one('#coreGameGenre').find_next('div', text='Genre')\ - .find_next_sibling('div').find_all('a') - - genres = [self.get_norm_text(a) for a in genres] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return MobygamesCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing (RPG)'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action', 'Role-Playing (RPG)'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Action', 'Role-Playing (RPG)'] - # - # Search 'Twin Sector'... - # Genres: ['Action'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action'] diff --git a/html_parsing/get_game_genres/parsers/playground_ru.py b/html_parsing/get_game_genres/parsers/playground_ru.py deleted file mode 100644 index 9744e90c8..000000000 --- a/html_parsing/get_game_genres/parsers/playground_ru.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class PlaygroundRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.playground.ru/site-search/?q={self.game_name}&filter=game' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.search-results .title'): - title = self.get_norm_text(game_block_preview) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, game_block_preview['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - #
    - # Экшен - # - # Ролевая - # - genres = [ - self.get_norm_text(a) for a in game_block.select('.genres > .item') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return PlaygroundRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Экшен', 'Ролевая', 'От первого лица', 'От третьего лица', 'Фэнтези'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Экшен', 'Ролевая', 'Стимпанк'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Экшен'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Экшен', 'Адвенчура', 'Ужасы', 'От первого лица'] diff --git a/html_parsing/get_game_genres/parsers/spong_com.py b/html_parsing/get_game_genres/parsers/spong_com.py deleted file mode 100644 index 302ed6a2b..000000000 --- a/html_parsing/get_game_genres/parsers/spong_com.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class SpongCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://spong.com/search/index.jsp?q={self.game_name}' - root = self.send_get(url, return_html=True) - - # Первая таблица -- та, что нужна нам - result = root.select_one('table.searchResult') - if result: - for game_block in result.select('tr'): - tds = game_block.select('td') - if len(tds) != 4: # Например, tr > th - continue - - td_title, _, genres_td, platforms_td = tds - - title = self.get_norm_text(td_title.a) - if not self.is_found_game(title): - continue - - # Adventure: Free Roaming
    Adventure: Survival Horror
    - # -> ['Adventure: Free Roaming', 'Adventure: Survival Horror'] - genres = list(genres_td.stripped_strings) - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return SpongCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Adventure: Survival Horror'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Adventure: Role Playing'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Adventure: Role Playing'] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Adventure: Survival Horror'] diff --git a/html_parsing/get_game_genres/parsers/squarefaction_ru.py b/html_parsing/get_game_genres/parsers/squarefaction_ru.py deleted file mode 100644 index 6e982ab8f..000000000 --- a/html_parsing/get_game_genres/parsers/squarefaction_ru.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List -from bs4 import BeautifulSoup -from base_parser import BaseParser - - -class SquarefactionRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'http://squarefaction.ru/main/search/games?q={self.game_name}' - rs = self.send_get(url) - - root = BeautifulSoup(rs.content, 'html.parser') - - # http://squarefaction.ru/main/search/games?q=dead+space - if '/main/search/games' in rs.url: - self.log_info(f'Parsing of game list') - - for game_block in root.select('#games > .entry'): - title = self.get_norm_text(game_block.select_one('.name')) - if not self.is_found_game(title): - continue - - #
    TPS,Survival Horror,Action
    - genres = self.get_norm_text(game_block.select_one('.infos')).split(',') - - # Сойдет первый, совпадающий по имени, вариант - return genres - - # http://squarefaction.ru/game/dead-space - else: - self.log_info(f'Parsing of game page') - - game_block = root.select_one('#page-info') - if game_block: - title = self.get_norm_text(game_block.select_one('#title')) - if not self.is_found_game(title): - self.log_warn(f'Not match game title {title!r}') - - # - # TPS, - # Survival Horror, - # Action - # - genres = [ - self.get_norm_text(a) for a in game_block.select('a') if '?genre=' in a['href'] - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return SquarefactionRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Action RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Survival Horror'] diff --git a/html_parsing/get_game_genres/parsers/stopgame_ru.py b/html_parsing/get_game_genres/parsers/stopgame_ru.py deleted file mode 100644 index 9fee8b117..000000000 --- a/html_parsing/get_game_genres/parsers/stopgame_ru.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class StopgameRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://stopgame.ru/search/?s={self.game_name}&where=games&sort=name' - root = self.send_get(url, return_html=True) - - for game_block in root.select('.game-block'): - title = self.get_norm_text(game_block.select_one('.title')) - if not self.is_found_game(title): - continue - - genres = [ - self.get_norm_text(a) for a in game_block.select('.game-genre-value > a') if '?genre[]' in a['href'] - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return StopgameRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['action', 'rpg'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['rpg'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['action', 'logic'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['action', 'adventure'] diff --git a/html_parsing/get_game_genres/parsers/store_steampowered_com.py b/html_parsing/get_game_genres/parsers/store_steampowered_com.py deleted file mode 100644 index b0106e642..000000000 --- a/html_parsing/get_game_genres/parsers/store_steampowered_com.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class StoreSteampoweredCom_Parser(BaseParser): - def _parse(self) -> List[str]: - # category1 = Игры - url = f'https://store.steampowered.com/search/?term={self.game_name}&category1=998' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.search_result_row'): - title = self.get_norm_text(game_block_preview.select_one('.search_name > .title')) - if not self.is_found_game(title): - continue - - href = game_block_preview['href'] - url_game = urljoin(url, href) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - #
    - # Title: HELLGATE: London
    - # Genre: - # Action, - # RPG - genres = [ - self.get_norm_text(a) for a in game_block.select('.details_block > a[href*="/genre/"]') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return StoreSteampoweredCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Action', 'RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action', 'Adventure', 'Indie', 'RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Action', 'RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action', 'Adventure'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: [] diff --git a/html_parsing/get_population_from_wikidata.py b/html_parsing/get_population_from_wikidata.py index b298fdfe9..d845cdd57 100644 --- a/html_parsing/get_population_from_wikidata.py +++ b/html_parsing/get_population_from_wikidata.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests @@ -10,17 +10,17 @@ def get_populations(url: str) -> dict: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") # P1082 -- идентификатор для population - population_node = root.select_one('#P1082') + population_node = root.select_one("#P1082") populations = dict() # Перебор строк в соседнем от population столбце - for row in population_node.select('.wikibase-statementview'): + for row in population_node.select(".wikibase-statementview"): # Небольшая хитрость -- берем только первые 2 значения, поидеи это будут: количество людей и дата - number_str, data_str = row.select('.wikibase-snakview-value')[:2] + number_str, data_str = row.select(".wikibase-snakview-value")[:2] # Вытаскиваем текст из number_str = number_str.text.strip() @@ -48,14 +48,15 @@ def get_population_from_url_by_year(url: str, year: int) -> str: return get_population_by_year(populations, year) -if __name__ == '__main__': - url = 'https://www.wikidata.org/wiki/Q148' +if __name__ == "__main__": + url = "https://www.wikidata.org/wiki/Q148" populations = get_populations(url) - print(populations) # {2012: '1,375,198,619', 2010: '1,359,755,102', 2015: '1,397,028,553', ... + print(populations) + # {2012: '1,375,198,619', 2010: '1,359,755,102', 2015: '1,397,028,553', ... # Выводим данные с сортировкой по ключу: по возрастанию for year in sorted(populations): - print("{}: {}".format(year, populations[year])) + print(f"{year}: {populations[year]}") # 2010: 1,359,755,102 # 2011: 1,367,480,264 diff --git a/html_parsing/get_price_game/from_direct2drive.py b/html_parsing/get_price_game/from_direct2drive.py index 61637d174..5c7c77cfb 100644 --- a/html_parsing/get_price_game/from_direct2drive.py +++ b/html_parsing/get_price_game/from_direct2drive.py @@ -1,20 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'titan' -url = 'https://www.direct2drive.com/backend/api/productquery/findpage?search.keywords=' + text +import requests -import requests +text = "titan" +url = "https://www.direct2drive.com/backend/api/productquery/findpage?search.keywords={text}" + rs = requests.get(url) print(rs) data = rs.json() print(data) -for game in data['products']['items']: - offer = game['offerActions'][0] - print(game['title'], offer['purchasePrice']['amount'] + ' ' + offer['purchasePrice']['currency']['isoCode']) +for game in data["products"]["items"]: + offer = game["offerActions"][0] + print( + game["title"], + offer["purchasePrice"]["amount"] + + " " + + offer["purchasePrice"]["currency"]["isoCode"], + ) diff --git a/html_parsing/get_price_game/from_gama-gama.py b/html_parsing/get_price_game/from_gama-gama.py index eec7bfe0a..46337d083 100644 --- a/html_parsing/get_price_game/from_gama-gama.py +++ b/html_parsing/get_price_game/from_gama-gama.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re + +from bs4 import BeautifulSoup + +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() self._page.loadFinished.connect(self._load_finished_handler) @@ -35,10 +40,10 @@ def __init__(self, url): # Чтобы избежать падений скрипта self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self, _): + def _load_finished_handler(self, _) -> None: self._counter_finished += 1 if self._counter_finished == 2: @@ -47,35 +52,31 @@ def _load_finished_handler(self, _): return ExtractorHtml(url).html -text = 'mad' -url = 'http://gama-gama.ru/search/?searchField=' + text +text = "mad" +url = f"http://gama-gama.ru/search/?searchField={text}" html = get_html(url) -from bs4 import BeautifulSoup -root = BeautifulSoup(html, 'lxml') +root = BeautifulSoup(html, "lxml") -for game in root.select('.catalog-content > a'): - name = game['title'].strip() - name = name.replace('Купить ', '') +for game in root.select(".catalog-content > a"): + name = game["title"].strip() + name = name.replace("Купить ", "") price = None - price_holder = game.select_one('.catalog_price_holder') + price_holder = game.select_one(".catalog_price_holder") - price_1 = price_holder.select_one('.price_1') + price_1 = price_holder.select_one(".price_1") if price_1: price = price_1.text.strip() else: # Содержит описание цены со скидкой. Вытаскиваем цену со скидкой - price_2 = price_holder.select_one('.price_2') + price_2 = price_holder.select_one(".price_2") if price_2: - price = price_2.select_one('.price_group > .promo_price').text + price = price_2.select_one(".price_group > .promo_price").text # Удаление пустых символов пробелом - import re - price = re.sub(r'\s+', ' ', price) - - price = price.strip() + price = re.sub(r"\s+", " ", price).strip() print(name, price) diff --git a/html_parsing/get_price_game/from_gog.py b/html_parsing/get_price_game/from_gog.py index 4d5c9e323..08aa0bcb7 100644 --- a/html_parsing/get_price_game/from_gog.py +++ b/html_parsing/get_price_game/from_gog.py @@ -1,23 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'titan quest' -url = 'https://www.gog.com/games/ajax/filtered?language=en&mediaType=game&page=1&sort=bestselling' \ - '&system=windows_10,windows_7,windows_8,windows_vista,windows_xp&search=' + text - import requests -rs = requests.get(url) -print(rs) -data = rs.json() -print(data) -if not data['totalGamesFound']: - print('Not found game') - quit() +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" + + +def get_games(name: str) -> list[tuple[str, str]]: + url = ( + "https://www.gog.com/games/ajax/filtered?" + f"language=ru&mediaType=game&page=1&search={name}" + ) + + rs = session.get(url) + rs.raise_for_status() + + data = rs.json() + + return [ + (game["title"], game["price"]["baseAmount"]) + for game in data["products"] + ] + + +if __name__ == "__main__": + print(get_games("titan quest")) + # [('Titan Quest: Eternal Embers', '649'), ('Titan Quest Anniversary Edition', '649'), ('Titan Quest: Atlantis', '449'), ('Titan Quest: Ragnarök', '649')] + + print(get_games("Titan Quest: Atlantis")) + # [('Titan Quest: Atlantis', '449')] + + print(get_games("Prodeus")) + # [('Prodeus', '465')] + + print(get_games("Psychonauts 2")) + # [] -for game in data['products']: - print(game['title'], game['price']['amount'] + game['price']['symbol']) + print(get_games("dfsfsdfdsf")) + # [] diff --git a/html_parsing/get_price_game/from_gog_v2.py b/html_parsing/get_price_game/from_gog_v2.py new file mode 100644 index 000000000..1a007b5f4 --- /dev/null +++ b/html_parsing/get_price_game/from_gog_v2.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" + + +def get_games(name: str) -> list[tuple[str, str]]: + url = ( + f"https://catalog.gog.com/v1/catalog?" + f"countryCode=RU¤cyCode=RUB&limit=20" + f"&locale=ru-RU&order=desc:score" + f"&page=1&productType=in:game,pack,dlc,extras" + f"&query=like:{name}" + ) + + rs = session.get(url) + rs.raise_for_status() + + data = rs.json() + return [ + (game["title"], game["price"]["baseMoney"]["amount"]) + for game in data["products"] + if game["price"] and game["price"]["baseMoney"]["currency"] == "RUB" + ] + + +if __name__ == "__main__": + print(get_games("titan quest")) + # [('Titan Quest: Eternal Embers', '649.00'), ('Titan Quest: Atlantis', '449.00'), ('Titan Quest: Ragnarök', '649.00'), ('Titan Quest Anniversary Edition', '649.00')] + + print(get_games("Titan Quest: Atlantis")) + # [('Titan Quest: Atlantis', '449.00')] + + print(get_games("Prodeus")) + # [('Prodeus', '465.00'), ('Prodeus MIDI Soundtrack', '0.00'), ('Prodeus Soundtrack', '892.00')] + + print(get_games("Psychonauts 2")) + # [('Psychonauts 2', '1085.00'), ('Psychonauts', '249.00')] + + print(get_games("dfsfsdfdsf")) + # [] diff --git a/html_parsing/get_price_game/from_origin.py b/html_parsing/get_price_game/from_origin.py index cd5c1ce97..833aa63e2 100644 --- a/html_parsing/get_price_game/from_origin.py +++ b/html_parsing/get_price_game/from_origin.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from bs4 import BeautifulSoup + +from PyQt5.QtCore import QUrl, QTimer +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl, QTimer - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() @@ -40,35 +43,32 @@ def __init__(self, url): self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self): + def _load_finished_handler(self) -> None: self._page.toHtml(self._callable) return ExtractorHtml(url).html -text = 'titan' -url = 'https://www.origin.com/rus/ru-ru/search?searchString=' + text +text = "titan" +url = f"https://www.origin.com/rus/ru-ru/search?searchString={text}" html = get_html(url) +root = BeautifulSoup(html, "lxml") - -from bs4 import BeautifulSoup -root = BeautifulSoup(html, 'lxml') - -for game in root.select('.origin-search-section .origin-storegametile-details'): - name = game.select_one('.origin-storegametile-title').text.strip() +for game in root.select(".origin-search-section .origin-storegametile-details"): + name = game.select_one(".origin-storegametile-title").text.strip() price = None - storeprice = game.select_one('.origin-storeprice') + storeprice = game.select_one(".origin-storeprice") - otkprice = storeprice.select_one('.otkprice') + otkprice = storeprice.select_one(".otkprice") if otkprice: price = otkprice.text.strip() else: - free = storeprice.select_one('.otkprice-free') + free = storeprice.select_one(".otkprice-free") if free: price = 0 diff --git a/html_parsing/get_price_game/from_shop_buka_ru.py b/html_parsing/get_price_game/from_shop_buka_ru.py index 94bc32af1..fc6a4c853 100644 --- a/html_parsing/get_price_game/from_shop_buka_ru.py +++ b/html_parsing/get_price_game/from_shop_buka_ru.py @@ -1,23 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'titan' +import requests +from bs4 import BeautifulSoup -post_data = { - 'search': text, -} -import requests -rs = requests.post('http://shop.buka.ru/search', data=post_data) +def search(text: str) -> list[tuple[str, str]]: + rs = requests.post("https://shop.buka.ru/search", params=dict(q=text)) + rs.raise_for_status() -from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'lxml') + items = [] + root = BeautifulSoup(rs.content, "html.parser") + for game in root.select(".product-thumb"): + name = game["data-name"] + price = game["data-price"] + + items.append((name, price)) + + return items -for game in root.select('.product'): - name = game.select_one('.name').text.strip() - price = game.select_one('.costs .c2').text.strip() - print(name, price) +if __name__ == "__main__": + text = "titan" + for name, price in search(text): + print(name, price) diff --git a/html_parsing/get_price_game/from_steam.py b/html_parsing/get_price_game/from_steam.py index 6d94075a5..2a6dd97a5 100644 --- a/html_parsing/get_price_game/from_steam.py +++ b/html_parsing/get_price_game/from_steam.py @@ -1,40 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def search_game_price_list(name): +import re + +import requests +from bs4 import BeautifulSoup + + +def search_game_price_list(name: str) -> list: # category1 = 998 (Game) - url = 'http://store.steampowered.com/search/?category1=998&os=win&supportedlang=english&term=' + name + url = f"https://store.steampowered.com/search/?category1=998&os=win&supportedlang=english&term={name}" - game_price_list = list() + game_price_list = [] - import requests rs = requests.get(url) if not rs.ok: - print('Что-то пошло не так: {}\n{}'.format(rs.status_code, rs.text)) + print(f"Что-то пошло не так: {rs.status_code}\n{rs.text}") return game_price_list - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') + root = BeautifulSoup(rs.content, "html.parser") - for div in root.select('.search_result_row'): - name = div.select_one('.title').text.strip() + for div in root.select(".search_result_row"): + name = div.select_one(".title").text.strip() - # Ищем тег скидки - if div.select_one('.search_discount > span'): - price = div.select_one('.search_price > span > strike').text.strip() - else: - price = div.select_one('.search_price').text.strip() + # Ищем тег скидки, чтобы вытащить оригинальную цену, а не ту, что получилась со скидкой + price_el = div.select_one(".discount_original_price") or div.select_one(".discount_final_price") # Если цены нет (например, игра еще не продается) - if not price: + if not price_el: price = None else: + price = price_el.get_text(strip=True) + # Если в цене нет цифры считаем что это "Free To Play" или что-то подобное - import re - match = re.search(r'\d', price) + match = re.search(r"\d", price) if not match: price = 0 @@ -43,14 +45,14 @@ def search_game_price_list(name): return game_price_list -if __name__ == '__main__': - text = 'resident evil 6' +if __name__ == "__main__": + text = "resident evil 6" game_price_list = search_game_price_list(text) for name, price in game_price_list: - print(name, price, sep=' / ') + print(name, price, sep=" / ") print() - text = 'prey' + text = "prey" game_price_list = search_game_price_list(text) for name, price in game_price_list: - print(name, price, sep=' / ') + print(name, price, sep=" / ") diff --git a/html_parsing/get_product_with_category cenoteka.rs.py b/html_parsing/get_product_with_category cenoteka.rs.py index 5fa7074a0..6b27238dc 100644 --- a/html_parsing/get_product_with_category cenoteka.rs.py +++ b/html_parsing/get_product_with_category cenoteka.rs.py @@ -1,29 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/894302/ -from bs4 import BeautifulSoup import requests +from bs4 import BeautifulSoup -url = 'https://cenoteka.rs/proizvodi/mlecni-proizvodi/jogurt?page=3' +url = "https://cenoteka.rs/proizvodi/mlecni-proizvodi/jogurt?page=3" rs = requests.get(url) -root = BeautifulSoup(rs.content, 'lxml') +root = BeautifulSoup(rs.content, "lxml") -for category in root.select('#products > .row.section'): - category_title = category.select_one('.section-title').get_text(strip=True) +for category in root.select("#products > .row.section"): + category_title = category.select_one(".section-title").get_text(strip=True) print(category_title) - for product in category.select('[data-product-id]'): + for product in category.select("[data-product-id]"): try: - title = product.select_one('.article-name').get_text(strip=True) - price_list = [price.get_text(strip=True) for price in product.select('.article-price')] - print(' {}: {}'.format(title, price_list)) + title = product.select_one(".article-name").get_text(strip=True) + price_list = [ + price.get_text(strip=True) + for price in product.select(".article-price") + ] + print(f" {title}: {price_list}") except: pass diff --git a/html_parsing/get_set_sushi_list__sushivkusno.com.py b/html_parsing/get_set_sushi_list__sushivkusno.com.py deleted file mode 100644 index f4d3b0d3b..000000000 --- a/html_parsing/get_set_sushi_list__sushivkusno.com.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from bs4 import BeautifulSoup -import requests - - -rs = requests.get('http://sushivkusno.com/category/nabory-siety') -print(rs) - -root = BeautifulSoup(rs.content, 'lxml') -items = root.select('.CardContent') - -for i, product in enumerate(items, 1): - title = product.select_one('.CardText__title').text - weight = product.select_one('.CardText__subtitle > span > b').text - - price = product.select_one('.ProductParams__price').text - price = int(re.sub('\D', '', price)) - - print('{}. "{}": {}, {}'.format(i, title, weight, price)) diff --git a/html_parsing/get_stackoverflow_people_reached.py b/html_parsing/get_stackoverflow_people_reached.py index aec27a49f..25b2ac804 100644 --- a/html_parsing/get_stackoverflow_people_reached.py +++ b/html_parsing/get_stackoverflow_people_reached.py @@ -1,48 +1,58 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Optional import re -from bs4 import BeautifulSoup import requests +from bs4 import BeautifulSoup -def get_stackoverflow_people_reached(url: str) -> Optional[str]: +def get_stackoverflow_people_reached(url: str) -> str | None: rs = requests.get(url) rs.raise_for_status() - root = BeautifulSoup(rs.content, 'html.parser') - return root.find(text=re.compile('^~\d+\.?\d*')) + root = BeautifulSoup(rs.content, "html.parser") + profile_avatar_el = root.select_one("#main-content .s-card") + if not profile_avatar_el: + return + + text = profile_avatar_el.get_text(strip=True, separator="\n") + m = re.search(r"(\d+\.?\d*[km]?)\s*(затронуто|reached)", text) + if not m: + raise Exception("Reached not found!") + + return m.group(1) -if __name__ == '__main__': - url = 'https://ru.stackoverflow.com/users/201445/gil9red' +if __name__ == "__main__": + url = "https://ru.stackoverflow.com/users/201445/gil9red" print(get_stackoverflow_people_reached(url)) - # ~412k + # 1.5m - url = 'https://ru.stackoverflow.com' + url = "https://ru.stackoverflow.com" print(get_stackoverflow_people_reached(url)) # None print() urls = [ - 'https://ru.stackoverflow.com/users/213987/a-k', - 'https://ru.stackoverflow.com/users/17609/%d0%ae%d1%80%d0%b8%d0%b9%d0%a1%d0%9f%d0%b1', - 'https://ru.stackoverflow.com/users/1984/nofate', - 'https://stackoverflow.com/users/541136/aaron-hall', - 'https://stackoverflow.com/users/106224/boltclock', - 'https://stackoverflow.com/users/168175/flexo', + "https://ru.stackoverflow.com/users/213987/a-k", + "https://ru.stackoverflow.com/users/17609/%d0%ae%d1%80%d0%b8%d0%b9%d0%a1%d0%9f%d0%b1", + "https://ru.stackoverflow.com/users/1984/nofate", + "https://stackoverflow.com/users/541136/aaron-hall", + "https://stackoverflow.com/users/106224/boltclock", + "https://stackoverflow.com/users/168175/flexo", ] for url in urls: print(get_stackoverflow_people_reached(url)) - # ~215k - # ~750k - # ~1.4m - # ~112.5m - # ~40.5m - # ~4.7m + """ + 525k + 1.1m + 1.7m + 149.5m + 50.8m + 5.5m + """ diff --git a/html_parsing/get_video_list__xcadr.com.py b/html_parsing/get_video_list__xcadr.com.py index a829c687f..219ab0db6 100644 --- a/html_parsing/get_video_list__xcadr.com.py +++ b/html_parsing/get_video_list__xcadr.com.py @@ -1,71 +1,77 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def get_video_list(url): - import requests - rs = requests.get(url) +import re + +import requests +from bs4 import BeautifulSoup - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - import re - pattern_get_duration = re.compile('(\d+).:(\d+).') +def get_video_list(url) -> list[dict]: + rs = requests.get(url) + root = BeautifulSoup(rs.content, "lxml") + + pattern_get_duration = re.compile(r"(\d+).:(\d+).") items = [] - for item in root.select('#playlist_view_playlist_view_items > .item'): - url = item.a['href'] - title = item.a['title'] - url_thumb = item.select_one('img')['src'] + for item in root.select("#playlist_view_playlist_view_items > .item"): + url = item.a["href"] + title = item.a["title"] + url_thumb = item.select_one("img")["src"] # Example: "60%", "80%" - rating = item.select_one('.rating').text - rating = int(rating.replace('%', '')) + rating = item.select_one(".rating").text + rating = int(rating.replace("%", "")) # Example: "0м:31с", "6м:32с" - duration = item.select_one('.duration').text + duration = item.select_one(".duration").text minutes, seconds = map(int, pattern_get_duration.findall(duration)[0]) duration = minutes * 60 + seconds items.append({ - 'title': title, - 'url': url, - 'duration': duration, - 'rating': rating, - 'url_thumb': url_thumb + "title": title, + "url": url, + "duration": duration, + "rating": rating, + "url_thumb": url_thumb, }) return items -if __name__ == '__main__': - items = get_video_list('http://xcadr.com/collection/seks-v-poze-naezdnicy/') - print('Total:', len(items)) +if __name__ == "__main__": + items = get_video_list("http://xcadr.com/collection/seks-v-poze-naezdnicy/") + print("Total:", len(items)) - items = get_video_list('http://xcadr.com/collection/sceny-bdsm-v-filmah/') - print('Total:', len(items)) + items = get_video_list("http://xcadr.com/collection/sceny-bdsm-v-filmah/") + print("Total:", len(items)) print() - items = get_video_list('http://xcadr.com/collection/luchshie-sceny-v-bane/') - print('Total:', len(items)) + items = get_video_list("http://xcadr.com/collection/luchshie-sceny-v-bane/") + print("Total:", len(items)) - def print_items(items): + def print_items(items) -> None: for i, item in enumerate(items, 1): - print('{0:2}. "{title}" ({duration} secs, rating: {rating}): {url} [{url_thumb}]'.format(i, **item)) - - print('Sorted by duration (top 5):') - new_items = sorted(items, key=lambda x: x['duration'], reverse=True)[:5] + print( + '{0:2}. "{title}" ({duration} secs, rating: {rating}): {url} [{url_thumb}]'.format( + i, **item + ) + ) + + print("Sorted by duration (top 5):") + new_items = sorted(items, key=lambda x: x["duration"], reverse=True)[:5] print_items(new_items) print() - print('Sorted by duration:') - new_items = sorted(items, key=lambda x: x['duration'], reverse=True) + print("Sorted by duration:") + new_items = sorted(items, key=lambda x: x["duration"], reverse=True) print_items(new_items) print() - print('Sorted by rating:') - new_items = sorted(items, key=lambda x: x['rating'], reverse=True) + print("Sorted by rating:") + new_items = sorted(items, key=lambda x: x["rating"], reverse=True) print_items(new_items) diff --git a/html_parsing/gifer_com__ru__gifs__loading.py b/html_parsing/gifer_com__ru__gifs__loading.py new file mode 100644 index 000000000..9b5b7011d --- /dev/null +++ b/html_parsing/gifer_com__ru__gifs__loading.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install selenium +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By + + +def get_urls(driver) -> list[str]: + urls = [] + + for link in driver.find_elements( + By.CSS_SELECTOR, "figure.media-thumb.desktop link[itemprop=contentUrl]" + ): + url_gif = link.get_attribute("content") + gif_id = url_gif.split("/")[-1] + url_download = URL_DOWNLOAD_TEMPLATE + gif_id + urls.append(url_download) + + return urls + + +URL_DOWNLOAD_TEMPLATE = "https://i.gifer.com/embedded/download/" + + +options = Options() +options.add_argument("--headless") + +driver = webdriver.Firefox(options=options) +driver.get("https://gifer.com/ru/gifs/loading") + +print(f'Title: "{driver.title}"') + +driver.implicitly_wait(20) + +urls = get_urls(driver) +print(f"{len(urls)}: {urls}") +# 4: ['https://i.gifer.com/embedded/download/g0R5.gif', 'https://i.gifer.com/embedded/download/VAyR.gif', 'https://i.gifer.com/embedded/download/ZKZx.gif', 'https://i.gifer.com/embedded/download/ZZ5H.gif'] + +# Small scroll down +driver.execute_script(f"window.scrollTo(0, 200);") + +urls = get_urls(driver) +print(f"{len(urls)}: {urls}") +# 8: ['https://i.gifer.com/embedded/download/g0R5.gif', 'https://i.gifer.com/embedded/download/VAyR.gif', 'https://i.gifer.com/embedded/download/ZKZx.gif', 'https://i.gifer.com/embedded/download/ZZ5H.gif', 'https://i.gifer.com/embedded/download/g0R9.gif', 'https://i.gifer.com/embedded/download/ZWdx.gif', 'https://i.gifer.com/embedded/download/7pld.gif', 'https://i.gifer.com/embedded/download/AqCa.gif'] + +driver.quit() diff --git a/html_parsing/gitmanga_com__get_chapters.py b/html_parsing/gitmanga_com__get_chapters.py new file mode 100644 index 000000000..002f867dc --- /dev/null +++ b/html_parsing/gitmanga_com__get_chapters.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from urllib.parse import urljoin + +import requests +from bs4 import BeautifulSoup + + +session = requests.session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0" + + +@dataclass +class Chapter: + title: str + url: str + + +def get_chapters(url: str) -> list[Chapter]: + rs = session.get(url) + rs.raise_for_status() + + soup = BeautifulSoup(rs.content, "html.parser") + return [ + Chapter( + title=el.select_one(".item-title").get_text(strip=True), + url=urljoin(rs.url, el.a["href"]), + ) + for el in soup.select(".chapters-list__item") + ] + + +if __name__ == "__main__": + + def print_chapter(items: list[Chapter]) -> None: + print(f"Chapters ({len(items)}):") + print(f" {items[0]}") + print(" ...") + print(f" {items[-1]}") + + url = "https://gitmanga.com/764-berserk.html" + items = get_chapters(url) + print_chapter(items) + """ + Chapters (387): + Chapter(title='Том 1. Глава 0 - Berserk - The Prototype', url='https://gitmanga.com/read-764-berserk.html?t=1&g=0&p=1') + ... + Chapter(title='Том 42. Глава 371 - Угасающий свет в гнетущей тёмной ночи', url='https://gitmanga.com/read-764-berserk.html?t=42&g=371&p=1') + """ + + print() + + url = "https://gitmanga.com/605-igrok.html" + items = get_chapters(url) + print_chapter(items) + """ + Chapters (447): + Chapter(title='Том 1. Глава 1', url='https://gitmanga.com/read-605-igrok.html?t=1&g=1&p=1') + ... + Chapter(title='Том 6. Глава 15', url='https://gitmanga.com/read-605-igrok.html?t=6&g=15&p=1') + """ diff --git a/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py b/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py new file mode 100644 index 000000000..9a16c5c6f --- /dev/null +++ b/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import requests + + +session = requests.session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0" + + +URL_BRAND = "https://goldapple.ru/brands/elian-russia" + + +rs = session.get(URL_BRAND) +m = re.search(r'"productsApiUrl":\s*"(.+?)",', rs.text) +if not m: + raise Exception("Не получилось найти ссылку на API!") + +URL_API_PRODUCTS = m.group(1) + +print(f"Using API: {URL_API_PRODUCTS}") +# Using API: https://goldapple.ru/web_scripts/discover/category/products?cat=6577 + + +total = 0 +page = 1 +while True: + rs = session.get(URL_API_PRODUCTS, params={"page": page}) + rs.raise_for_status() + + products = rs.json().get("products") + if not products: + break + + total += len(products) + + page += 1 + +print(total) +# 27 diff --git a/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py b/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py new file mode 100644 index 000000000..5db8b2338 --- /dev/null +++ b/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install feedparser==6.0.8 +import feedparser + + +URL_USER_RSS = "https://grouple.co/user/rss/315828?filter=" +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" +) + + +def get_feeds_by_manga_chapters(url: str = URL_USER_RSS) -> list[str]: + feed = feedparser.parse(url, agent=USER_AGENT) + + feeds = [] + for entry in feed.entries: + title: str = entry.title + title = ( + title.replace(""", '"') + .replace("Манга", "") + .replace("Взрослая манга", "") + .strip() + ) + + feeds.append(title) + + return feeds + + +if __name__ == "__main__": + items = get_feeds_by_manga_chapters() + print(f"Items ({len(items)}):", items) diff --git a/html_parsing/grouple_co/common.py b/html_parsing/grouple_co/common.py new file mode 100644 index 000000000..4af15f367 --- /dev/null +++ b/html_parsing/grouple_co/common.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import json +import re +import time + +from dataclasses import dataclass, field +from urllib.parse import urljoin, urlsplit + +import requests +from bs4 import BeautifulSoup, Tag + +from config import LOGIN, PASSWORD + + +@dataclass +class Bookmark: + title: str + url: str + tags: list[str] = field(default_factory=list) + + def get_title_with_tags(self) -> str: + return self.title + (f" [{', '.join(self.tags)}]" if self.tags else "") + + +class AutoName(enum.Enum): + def _generate_next_value_(name, start, count, last_values): + return name + + +class Status(AutoName): + WATCHING = enum.auto() # В процессе + USER_DEFINED = enum.auto() # Пользовательская + ON_HOLD = enum.auto() # Пока бросил + PLANED = enum.auto() # В планах + COMPLETED = enum.auto() # Готово + FAVORITE = enum.auto() # Любимая + + +HOST: str = "https://grouple.co" +ADDITIONAL_HOSTS: list[str] = [ + "https://1.grouple.co", + "https://2.grouple.co", + "https://3.grouple.co", +] + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0" + + +def get_url(uri: str) -> str: + return urljoin(HOST, uri) + + +def do_get(uri: str, **kwargs) -> requests.Response: + rs = session.get(get_url(uri), **kwargs) + rs.raise_for_status() + + return rs + + +def do_post(uri: str, **kwargs) -> requests.Response: + rs = session.post(get_url(uri), **kwargs) + rs.raise_for_status() + + return rs + + +def do_auth() -> requests.Response: + form_data = { + "username": LOGIN, + "password": PASSWORD, + "remember_me": "true", + "_remember_me_yes": "", + "remember_me_yes": "on", + } + + rs = do_get("/internal/auth/login") + rs.raise_for_status() + + rs = do_post("/login/authenticate", data=form_data) + rs.raise_for_status() + + # Example: https://grouple.co/internal/auth/login?login_error=1 + if "error" in rs.url: + raise Exception("Invalid auth!") + + return rs + + +# NOTE: Была замечена смена домена +def update_host(): + global HOST + + def _update(url: str) -> bool: + global HOST + + if "grouple" in url and not url.startswith(HOST): + HOST = urlsplit(url)._replace(path="", query="", fragment="").geturl() + return True + + return False + + try: + rs = do_get("/") + except Exception as e: + # Перебор дополнительных хостов при ошибках + if HOST == ADDITIONAL_HOSTS[-1]: + raise e + + try: + idx = ADDITIONAL_HOSTS.index(HOST) + 1 + except ValueError: + idx = 0 + + HOST = ADDITIONAL_HOSTS[idx] + + update_host() + return + + if not _update(rs.url): + rs = do_get("/internal/auth") + _update(rs.url) + + rs = do_auth() + _update(rs.url) + + +def load(uri: str, **kwargs) -> requests.Response: + update_host() + + while True: + rs = do_get(uri, **kwargs) + time.sleep(1) + + # Если нужно авторизоваться + if "/login/auth" in rs.url: + do_auth() + continue + + return rs + + +def parse_bookmark(el: Tag) -> Bookmark: + tags = [] + if el.sup: + tags += [x.get_text(strip=True).lower() for x in el.sup.select("span[class]")] + + # Удаление сноски ("Выпуск завершен", "переведено" и т.п.), чтобы в title она не попала + el.sup.decompose() + + title = el.get_text(strip=True) + url = el["href"] + + return Bookmark(title=title, url=url, tags=tags) + + +def get_bookmarks_by_status(status: Status) -> list[Bookmark]: + rs = load("/private/bookmarks") + + m = re.search(r"var SITES = (\[.+]);", rs.text) + if not m: + raise Exception('Не удалось найти "var SITES = "!') + + # Example: var SITES = [{"id":1,"title":"ReadManga","url":"https://readmanga.io"}, ... + sites_str = m.group(1) + sites = json.loads(sites_str) + site_id_by_url = {x["id"]: x["url"] for x in sites} + + items: list[Bookmark] = [] + + limit = 50 + offset = 0 + + while True: + data = { + "bookmarkSort": "NAME", + "query": "", + "elementFilter": [], + "statusFilter": [status.value], + "limit": limit, + "offset": offset, + } + headers = { + "Authorization": f'Bearer {session.cookies["gwt"]}', + "Referer": rs.url, + } + rs = do_post("/api/bookmark/list", json=data, headers=headers) + + result = rs.json() + for item in result["list"]: + title = item["element"]["name"] + + site_id = item["element"]["elementId"]["siteId"] + element_url = item["element"]["elementUrl"] + url = site_id_by_url[site_id] + element_url + + # Example: + # tags_str = " СборникOnline" + # tags = ['Сборник', 'Online'] + tags_str = item["element"]["tagsString"] + tags = [ + el.get_text(strip=True).lower() + for el in BeautifulSoup(tags_str, "html.parser").select("span") + ] + + items.append(Bookmark(title=title, url=url, tags=tags)) + + if result["offset"] + result["limit"] >= result["total"]: + break + + offset += limit + time.sleep(2) + + return items + + +def get_plain_all_bookmarks_from_user(user_id: int) -> list[Bookmark]: + rs = load(f"/user/{user_id}/bookmarks") + root = BeautifulSoup(rs.content, "html.parser") + + return [parse_bookmark(row) for row in root.select("a.site-element")] + + +def get_all_bookmarks() -> dict[Status, list[Bookmark]]: + return {status: get_bookmarks_by_status(status) for status in Status} + + +if __name__ == "__main__": + assert Status.WATCHING.name == Status.WATCHING.value + assert Status.WATCHING.name == "WATCHING" + + print("HOST:", HOST) + # HOST: https://grouple.co + + print(session.cookies) + print(load("/")) + print(do_auth()) + print(session.cookies) + + print("HOST:", HOST) + # HOST: https://1.grouple.co + + print("\n" + "-" * 50 + "\n") + + print(get_plain_all_bookmarks_from_user(315828)) + + print("\n" + "-" * 50 + "\n") + + bookmarks = get_bookmarks_by_status(Status.WATCHING) + print(f"Bookmarks ({len(bookmarks)}):") + for i, bookmark in enumerate(bookmarks, 1): + print(f"{i}. {bookmark}") + """ + Bookmarks (13): + 1. Bookmark(title='Башня Бога', url='https://web.usagi.one/tower_of_god', tags=[]) + 2. Bookmark(title='Боруто. Наруто: Новое поколение', url='https://web.usagi.one/boruto__naruto_next_generations', tags=['завершён']) + 3. Bookmark(title='Ван Пис', url='https://web.usagi.one/van_pis', tags=[]) + 4. Bookmark(title='Ванпанчмен', url='https://1.seimanga.me/vanpanchmen', tags=[]) + 5. Bookmark(title='Ванпанчмен (ONE)', url='https://1.seimanga.me/vanpanchmen__one_', tags=[]) + 6. Bookmark(title='Выживание в игре за варвара', url='https://web.usagi.one/surviving_the_game_as_a_barbarian', tags=[]) + 7. Bookmark(title='Необъятный океан', url='https://1.seimanga.me/neobiatnyi_okean', tags=[]) + 8. Bookmark(title='Охотник × Охотник', url='https://web.usagi.one/hunter_x_hunter', tags=[]) + 9. Bookmark(title='Священная земля', url='https://web.usagi.one/holyland', tags=['завершён']) + 10. Bookmark(title='Семь смертных грехов', url='https://web.usagi.one/the_seven_deadly_sins', tags=['завершён', 'без глав']) + 11. Bookmark(title='Серафим конца', url='https://web.usagi.one/seraph_of_the_end', tags=[]) + 12. Bookmark(title='Терраформирование', url='https://1.seimanga.me/terraformirovanie', tags=[]) + 13. Bookmark(title='Чародейки', url='https://web.usagi.one/witch', tags=['завершён']) + """ diff --git a/html_parsing/grouple_co/config.py b/html_parsing/grouple_co/config.py new file mode 100644 index 000000000..3a49963e2 --- /dev/null +++ b/html_parsing/grouple_co/config.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +from pathlib import Path + + +DIR = Path(__file__).resolve().parent +TOKEN_FILE_NAME = DIR / "TOKEN.txt" + +TOKEN = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() +LOGIN, PASSWORD = TOKEN.splitlines() diff --git a/html_parsing/grouple_co/get_updates_from_api.py b/html_parsing/grouple_co/get_updates_from_api.py new file mode 100644 index 000000000..ce223c428 --- /dev/null +++ b/html_parsing/grouple_co/get_updates_from_api.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import session, do_post, load + + +def get_feeds_by_manga_chapters() -> list[str]: + # Auth and load + rs = load("/private/bookmarks/index#") + + data = { + "bookmarkSort": "NAME", + "elementFilter": [], + "statusFilter": ["ANOTHER_UPDATES"], + "includeUpdates": True, + "limit": 50, + "offset": 0, + } + + session.headers.update( + { + "Authorization": f'Bearer {session.cookies["gwt"]}', + "Referer": rs.url, + } + ) + + rs = do_post("/api/bookmark/activitiesList", json=data) + return [ + f'{item["element"]["name"]} {item["title"]}' + for item in rs.json()["list"] + if item["type"] == "CHAPTER_NEW" + ] + + +if __name__ == "__main__": + items = get_feeds_by_manga_chapters() + print(f"Items ({len(items)}):", items) diff --git a/html_parsing/grouple_co/print_all_bookmarks.py b/html_parsing/grouple_co/print_all_bookmarks.py new file mode 100644 index 000000000..5d8314c68 --- /dev/null +++ b/html_parsing/grouple_co/print_all_bookmarks.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import get_all_bookmarks + + +status_by_bookmarks = get_all_bookmarks() +print( + "Total bookmarks:", + sum(len(bookmarks) for bookmarks in status_by_bookmarks.values()), +) +# Total bookmarks: 143 + +print() + +for status, bookmarks in status_by_bookmarks.items(): + print(f"{status.value}. Bookmarks ({len(bookmarks)}):") + for i, bookmark in enumerate(bookmarks, 1): + print(f"{i}. {bookmark}") + + print() + +""" +WATCHING. Bookmarks (28): +1. Bookmark(title='Башня Бога', url='https://readmanga.io/bashnia_boga__A339d2', tags=[]) +... +28. Bookmark(title='Фейри Тейл. Начало', url='https://readmanga.io/feiri_teil__nachalo', tags=['переведено', 'без глав']) + +USER_DEFINED. Bookmarks (0): + +ON_HOLD. Bookmarks (4): +1. Bookmark(title='Д.Грэй-мен', url='https://readmanga.io/d_grei_men__A5327', tags=[]) +... +4. Bookmark(title='Четыре рыцаря', url='https://readmanga.io/chetyre_rycaria__A5327', tags=[]) + +PLANED. Bookmarks (56): +1. Bookmark(title='"Сверхъестественное" для чайников', url='https://readmanga.io/_sverhestestvennoe__dlia_chainikov', tags=['сингл']) +... +56. Bookmark(title='Энигма', url='https://readmanga.io/enigma__A5274', tags=['переведено']) + +COMPLETED. Bookmarks (55): +1. Bookmark(title='666 Сатана', url='https://readmanga.io/666_satana__A533b', tags=['переведено']) +... +55. Bookmark(title='Я — герой!', url='https://mintmanga.live/ia___geroi___A5327', tags=['переведено']) + +FAVORITE. Bookmarks (0): +""" diff --git a/html_parsing/grouple_co/print_all_bookmarks_from_user.py b/html_parsing/grouple_co/print_all_bookmarks_from_user.py new file mode 100644 index 000000000..f6e8368fc --- /dev/null +++ b/html_parsing/grouple_co/print_all_bookmarks_from_user.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import get_plain_all_bookmarks_from_user + + +bookmarks = get_plain_all_bookmarks_from_user(315828) +print(f"Bookmarks ({len(bookmarks)}):") +for i, bookmark in enumerate(bookmarks, 1): + print(f"{i}. {bookmark}") +""" +Bookmarks (85): +1. Bookmark(title='Башня Бога', url='https://readmanga.io/bashnia_boga__A339d2', tags=[]) +2. Bookmark(title='Берсерк', url='https://readmanga.io/berserk', tags=[]) +3. Bookmark(title='Боруто', url='https://readmanga.io/boruto__A5327', tags=[]) +... +83. Bookmark(title='Школа мертвецов', url='/internal/red/14905', tags=['переведено']) +84. Bookmark(title='Школьный эфир!', url='https://readmanga.io/love_live__dj___school_live', tags=['сингл']) +85. Bookmark(title='Энигма', url='https://readmanga.io/enigma__A5274', tags=['переведено']) +""" diff --git a/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py new file mode 100644 index 000000000..34d962d81 --- /dev/null +++ b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import Status, get_bookmarks_by_status + + +items = get_bookmarks_by_status(Status.WATCHING) + +print(f"Total bookmarks ({len(items)}):") +for x in items: + print(f" {x.title!r}: {x.url}") + +print("\n") + +completed = [x for x in items if "завершён" in x.tags] +print(f"Total bookmarks completed ({len(completed)}):") +for x in completed: + print(f" {x.title!r}: {x.url}") diff --git a/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py b/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py new file mode 100644 index 000000000..b0ccecf97 --- /dev/null +++ b/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import get_all_bookmarks + + +def get_all_pretty_title_of_bookmarks() -> list[str]: + items = [] + for bookmarks in get_all_bookmarks().values(): + items += [x.get_title_with_tags() for x in bookmarks] + + return items + + +if __name__ == "__main__": + all_bookmarks = get_all_pretty_title_of_bookmarks() + print("Total bookmarks:", len(all_bookmarks)) + # Total bookmarks: 143 + + print() + + for i, bookmark_title in enumerate(all_bookmarks, 1): + print(f"{i}. {bookmark_title}") + + """ + 1. Башня Бога + 2. Берсерк + 3. Боруто + 4. Ван Пис [обновлено] + ... + 139. Фейри Тейл [переведено, без глав] + 140. Шаман Кинг - зеро [выпуск завершен] + 141. Шухер! У нас новый студент! [переведено] + 142. Эльфийская песнь [переведено] + 143. Я — герой! [переведено] + """ diff --git a/html_parsing/hentailib_me.py b/html_parsing/hentailib_me.py new file mode 100644 index 000000000..1978dac33 --- /dev/null +++ b/html_parsing/hentailib_me.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import re + +import requests + + +def get_images(url: str) -> list[str]: + rs = requests.get(url) + + info = re.search("window.__info = (.+?);", rs.text) + if not info: + print("[#] Not found window.__info!") + return [] + + pages = re.search("window.__pg = (.+?);", rs.text) + if not pages: + print("[#] Not found window.__pg!") + return [] + + info = json.loads(info.group(1)) + pages = json.loads(pages.group(1)) + + url_chapter = info["img"]["url"] + url_base = info["servers"]["main"] + url_chapter + + return [url_base + p["u"] for p in pages] + + +if __name__ == "__main__": + url = "https://hentailib.me/koshkodevochki-eto-lozh/v1/c1?page=1" + items = get_images(url) + + print(f"Images ({len(items)}):") + for i, url in enumerate(items, 1): + print(f" {i}. {url}") + + # Images (22): + # 1. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/02_iips.png + # 2. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/03_67pz.png + # 3. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/04_v5SN.png + # 4. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/05_0dsy.png + # 5. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/06_guTg.png + # 6. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/07_cCtQ.png + # 7. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/08_EQUb.png + # 8. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/09_UfrK.png + # 9. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/10_bffa.png + # 10. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/11_7iDq.png + # 11. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/12_bwHN.png + # 12. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/13_ENhV.png + # 13. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/14_wisV.png + # 14. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/15_uVKc.png + # 15. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/16_DOEE.png + # 16. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/17_D3O1.png + # 17. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/18_Y8S2.png + # 18. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/19_g7SA.png + # 19. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/20_B9qA.png + # 20. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/21_IhKI.png + # 21. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/22_sUOK.png + # 22. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/23_PY78.png diff --git a/html_parsing/horoscope/horo.mail.ru_pisces_today.py b/html_parsing/horoscope/horo.mail.ru_pisces_today.py index a394f5564..76e4e6b05 100644 --- a/html_parsing/horoscope/horo.mail.ru_pisces_today.py +++ b/html_parsing/horoscope/horo.mail.ru_pisces_today.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('https://horo.mail.ru/prediction/pisces/today/') - from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'lxml') -text = root.select_one('.article__text').text.strip() + + +rs = requests.get("https://horo.mail.ru/prediction/pisces/today/") +root = BeautifulSoup(rs.content, "lxml") +text = root.select_one(".article__text").text.strip() print(repr(text)) print(text) diff --git a/html_parsing/howlongtobeat_com/common.py b/html_parsing/howlongtobeat_com/common.py new file mode 100644 index 000000000..8c244d9ef --- /dev/null +++ b/html_parsing/howlongtobeat_com/common.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass, field +from typing import Any + +import requests + +from seconds_to_str import seconds_to_str + + +URL_BASE = "https://howlongtobeat.com" + +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" + +session = requests.session() +session.headers["User-Agent"] = USER_AGENT + + +@dataclass +class Game: + id: int + title: str + aliases: list[str] + + duration_main_seconds: int # Main Story + duration_main_title: str = field(init=False) + + duration_plus_seconds: int # Main + Sides + duration_plus_title: str = field(init=False) + + duration_100_seconds: int # Completionist + duration_100_title: str = field(init=False) + + duration_all_seconds: int # All Styles + duration_all_title: str = field(init=False) + + release_world: int + profile_platforms: list[str] + + profile_genres: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + self.duration_main_title = seconds_to_str(self.duration_main_seconds) + self.duration_plus_title = seconds_to_str(self.duration_plus_seconds) + self.duration_100_title = seconds_to_str(self.duration_100_seconds) + self.duration_all_title = seconds_to_str(self.duration_all_seconds) + + @classmethod + def parse(cls, data: dict[str, Any]) -> "Game": + return cls( + id=data["game_id"], + title=data["game_name"], + aliases=data["game_alias"].split(", "), + duration_main_seconds=data["comp_main"], + duration_plus_seconds=data["comp_plus"], + duration_100_seconds=data["comp_100"], + duration_all_seconds=data["comp_all"], + release_world=data["release_world"], + + # В случаи поиска некоторые из полей недоступны в результатах - нужно на страницу игры идти + profile_platforms=data.get("profile_platform", "").split(", "), + profile_genres=data.get("profile_genre", "").split(", "), + ) diff --git a/html_parsing/howlongtobeat_com/get_game_info.py b/html_parsing/howlongtobeat_com/get_game_info.py new file mode 100644 index 000000000..a574df17b --- /dev/null +++ b/html_parsing/howlongtobeat_com/get_game_info.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json + +# pip install dpath +import dpath.util + +from bs4 import BeautifulSoup + +from common import session, URL_BASE, Game + + +def get_game_info(game_id: int) -> Game: + url = f"{URL_BASE}/game/{game_id}" + + rs = session.get(url) + rs.raise_for_status() + + soup = BeautifulSoup(rs.content, "html.parser") + + next_data_el = soup.select_one("#__NEXT_DATA__") + if not next_data_el: + raise Exception('Not found id="__NEXT_DATA__"!') + + next_data_str = next_data_el.string + + next_data = json.loads(next_data_str) + return Game.parse( + data=dpath.util.get(next_data, "**/data/game/0") + ) + + +if __name__ == "__main__": + print( + get_game_info(game_id=3505) + ) + # Game(id=3505, title='Final Fantasy IX', aliases=['Final Fantasy 9', 'FF9'], duration_main_seconds=138721, duration_main_title='38:32:01', duration_plus_seconds=189924, duration_plus_title='52:45:24', duration_100_seconds=299502, duration_100_title='83:11:42', duration_all_seconds=171503, duration_all_title='47:38:23', release_world='2000-02-16', profile_platforms=['Mobile', 'Nintendo Switch', 'PC', 'PlayStation', 'PlayStation 4', 'Xbox One'], profile_genres=['Role-Playing']) + + print( + get_game_info(game_id=3519) + ) + # Game(id=3519, title='Final Fantasy VI', aliases=['Final Fantasy III [NA]', 'Final Fantasy 3 [NA]', 'Final Fantasy 6', 'FF6', 'Final Fantasy VI Advance', 'Final Fantasy VI: Pixel Remaster'], duration_main_seconds=124906, duration_main_title='34:41:46', duration_plus_seconds=147288, duration_plus_title='40:54:48', duration_100_seconds=220653, duration_100_title='61:17:33', duration_all_seconds=148583, duration_all_title='41:16:23', release_world='1994-04-02', profile_platforms=['Game Boy Advance', 'Mobile', 'Nintendo Switch', 'PC', 'PlayStation', 'PlayStation 4', 'Super Nintendo'], profile_genres=['Turn-Based', 'Role-Playing']) diff --git a/html_parsing/howlongtobeat_com/search.py b/html_parsing/howlongtobeat_com/search.py new file mode 100644 index 000000000..8b4abb967 --- /dev/null +++ b/html_parsing/howlongtobeat_com/search.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# import re # TODO: Удалить + +from datetime import datetime + +# from urllib.parse import urljoin # TODO: Удалить +from typing import Any + +from common import URL_BASE, session, Game + + +def api_search(text: str, page: int = 1) -> dict[str, Any]: + url_first = f"{URL_BASE}/?q={text}" + + rs = session.get(url_first) + rs.raise_for_status() + + headers = {"Referer": url_first} + + # TODO: Перестала быть рабочей. Работа API теперь в файлах вида "/_next/static/chunks/\w+.js" + # # NOTE: Получение url из js. Старая защита. Возможно, будет комбинироваться с новой + # m = re.search(r' + + + + + +
    +

    {{ title }}

    + +
    +
    + + + + {% for header in headers %} + + {% endfor %} + + + +
    {{ header }}
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" index 41a8a76ba..684a9e88d 100644 --- "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" +++ "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" @@ -1,190 +1,58 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt -from flask import Flask, render_template_string +from flask import Flask, render_template from common import get_crash_statistics_list_db app = Flask(__name__) -@app.route('/') +@app.route("/") def index(): headers = ["Дата", "ДТП", "Погибли", "Погибло детей", "Ранены", "Ранено детей"] rows = get_crash_statistics_list_db() rows.reverse() - data = [] + data: list[dict] = [] + year_by_number: dict[int, int] = dict() + for date_str, dtp, died, children_died, wounded, wounded_children in rows: + date = dt.datetime.strptime(date_str, "%d.%m.%Y").date() + dtp = int(dtp) - for date, dtp, died, children_died, wounded, wounded_children in rows: data.append({ - 'date': date, - 'date_iso': DT.datetime.strptime(date, '%d.%m.%Y').date().isoformat(), - 'dtp': int(dtp), - 'died': int(died), - 'children_died': int(children_died), - 'wounded': int(wounded), - 'wounded_children': int(wounded_children), + "date": date_str, + "date_iso": date.isoformat(), + "dtp": dtp, + "died": int(died), + "children_died": int(children_died), + "wounded": int(wounded), + "wounded_children": int(wounded_children), }) - return render_template_string(""" - - - - - {{ title }} - - - - - - - - -
    -

    {{ title }}

    - -
    -
    - - - - {% for header in headers %} - - {% endfor %} - - - -
    {{ header }}
    -
    -
    - -
    -
    -
    - - - - """, title="АВАРИЙНОСТЬ НА ДОРОГАХ РОССИИ", headers=headers, data=data -) + if date.year not in year_by_number: + year_by_number[date.year] = 0 + year_by_number[date.year] += dtp + + return render_template( + "index.html", + title="АВАРИЙНОСТЬ НА ДОРОГАХ РОССИИ", + headers=headers, + data=data, + year_by_number=year_by_number, + ) if __name__ == "__main__": app.debug = True # Localhost - app.run( - # Включение поддержки множества подключений - threaded=True, - port=10009, - ) + app.run(port=10009) # # Public IP # app.run(host='0.0.0.0') diff --git "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" index 0de7fdb54..f3278d76b 100644 --- "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" +++ "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + from PyQt5.QtCore import QUrl, QEventLoop from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox @@ -11,17 +14,15 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) - quit() + QMessageBox.critical(None, "Error", text) + sys.exit() -import sys sys.excepthook = log_uncaught_exceptions @@ -63,16 +64,13 @@ def log_uncaught_exceptions(ex_cls, ex, tb): """ - def run_js_code(page: QWebEnginePage, code: str) -> object: loop = QEventLoop() - result_value = { - 'value': None - } + result_value = {"value": None} - def _on_callback(result: object): - result_value['value'] = result + def _on_callback(result: object) -> None: + result_value["value"] = result loop.quit() @@ -80,15 +78,24 @@ def _on_callback(result: object): loop.exec() - return result_value['value'] + return result_value["value"] class MyWebEnginePage(QWebEnginePage): - def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: str, line_number: int, source_id: str): - print(f'javascript_console_message: {level}, {message}, {line_number}, {source_id}', file=sys.stderr) - - -with open('js/jquery-3.1.1.min.js') as f: + def javaScriptConsoleMessage( + self, + level: "JavaScriptConsoleMessage", + message: str, + line_number: int, + source_id: str, + ) -> None: + print( + f"javascript_console_message: {level}, {message}, {line_number}, {source_id}", + file=sys.stderr, + ) + + +with open("js/jquery-3.1.1.min.js") as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -102,7 +109,7 @@ def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: s # mw.setWindowTitle(str(url)) -url = 'https://гибдд.рф/request_main' +url = "https://гибдд.рф/request_main" page = MyWebEnginePage() page.load(QUrl(url)) @@ -113,17 +120,17 @@ def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: s # page.urlChanged.connect(_on_url_changed) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: print(page.url().toString()) page.runJavaScript(jquery_text) result = run_js_code(page, "document.title") - print('run_java_script:', result) + print("run_java_script:", result) # TODO: обернуть в функцию has с css-selector result = run_js_code(page, "qt.jQuery('#surname_check').length > 0") - print('run_java_script:', result) + print("run_java_script:", result) # Клик на флажок "С информацией ознакомлен" run_js_code(page, """qt.jQuery('input[name="agree"]').click();""") @@ -137,7 +144,10 @@ def _on_load_finished(ok: bool): run_js_code(page, """qt.jQuery('#email_check').val('FOOBAR@EMAIL.COM');""") - run_js_code(page, """qt.jQuery('#message_check > textarea').val({});""".format(repr(TEXT_PATTERN))) + run_js_code( + page, + f"qt.jQuery('#message_check > textarea').val({TEXT_PATTERN!r});", + ) # code = """ # console.log('testetst'); @@ -158,7 +168,9 @@ def _on_load_finished(ok: bool): print() -view.loadProgress.connect(lambda value: mw.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: mw.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/human__timestamp_to_date_string_format.py b/human__timestamp_to_date_string_format.py index da724d428..d0738ca77 100644 --- a/human__timestamp_to_date_string_format.py +++ b/human__timestamp_to_date_string_format.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/843386/ @@ -9,20 +9,20 @@ def timestamp_to_human(time_s): items = [ - (31536000, '{} г. '), - (2592000, '{} мес. '), - (604800, '{} нед. '), - (86400, '{} д. '), - (3600, '{} ч.'), + (31536000, "{} г. "), + (2592000, "{} мес. "), + (604800, "{} нед. "), + (86400, "{} д. "), + (3600, "{} ч."), ] if time_s == 0: return None if time_s < 3600: - return 'меньше часа' + return "меньше часа" - result = '' + result = "" for value, fmt in items: if time_s >= value: @@ -32,9 +32,9 @@ def timestamp_to_human(time_s): return result -if __name__ == '__main__': +if __name__ == "__main__": print(timestamp_to_human(2827567.5759670734)) # 1 мес. 2 д. 17 ч. assert timestamp_to_human(2827567.5759670734) == "1 мес. 2 д. 17 ч." - print(timestamp_to_human(269649.6857390404)) # 3 д. 2 ч. + print(timestamp_to_human(269649.6857390404)) # 3 д. 2 ч. assert timestamp_to_human(269649.6857390404) == "3 д. 2 ч." diff --git a/human_byte_size.py b/human_byte_size.py index 42658853f..964f17b9f 100644 --- a/human_byte_size.py +++ b/human_byte_size.py @@ -1,25 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: +def sizeof_fmt(num: int | float) -> str: + for x in ["bytes", "KB", "MB", "GB"]: if num < 1024.0: - return "%3.1f %s" % (num, x) + return "%.1f %s" % (num, x) num /= 1024.0 - return "%3.1f %s" % (num, 'TB') + return "%.1f %s" % (num, "TB") -if __name__ == '__main__': +if __name__ == "__main__": print(sizeof_fmt(25000000000)) print() import shutil - usage = shutil.disk_usage('C://') - print('total: {:>8} ({} bytes)'.format(sizeof_fmt(usage.total), usage.total)) - print('used: {:>8} ({} bytes)'.format(sizeof_fmt(usage.used), usage.used)) - print('free: {:>8} ({} bytes)'.format(sizeof_fmt(usage.free), usage.free)) + + usage = shutil.disk_usage("C://") + print(f"total: {sizeof_fmt(usage.total):>8} ({usage.total} bytes)") + print(f"used: {sizeof_fmt(usage.used):>8} ({usage.used} bytes)") + print(f"free: {sizeof_fmt(usage.free):>8} ({usage.free} bytes)") diff --git a/human_byte_size__using_humanize.py b/human_byte_size__using_humanize.py index 56fad2941..7ab4add8b 100644 --- a/human_byte_size__using_humanize.py +++ b/human_byte_size__using_humanize.py @@ -1,19 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import shutil + # pip install humanize import humanize -print(humanize.naturalsize(25000000000, binary=True)) # '23.3 GiB' -print(humanize.naturalsize(25000000000, binary=True).replace('i', '')) # '23.3 GB', Strange, misleading +print(humanize.naturalsize(25000000000, binary=True)) # '23.3 GiB' +print( + humanize.naturalsize(25000000000, binary=True).replace("i", "") +) # '23.3 GB', Strange, misleading print() -import shutil -usage = shutil.disk_usage('C://') -print('total: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.total, binary=True), usage.total)) -print('used: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.used, binary=True), usage.used)) -print('free: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.free, binary=True), usage.free)) +usage = shutil.disk_usage("C://") +print( + f"total: {humanize.naturalsize(usage.total, binary=True):>8} ({usage.total} bytes)" +) +print( + f"used: {humanize.naturalsize(usage.used, binary=True):>8} ({usage.used} bytes)" +) +print( + f"free: {humanize.naturalsize(usage.free, binary=True):>8} ({usage.free} bytes)" +) diff --git a/human_format_number__1000_separator.py b/human_format_number__1000_separator.py index 9805b833d..c7e8f96b5 100644 --- a/human_format_number__1000_separator.py +++ b/human_format_number__1000_separator.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -print('{:,d}'.format(5213234)) # 5,213,234 -print('{:,d}'.format(123)) # 123 -print('{:,d}'.format(1004)) # 1,004 -print('{:,d}'.format(4321)) # 4,321 +print(f"{5213234:,d}") # 5,213,234 +print(f"{123:,d}") # 123 +print(f"{1004:,d}") # 1,004 +print(f"{4321:,d}") # 4,321 diff --git a/humanize__examples/date_and_time__humanization.py b/humanize__examples/date_and_time__humanization.py index 6f1c97951..6710dd1ec 100644 --- a/humanize__examples/date_and_time__humanization.py +++ b/humanize__examples/date_and_time__humanization.py @@ -1,34 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize +import datetime as dt + # pip install humanize import humanize -# Date & time humanization: -import datetime as DT -print(humanize.naturalday(DT.datetime.now())) # 'today' -print(humanize.naturalday(DT.datetime.now() - DT.timedelta(days=1))) # 'yesterday' -print(humanize.naturalday(DT.date(2007, 6, 5))) # 'Jun 05' +# Date & time humanization +print(humanize.naturalday(dt.datetime.now())) # 'today' +print(humanize.naturalday(dt.datetime.now() - dt.timedelta(days=1))) # 'yesterday' +print(humanize.naturalday(dt.date(2007, 6, 5))) # 'Jun 05' print() -print(DT.timedelta(seconds=1001)) # '0:16:41' -print(humanize.naturaldelta(DT.timedelta(seconds=1001))) # '16 minutes' -print(humanize.naturaldelta(DT.timedelta(seconds=5))) # '5 seconds' -print(humanize.naturaldelta(DT.timedelta(hours=30))) # 'a day' -print(humanize.naturaldelta(DT.timedelta(hours=60))) # '2 days' +print(dt.timedelta(seconds=1001)) # '0:16:41' +print(humanize.naturaldelta(dt.timedelta(seconds=1001))) # '16 minutes' +print(humanize.naturaldelta(dt.timedelta(seconds=5))) # '5 seconds' +print(humanize.naturaldelta(dt.timedelta(hours=30))) # 'a day' +print(humanize.naturaldelta(dt.timedelta(hours=60))) # '2 days' print() -print(humanize.naturaldate(DT.date(2007, 6, 5))) # 'Jun 05 2007' -print(humanize.naturaldate(DT.date(2007, 6, 5))) # 'Jun 05 2007' +print(humanize.naturaldate(dt.date(2007, 6, 5))) # 'Jun 05 2007' +print(humanize.naturaldate(dt.date(2007, 6, 5))) # 'Jun 05 2007' print() -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(seconds=1))) # 'a second ago' -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(seconds=3600))) # 'an hour ago' -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(hours=30))) # 'a day ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=1))) +# 'a second ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=3600))) +# 'an hour ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(hours=30))) +# 'a day ago' diff --git a/humanize__examples/file_size__humanization.py b/humanize__examples/file_size__humanization.py index c9f20d515..467a1ac63 100644 --- a/humanize__examples/file_size__humanization.py +++ b/humanize__examples/file_size__humanization.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize @@ -12,7 +12,7 @@ # File size humanization: -print(humanize.naturalsize(100)) # '100 Bytes' -print(humanize.naturalsize(10 ** 9)) # '1.0 GB' -print(humanize.naturalsize(10 ** 9, binary=True)) # '953.7 MiB' -print(humanize.naturalsize(10 ** 9, gnu=True)) # '953.7M' +print(humanize.naturalsize(100)) # '100 Bytes' +print(humanize.naturalsize(10**9)) # '1.0 GB' +print(humanize.naturalsize(10**9, binary=True)) # '953.7 MiB' +print(humanize.naturalsize(10**9, gnu=True)) # '953.7M' diff --git a/humanize__examples/integer__humanization.py b/humanize__examples/integer__humanization.py index eab43bbe4..74741cbc8 100644 --- a/humanize__examples/integer__humanization.py +++ b/humanize__examples/integer__humanization.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize @@ -12,15 +12,15 @@ # Integer humanization: -print(humanize.intcomma(12345)) # '12,345' +print(humanize.intcomma(12345)) # '12,345' print(humanize.intcomma(123456789)) # '123,456,789' print() -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' print() -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' print(humanize.apnumber(41)) # '41' diff --git a/humanize__examples/localization__change_locale_in_runtime.py b/humanize__examples/localization__change_locale_in_runtime.py index 210973124..4069cc376 100644 --- a/humanize__examples/localization__change_locale_in_runtime.py +++ b/humanize__examples/localization__change_locale_in_runtime.py @@ -1,41 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize +import datetime as dt + # pip install humanize import humanize -import datetime as DT # Localization. How to change locale in runtime -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 seconds ago' -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 seconds ago' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' print() -_t = humanize.i18n.activate('ru_RU') +_t = humanize.i18n.activate("ru_RU") -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 секунды назад' -print(humanize.intword(123455913)) # '123.5 миллиона' -print(humanize.intword(12345591313)) # '12.3 миллиарда' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 секунды назад' +print(humanize.intword(123455913)) # '123.5 миллиона' +print(humanize.intword(12345591313)) # '12.3 миллиарда' print(humanize.intword(1339014900000)) # '1.3 триллиона' -print(humanize.apnumber(4)) # 'четыре' -print(humanize.apnumber(7)) # 'семь' +print(humanize.apnumber(4)) # 'четыре' +print(humanize.apnumber(7)) # 'семь' print() humanize.i18n.deactivate() -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 seconds ago' -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 seconds ago' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' diff --git a/i_watching_u.py b/i_watching_u.py index e6cd4d033..09b45012e 100644 --- a/i_watching_u.py +++ b/i_watching_u.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Программа делает скриншоты, сохраняет их в zip-архив.""" -if __name__ == '__main__': - from PIL import ImageGrab - from datetime import datetime +if __name__ == "__main__": import time - from zipfile import ZipFile + + from datetime import datetime from io import BytesIO + from zipfile import ZipFile + + from PIL import ImageGrab # Сбор статистики о действиях пользователя @@ -21,32 +23,34 @@ today = datetime.today() # Получаем строку текущего времени - str_today_time = today.strftime('%H.%M.%S') + str_today_time = today.strftime("%H.%M.%S") # Имя zip-архива - zip_file_name = 'screenshots_' + today.strftime('%d.%m.%Y') + '.zip' + zip_file_name = "screenshots_" + today.strftime("%d.%m.%Y") + ".zip" # Составляем имя файла, содержащее в названии текущее время - file_name = 'screenshot_{}.png'.format(str_today_time) + file_name = f"screenshot_{str_today_time}.png" # Делаем скриншот im = ImageGrab.grab() - print('Сделан скриншот "{}"'.format(file_name)) + print(f'Сделан скриншот "{file_name}"') # Открываем zip-файл - with ZipFile(zip_file_name, 'a') as zip_file: + with ZipFile(zip_file_name, "a") as zip_file: # Сохраняем изображение в буфер io = BytesIO() - im.save(io, 'png') - + im.save(io, "png") + # Байты изображения image_bytes = io.getvalue() # Добавляем изображение в архив zip_file.writestr(file_name, image_bytes) - print(' Скриншот "{}" добавлен в архив "{}"\n'.format(file_name, zip_file_name)) + print( + f' Скриншот "{file_name}" добавлен в архив "{zip_file_name}"\n' + ) # Ожидание каждые 15 минут time.sleep(15 * 60) diff --git a/imagehash__examples/find_similar_images__phash.py b/imagehash__examples/find_similar_images__phash.py index 5c3f27bd1..139d3d426 100644 --- a/imagehash__examples/find_similar_images__phash.py +++ b/imagehash__examples/find_similar_images__phash.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash -from glob import glob import itertools +from glob import glob # pip install imagehash import imagehash @@ -17,8 +17,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -31,7 +31,7 @@ img_by_hash[file_name] = hash_img -print('Find similar images') +print("Find similar images") for img_1, img_2 in itertools.combinations(img_by_hash.items(), 2): file_name_1, hash_img_1 = img_1 diff --git a/imagehash__examples/find_similar_images_with_group_and_sort__phash.py b/imagehash__examples/find_similar_images_with_group_and_sort__phash.py index 69780bde2..25b5f54e4 100644 --- a/imagehash__examples/find_similar_images_with_group_and_sort__phash.py +++ b/imagehash__examples/find_similar_images_with_group_and_sort__phash.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash +import itertools + from collections import defaultdict from glob import glob -import itertools # pip install imagehash import imagehash @@ -18,8 +19,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -32,7 +33,7 @@ img_by_hash[file_name] = hash_img -print('\nFind similar images') +print("\nFind similar images") file_name_by_similars = defaultdict(list) diff --git a/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py b/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py index bfec48b2a..9bdbedc92 100644 --- a/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py +++ b/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash +import itertools + from collections import defaultdict from glob import glob -import itertools # pip install imagehash import imagehash @@ -18,8 +19,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -32,7 +33,7 @@ img_by_hash[file_name] = hash_img -print('\nFind similar images') +print("\nFind similar images") file_name_by_similars = defaultdict(list) diff --git a/img_to_base64_html/main.py b/img_to_base64_html/main.py index 91e50adad..e123b21d7 100644 --- a/img_to_base64_html/main.py +++ b/img_to_base64_html/main.py @@ -1,39 +1,40 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def img_to_base64_html(file_name__or__bytes__or__file_object): +import base64 +import io + +# pip install pillow +from PIL import Image + + +def img_to_base64_html(file_name__or__bytes__or__file_object: str | bytes | io.IOBase) -> str: arg = file_name__or__bytes__or__file_object - if type(arg) == str: - with open(arg, mode='rb') as f: + if isinstance(arg, str): + with open(arg, mode="rb") as f: img_bytes = f.read() - elif type(arg) == bytes: + elif isinstance(arg, bytes): img_bytes = arg else: img_bytes = arg.read() - import io bytes_io = io.BytesIO(img_bytes) - - from PIL import Image img = Image.open(bytes_io) - import base64 - img_base64 = base64.b64encode(img_bytes).decode('utf-8') - # print(img_base64) - - return 'data:image/{};base64,'.format(img.format.lower()) + img_base64 + img_base64 = base64.b64encode(img_bytes).decode("utf-8") + return f"data:image/{img.format.lower()};base64,{img_base64}" -if __name__ == '__main__': - file_name = 'img.jpg' +if __name__ == "__main__": + file_name = "img.jpg" img_base64 = img_to_base64_html(file_name) - print('[len {}]: {}...'.format(len(img_base64), img_base64[:50])) + print(f"[len {len(img_base64)}]: {img_base64[:50]}...") - with open(file_name + '_base64.txt', mode='w', encoding='utf-8') as f: + with open(file_name + "_base64.txt", mode="w", encoding="utf-8") as f: f.write(img_base64) diff --git a/incremental timeouts.py b/incremental timeouts.py index c7428222f..319f02832 100644 --- a/incremental timeouts.py +++ b/incremental timeouts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,12 +10,15 @@ """ +import random +import time + + def work(): - import random if random.randint(0, 5): raise Exception() - print('Work!') + print("Work!") timeout = 0.5 @@ -29,9 +32,7 @@ def work(): break except: - print('Неудача с таймаутом {}.'.format(timeout)) + print(f"Неудача с таймаутом {timeout}.") - import time time.sleep(timeout) timeout += 0.1 - diff --git a/infinity_iterator.py b/infinity_iterator.py new file mode 100644 index 000000000..5d3d98e7b --- /dev/null +++ b/infinity_iterator.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +def inf_it() -> iter: + return iter(lambda: 0, 1) + + +if __name__ == "__main__": + from tqdm import tqdm + + for _ in tqdm(inf_it()): + pass diff --git a/info_from_bitkurs.ru.py b/info_from_bitkurs.ru.py index f1c587f6b..6803a9b0e 100644 --- a/info_from_bitkurs.ru.py +++ b/info_from_bitkurs.ru.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re import grab -URL = 'http://bitkurs.ru/' +URL = "http://bitkurs.ru/" # Copy page yandex # URL = 'http://hghltd.yandex.net/yandbtm?fmode=inject&url=http%3A%2F%2Fbitkurs.ru%2F&tld=ru&lang=ru&la=1453455360&tm=1454924280&text=bitkurs.ru&l10n=ru&mime=html&sign=2105e628b413520a0e087ae4ac0db180&keyno=0' g = grab.Grab() -g.setup(proxy='proxy.compassplus.ru:3128', proxy_type='http') +g.setup(proxy="proxy.compassplus.ru:3128", proxy_type="http") g.go(URL) #
    @@ -30,14 +30,15 @@ def get_amt(text): # Удаление всех символов, кроме цифр и точки - text = re.sub('[^\\d\.]+', '', text) + text = re.sub(r"[^\d.]+", "", text) # Вытаскивание строки, описывающей число - text = re.search('\\d+(\.\\d*)?', text).group() + text = re.search(r"\d+(\.\d*)?", text).group() return text + usd = get_amt(usd) rub = get_amt(rub) eur = get_amt(eur) -print('1 BTC:\n {} USD\n {} RUB\n {} EUR'.format(usd, rub, eur)) +print(f"1 BTC:\n {usd} USD\n {rub} RUB\n {eur} EUR") diff --git a/ini with configparser.py b/ini with configparser.py deleted file mode 100644 index 537d0a1c0..000000000 --- a/ini with configparser.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import configparser -ini = configparser.ConfigParser() -ini['Default'] = { - 'x': 10, - 'y': 15, - 'z': 3, -} - -ini['Additional'] = {} -additional = ini['Additional'] -additional['top'] = str(True) -additional['text'] = "Hello World!" -additional['arrays'] = str([1, 2, 3, 4, 5]) - -ini['Empty'] = {} - -with open('config.ini', 'w') as f: - ini.write(f) - - -ini_read = configparser.ConfigParser() -ini_read.read('config.ini') -print(ini_read.sections()) - -print(ini_read['Additional']['top']) -print(ini_read['Additional']['text']) -print(ini_read['Additional']['arrays']) -print(ini_read['Additional']['arrays'].replace('[', '').replace(']', '').split(', ')) diff --git a/ini__configparser__examples/hello_world.py b/ini__configparser__examples/hello_world.py new file mode 100644 index 000000000..ba4269454 --- /dev/null +++ b/ini__configparser__examples/hello_world.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import configparser + + +ini = configparser.ConfigParser() +ini["Default"] = { + "x": 10, + "y": 15, + "z": 3, +} + +ini["Additional"] = {} +additional = ini["Additional"] +additional["top"] = str(True) +additional["text"] = "Hello World!" +additional["arrays"] = str([1, 2, 3, 4, 5]) + +ini["Empty"] = {} + +with open("config.ini", "w") as f: + ini.write(f) + + +ini_read = configparser.ConfigParser() +ini_read.read("config.ini") +print(ini_read.sections()) + +print(ini_read["Additional"]["top"]) +print(ini_read["Additional"]["text"]) +print(ini_read["Additional"]["arrays"]) +print(ini_read["Additional"]["arrays"].replace("[", "").replace("]", "").split(", ")) diff --git a/ini__configparser__examples/read__list_of_dicts.py b/ini__configparser__examples/read__list_of_dicts.py new file mode 100644 index 000000000..c0d427268 --- /dev/null +++ b/ini__configparser__examples/read__list_of_dicts.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://ru.stackoverflow.com/q/1270426/201445 + + +import configparser + + +config = configparser.ConfigParser() +config.read("example.ini") + +print(dict(config["DEFAULT"])) + +for section in config.sections(): + print(dict(config[section])) diff --git a/ini__configparser__examples/write__list_of_dicts.py b/ini__configparser__examples/write__list_of_dicts.py new file mode 100644 index 000000000..2485467fb --- /dev/null +++ b/ini__configparser__examples/write__list_of_dicts.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://ru.stackoverflow.com/q/1270426/201445 + + +import configparser + + +dict1 = [ + { + "object": "label", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "button", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "color", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "line", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "unknown", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, +] + +config = configparser.ConfigParser() + +for d in dict1: + config[d["object"]] = d + +with open("example.ini", "w") as f: + config.write(f) diff --git a/input_email_list.py b/input_email_list.py index 541fed476..5f365969a 100644 --- a/input_email_list.py +++ b/input_email_list.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://stackoverflow.com/questions/35332611/a-program-which-creates-emails -def fill_list(l, promt): + +def fill_list(l, promt) -> None: while True: x = input(promt) if not x: @@ -15,15 +16,15 @@ def fill_list(l, promt): l.append(x) -if __name__ == '__main__': +if __name__ == "__main__": first_names = list() last_names = list() fill_list(first_names, "Input first name: ") - print('first_names: {}'.format(first_names)) + print(f"first_names: {first_names}") fill_list(last_names, "Input last name: ") - print('last_names: {}'.format(last_names)) + print(f"last_names: {last_names}") for first, last in zip(first_names, last_names): - print('{}.{}@abc.mail.com'.format(first, last)) + print(f"{first}.{last}@abc.mail.com") diff --git a/inputs__examples/read_events_from_gamepad.py b/inputs__examples/read_events_from_gamepad.py new file mode 100644 index 000000000..1fd065a29 --- /dev/null +++ b/inputs__examples/read_events_from_gamepad.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install inputs +from inputs import get_gamepad + + +if __name__ == "__main__": + while True: + events = get_gamepad() + for event in events: + print(event.ev_type, event.code, event.state) diff --git a/inputs__examples/show_all_devices.py b/inputs__examples/show_all_devices.py new file mode 100644 index 000000000..ff435b884 --- /dev/null +++ b/inputs__examples/show_all_devices.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +# pip install inputs +from inputs import DeviceManager + + +while True: + devices = DeviceManager() + print( + f"keyboards: {len(devices.keyboards)}, mice: {len(devices.mice)}, " + f"gamepads: {len(devices.gamepads)}, microbits: {len(devices.microbits)}, " + f"leds: {len(devices.leds)}, other_devices: {len(devices.other_devices)}, " + f"all_devices: {len(devices.all_devices)}" + ) + time.sleep(1) diff --git a/inspect__examples/check_is_empty_function__use__inspect__dis.py b/inspect__examples/check_is_empty_function__use__inspect__dis.py index 53900e818..bc9188079 100644 --- a/inspect__examples/check_is_empty_function__use__inspect__dis.py +++ b/inspect__examples/check_is_empty_function__use__inspect__dis.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from dis__bytecode__is_empty_function import check_is_empty_function as check_is_empty_function__use_dis from inspect__get_source__function import check_is_empty_function as check_is_empty_function__use_inspect -def foo(): +def foo() -> None: pass -def foo2(): +def foo2() -> int: return 1 -print('check_is_empty_function__use_inspect:', check_is_empty_function__use_inspect(foo)) -print('check_is_empty_function__use_inspect:', check_is_empty_function__use_inspect(foo2)) +print("check_is_empty_function__use_inspect:", check_is_empty_function__use_inspect(foo)) +print("check_is_empty_function__use_inspect:", check_is_empty_function__use_inspect(foo2)) print() -print('check_is_empty_function__use_dis:', check_is_empty_function__use_dis(foo)) -print('check_is_empty_function__use_dis:', check_is_empty_function__use_dis(foo2)) +print("check_is_empty_function__use_dis:", check_is_empty_function__use_dis(foo)) +print("check_is_empty_function__use_dis:", check_is_empty_function__use_dis(foo2)) diff --git a/inspect__examples/hello_world.py b/inspect__examples/hello_world.py index aa4c602dc..7ee1f58d3 100644 --- a/inspect__examples/hello_world.py +++ b/inspect__examples/hello_world.py @@ -1,29 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import inspect -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) print() -print('Current filename:', inspect.getframeinfo(inspect.currentframe()).filename) +print("Current filename:", inspect.getframeinfo(inspect.currentframe()).filename) print() -print('Current code context:', inspect.getframeinfo(inspect.currentframe()).code_context) +print( + "Current code context:", inspect.getframeinfo(inspect.currentframe()).code_context +) print() -print('Current function:', inspect.getframeinfo(inspect.currentframe()).function) -print('Current function:', (lambda: inspect.getframeinfo(inspect.currentframe()).function)()) +print("Current function:", inspect.getframeinfo(inspect.currentframe()).function) +print( + "Current function:", + (lambda: inspect.getframeinfo(inspect.currentframe()).function)(), +) def foo(): return inspect.getframeinfo(inspect.currentframe()).function -print('Current function:', foo()) +print("Current function:", foo()) diff --git a/inspect__examples/inspect__get_source__function.py b/inspect__examples/inspect__get_source__function.py index b17eab794..1f9cbbcba 100644 --- a/inspect__examples/inspect__get_source__function.py +++ b/inspect__examples/inspect__get_source__function.py @@ -1,21 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import inspect def check_is_empty_function(function): - import inspect lines = inspect.getsourcelines(function) - return lines[0][-1].strip() == 'pass' + return lines[0][-1].strip() == "pass" + +if __name__ == "__main__": -if __name__ == '__main__': - def foo(): + def foo() -> None: pass - def foo2(): + def foo2() -> int: return 1 - print('Empty:', check_is_empty_function(foo)) - print('Empty:', check_is_empty_function(foo2)) + print("Empty:", check_is_empty_function(foo)) + print("Empty:", check_is_empty_function(foo2)) diff --git a/inspect__examples/is_function.py b/inspect__examples/is_function.py index 9419af8a0..ba007dc9c 100644 --- a/inspect__examples/is_function.py +++ b/inspect__examples/is_function.py @@ -1,30 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def foo(): +import inspect + + + +def foo() -> int: return 1 class Foo: - def __call__(self): + def __call__(self) -> int: return 1 -import inspect print(inspect.isroutine(lambda: 1)) # True -print(inspect.isroutine(foo)) # True +print(inspect.isroutine(foo)) # True print() # Functor foo = Foo() -print(inspect.isroutine(foo)) # False -print(hasattr(foo, '__call__')) # True -print(callable(foo)) # True +print(inspect.isroutine(foo)) # False +print(hasattr(foo, "__call__")) # True +print(callable(foo)) # True print() import math + print(inspect.isroutine(math.sin)) # True -print(inspect.isroutine(print)) # True +print(inspect.isroutine(print)) # True diff --git a/invert_number.py b/invert_number.py index 18fb08adf..b32538251 100644 --- a/invert_number.py +++ b/invert_number.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def invert_number(number: int) -> int: @@ -14,18 +14,18 @@ def invert_number(number: int) -> int: :return: invert number """ - return int(''.join('1' if i == '0' else '0' for i in bin(number)[2:]), base=2) + return int("".join("1" if i == "0" else "0" for i in bin(number)[2:]), base=2) -if __name__ == '__main__': +if __name__ == "__main__": print(invert_number(0)) print(invert_number(1)) print(invert_number(7)) print(invert_number(111)) - print(invert_number(10 ** 10)) + print(invert_number(10**10)) assert invert_number(0) == 1 assert invert_number(1) == 0 assert invert_number(7) == 0 assert invert_number(111) == 16 - assert invert_number(10 ** 10) == 7179869183 + assert invert_number(10**10) == 7179869183 diff --git a/iri_to_uri__cyrillic__no_ascii_url__idna.py b/iri_to_uri__cyrillic__no_ascii_url__idna.py index 43ea72bfb..ca08bd768 100644 --- a/iri_to_uri__cyrillic__no_ascii_url__idna.py +++ b/iri_to_uri__cyrillic__no_ascii_url__idna.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/487922/201445 @@ -10,45 +10,53 @@ from urllib.parse import quote, urlsplit, urlunsplit -def iri_to_uri(iri): +def iri_to_uri(iri: str) -> str: parts = urlsplit(iri) - uri = urlunsplit(( - parts.scheme, - parts.netloc.encode('idna').decode('ascii'), - quote(parts.path), - quote(parts.query, '=&'), - quote(parts.fragment), - )) + uri = urlunsplit( + ( + parts.scheme, + parts.netloc.encode("idna").decode("ascii"), + quote(parts.path), + quote(parts.query, "=&"), + quote(parts.fragment), + ) + ) return uri -if __name__ == '__main__': - url = 'https://сайт.рф/документы?номер=1234&устройство=телефон' +if __name__ == "__main__": + url = "https://сайт.рф/документы?номер=1234&устройство=телефон" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD' + assert ( + new_url + == "https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD" + ) - url = 'https://anapa.russianrealty.ru/Продажа-квартир/' + url = "https://anapa.russianrealty.ru/Продажа-квартир/" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/' + assert ( + new_url + == "https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/" + ) - url = 'http://www.a\u0131b.com/a\u0131b' + url = "http://www.a\u0131b.com/a\u0131b" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://www.xn--ab-hpa.com/a%C4%B1b' + assert new_url == "http://www.xn--ab-hpa.com/a%C4%B1b" - url = 'https://ya.ru' + url = "https://ya.ru" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://ya.ru' + assert new_url == "https://ya.ru" - url = 'https://google.com' + url = "https://google.com" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://google.com' + assert new_url == "https://google.com" - url = 'http://домены.рф' + url = "http://домены.рф" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://xn--d1acufc5f.xn--p1ai' + assert new_url == "http://xn--d1acufc5f.xn--p1ai" diff --git a/iri_to_uri__get_ascii_url.py b/iri_to_uri__get_ascii_url.py index 41cf72661..e49d9da6a 100644 --- a/iri_to_uri__get_ascii_url.py +++ b/iri_to_uri__get_ascii_url.py @@ -1,53 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.parse import urlparse, urlunparse import re +from urllib.parse import urlparse, urlunparse def url_encode_non_ascii(b: bytes) -> bytes: - return re.sub(b'[\x80-\xFF]', lambda c: b'%%%02X' % ord(c.group(0)), b) + return re.sub(b"[\x80-\xFF]", lambda c: b"%%%02X" % ord(c.group(0)), b) # SOURCE: https://stackoverflow.com/a/4391299/5909792 def iri_to_uri(iri: str) -> str: parts = urlparse(iri) return urlunparse( - part.encode('idna') if parti == 1 else url_encode_non_ascii(part.encode('utf-8')) + part.encode("idna") if parti == 1 else url_encode_non_ascii(part.encode("utf-8")) for parti, part in enumerate(parts) - ).decode('utf-8') + ).decode("utf-8") -if __name__ == '__main__': - url = 'https://сайт.рф/документы?номер=1234&устройство=телефон' +if __name__ == "__main__": + url = "https://сайт.рф/документы?номер=1234&устройство=телефон" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD' + assert ( + new_url + == "https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD" + ) - url = 'https://anapa.russianrealty.ru/Продажа-квартир/' + url = "https://anapa.russianrealty.ru/Продажа-квартир/" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/' + assert ( + new_url + == "https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/" + ) - url = 'http://www.a\u0131b.com/a\u0131b' + url = "http://www.a\u0131b.com/a\u0131b" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://www.xn--ab-hpa.com/a%C4%B1b' + assert new_url == "http://www.xn--ab-hpa.com/a%C4%B1b" - url = 'https://ya.ru' + url = "https://ya.ru" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://ya.ru' + assert new_url == "https://ya.ru" - url = 'https://google.com' + url = "https://google.com" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://google.com' + assert new_url == "https://google.com" - url = 'http://домены.рф' + url = "http://домены.рф" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://xn--d1acufc5f.xn--p1ai' + assert new_url == "http://xn--d1acufc5f.xn--p1ai" diff --git a/is_correct_brackets.py b/is_correct_brackets.py index b9941d0e4..77bf8f993 100644 --- a/is_correct_brackets.py +++ b/is_correct_brackets.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_correct_brackets(text): - while '()' in text or '[]' in text or '{}' in text: - text = text.replace('()', '') - text = text.replace('[]', '') - text = text.replace('{}', '') +def is_correct_brackets(text) -> bool: + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()", "") + text = text.replace("[]", "") + text = text.replace("{}", "") # Возвращаем True, если text с пустой строкой return not text -if __name__ == '__main__': - assert is_correct_brackets('(((())))') - assert is_correct_brackets('(((())') is False - assert is_correct_brackets('())))') is False - assert is_correct_brackets('((((){}[]{}[])))') - assert is_correct_brackets('(){}[]{}[])))') is False - assert is_correct_brackets('(){}[]{}[]') +if __name__ == "__main__": + assert is_correct_brackets("(((())))") + assert is_correct_brackets("(((())") is False + assert is_correct_brackets("())))") is False + assert is_correct_brackets("((((){}[]{}[])))") + assert is_correct_brackets("(){}[]{}[])))") is False + assert is_correct_brackets("(){}[]{}[]") diff --git a/is_even__is_odd.py b/is_even__is_odd.py index 1d31fb9ae..2228e8c6c 100644 --- a/is_even__is_odd.py +++ b/is_even__is_odd.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_even(num): +def is_even(num) -> bool: return num % 2 == 0 -def is_odd(num): +def is_odd(num) -> bool: return not is_even(num) -def is_even_2(num): +def is_even_2(num) -> bool: return num & 1 == 0 -if __name__ == '__main__': +if __name__ == "__main__": for i in range(10): - print('{} is even: {}, {}'.format(i, is_even(i), is_even_2(i))) - print('{} is odd: {}'.format(i, is_odd(i))) + print(f"{i} is even: {is_even(i)}, {is_even_2(i)}") + print(f"{i} is odd: {is_odd(i)}") print() diff --git a/is_free_port.py b/is_free_port.py new file mode 100644 index 000000000..d825d8a80 --- /dev/null +++ b/is_free_port.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import socket + + +def is_free_port(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("", port)) + return True + except OSError: + return False + + +if __name__ == "__main__": + print(is_free_port(5510)) + print(is_free_port(8080)) + print(is_free_port(9999)) diff --git a/is_ip.py b/is_ip.py index 21b9ebbda..ec07a4be1 100644 --- a/is_ip.py +++ b/is_ip.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,17 +9,21 @@ # SOURCE: https://stackoverflow.com/a/23166561/5909792 IP_RANGE = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" -IP_REGEXP = f'^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$' +IP_REGEXP = f"^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$" def is_ip(ip: str) -> bool: return bool(re.match(IP_REGEXP, ip)) -if __name__ == '__main__': +if __name__ == "__main__": tests = [ - ('127.0.0.1', True), ('100.100.100.100', True), ('0.0.0.0', True), ('300.300.300.300', False), - ('300.0.0.1', False), ('255.255.255.255', True) + ("127.0.0.1", True), + ("100.100.100.100", True), + ("0.0.0.0", True), + ("300.300.300.300", False), + ("300.0.0.1", False), + ("255.255.255.255", True), ] for ip, expected in tests: diff --git a/is_math_magic_square.py b/is_math_magic_square.py index 0ee9f457d..6715cc329 100644 --- a/is_math_magic_square.py +++ b/is_math_magic_square.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для решения головоломки в локации аэропорта из Grim Tales 9: Threads Of Destiny diff --git a/is_user_admin.py b/is_user_admin.py index c3bbc9440..d1495c90b 100644 --- a/is_user_admin.py +++ b/is_user_admin.py @@ -1,34 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/19719292/5909792 -def is_user_admin(): - import os +import ctypes +import os +import traceback - if os.name == 'nt': - import ctypes + +def is_windows() -> bool: + return os.name == "nt" + + +# Linux or mac +def is_posix() -> bool: + return os.name == "posix" + + +def is_user_admin() -> bool: + if is_windows(): try: - # WARNING: requires Windows XP SP2 or higher! + # WARNING: Requires Windows XP SP2 or higher! return bool(ctypes.windll.shell32.IsUserAnAdmin()) - - except: - import traceback + except (AttributeError, OSError): traceback.print_exc() print("Admin check failed, assuming not an admin.") return False - elif os.name == 'posix': + elif is_posix(): # Check for root on Posix - return os.getuid() == 0 + return os.geteuid() == 0 else: - raise RuntimeError("Unsupported operating system for this module: %s" % (os.name,)) + raise RuntimeError(f"Unsupported operating system for this module: {os.name}") -if __name__ == '__main__': - print(is_user_admin()) +if __name__ == "__main__": + print("is_user_admin:", is_user_admin()) diff --git a/is_valid_ip.py b/is_valid_ip.py index 10c180608..c1082430f 100644 --- a/is_valid_ip.py +++ b/is_valid_ip.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_valid_ip(ip): - import ipaddress +import ipaddress + +def is_valid_ip(ip) -> bool: try: ipaddress.IPv4Address(ip) return True - except ipaddress.AddressValueError: return False -if __name__ == '__main__': - assert is_valid_ip('127.0.0.1') is True - assert is_valid_ip('255.255.255.255') is True - assert is_valid_ip('127.0.0.0.1') is False - assert is_valid_ip('127.0.0.0.-1') is False +if __name__ == "__main__": + assert is_valid_ip("127.0.0.1") is True + assert is_valid_ip("255.255.255.255") is True + assert is_valid_ip("127.0.0.0.1") is False + assert is_valid_ip("127.0.0.0.-1") is False diff --git a/isinstance_example/isinstance.py b/isinstance_example/isinstance.py index 1f0e40533..c7f5bc24a 100644 --- a/isinstance_example/isinstance.py +++ b/isinstance_example/isinstance.py @@ -1,9 +1,8 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - print(isinstance(5, str)) - print(isinstance(5, int)) - print(isinstance(5, bool)) - print(isinstance(False, bool)) - print(isinstance("Hello", str)) \ No newline at end of file +print(isinstance(5, str)) +print(isinstance(5, int)) +print(isinstance(5, bool)) +print(isinstance(False, bool)) +print(isinstance("Hello", str)) diff --git a/jazzcinema_ru/print_all_coming_films.py b/jazzcinema_ru/print_all_coming_films.py index 33291057e..36d705982 100644 --- a/jazzcinema_ru/print_all_coming_films.py +++ b/jazzcinema_ru/print_all_coming_films.py @@ -1,48 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime + from urllib.parse import urljoin from urllib.request import urlopen -from datetime import datetime - from bs4 import BeautifulSoup -if __name__ == '__main__': - url = 'http://www.jazzcinema.ru/' +if __name__ == "__main__": + url = "http://www.jazzcinema.ru/" with urlopen(url) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") for schedule in schedule_list: - schedule_date = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').strftime('%d/%m/%Y') - border_list = schedule.select('.border') + schedule_date = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).strftime("%d/%m/%Y") + border_list = schedule.select(".border") # Если фильмов нет if not border_list: continue - print('Расписание фильмов за {}:'.format(schedule_date)) + print(f"Расписание фильмов за {schedule_date}:") # Получение фильмов в текущей вкладке (по идеи, текущая вкладка -- текущий день) - for border in schedule.select('.border'): - a = border.select_one('.movie .title > a') - url = urljoin(url, a['href']) - print(' "{}": {}'.format(a['title'], url)) - genre = border.select_one('.genre') + for border in schedule.select(".border"): + a = border.select_one(".movie .title > a") + url = urljoin(url, a["href"]) + print(f' "{a["title"]}": {url}') + genre = border.select_one(".genre") if genre: - print(' {}'.format(genre.text)) + print(f" {genre.text}") - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text - print(' {} : {}'.format(time, price)) + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text + print(f" {time} : {price}") print() diff --git a/jazzcinema_ru/print_today_films.py b/jazzcinema_ru/print_today_films.py index 3e964530b..c73f5a354 100644 --- a/jazzcinema_ru/print_today_films.py +++ b/jazzcinema_ru/print_today_films.py @@ -1,52 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime, date + from urllib.parse import urljoin from urllib.request import urlopen from bs4 import BeautifulSoup -if __name__ == '__main__': - url = 'http://www.jazzcinema.ru/' +if __name__ == "__main__": + url = "http://www.jazzcinema.ru/" with urlopen(url) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") - from datetime import datetime, date today = date.today() - today_found = False # Проходим по списку и ищем расписание на текущую дату for schedule in schedule_list: - schedule_date = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').date() - schedule_date_str = schedule_date.strftime('%d/%m/%Y') + schedule_date = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).date() + schedule_date_str = schedule_date.strftime("%d/%m/%Y") - border_list = schedule.select('.border') + border_list = schedule.select(".border") if schedule_date == today and border_list: today_found = True - print('Расписание фильмов на сегодня {}:'.format(schedule_date_str)) + print(f"Расписание фильмов на сегодня {schedule_date_str}:") # Получение фильмов в текущей вкладке (по идеи, текущая вкладка -- текущий день) for border in border_list: - a = border.select_one('.movie .title > a') - url = urljoin(url, a['href']) - print(' "{}": {}'.format(a['title'], url)) - print(' {}'.format(border.select_one('.genre').text)) + a = border.select_one(".movie .title > a") + url = urljoin(url, a["href"]) + print(f' "{a["title"]}": {url}') + print(f" {border.select_one('.genre').text}") - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text - print(' {} : {}'.format(time, price)) + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text + print(f" {time} : {price}") print() if not today_found: - print("Фильмов на сегодня ({}) нет".format(today.strftime('%d/%m/%Y'))) + print(f"Фильмов на сегодня ({today.strftime('%d/%m/%Y')}) нет") diff --git a/jazzcinema_ru/show_schedule_gui/main.py b/jazzcinema_ru/show_schedule_gui/main.py index 80ec6d82a..b49e40b43 100644 --- a/jazzcinema_ru/show_schedule_gui/main.py +++ b/jazzcinema_ru/show_schedule_gui/main.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.parse import urljoin -from urllib.request import urlopen - -from datetime import datetime +import base64 +import logging +import traceback +import sys from collections import OrderedDict +from datetime import datetime +from urllib.parse import urljoin +from urllib.request import urlopen from bs4 import BeautifulSoup @@ -18,47 +21,43 @@ from qtpy.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - import logging logging.critical(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -URL = 'http://www.jazzcinema.ru/schedule/' +URL = "http://www.jazzcinema.ru/schedule/" class Movie: - def __init__(self, border): - a = border.select_one('.movie .title > a') - self.movie_url = urljoin(URL, a['href']) + def __init__(self, border) -> None: + a = border.select_one(".movie .title > a") + self.movie_url = urljoin(URL, a["href"]) - self.title = a['title'] + self.title = a["title"] - self.genre = border.select_one('.genre') + self.genre = border.select_one(".genre") if self.genre: self.genre = self.genre.text self.seanses = OrderedDict() - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text self.seanses[time] = price movie_info = border.next_sibling - self.annotation = movie_info.select_one('.text').text.strip() + self.annotation = movie_info.select_one(".text").text.strip() - img_url = movie_info.select_one('.poster img')["src"] + img_url = movie_info.select_one(".poster img")["src"] self.img_url = urljoin(URL, img_url) # Начало и конец проката @@ -69,29 +68,29 @@ def __init__(self, border): self.producer = None self.actors = None self.duration = None - self.age_restrictions = movie_info.select_one('.text.age-count').text.strip() + self.age_restrictions = movie_info.select_one(".text.age-count").text.strip() - for td in movie_info.select('table td'): + for td in movie_info.select("table td"): key = td.text.strip() if key.endswith(":"): value = td.next_sibling.text.strip() - if 'Начало проката:' == key: + if "Начало проката:" == key: self.start_rentals = value - elif 'Окончание проката:' == key: + elif "Окончание проката:" == key: self.end_rentals = value - elif 'Страна:' == key: + elif "Страна:" == key: self.country = value - elif 'Режиссёр:' == key: + elif "Режиссёр:" == key: self.producer = value - elif 'В ролях:' == key: + elif "В ролях:" == key: self.actors = value - elif 'Продолжительность:' == key: + elif "Продолжительность:" == key: self.duration = value class MovieInfoWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() # TODO: можно и видео добавить @@ -104,19 +103,20 @@ def __init__(self): self.setLayout(layout) - def set_movie(self, movie): + def set_movie(self, movie) -> None: data = dict() # Скачивание обложки и получени base64 with urlopen(movie.img_url) as f: - import base64 - img_base64 = base64.standard_b64encode(f.read()).decode('utf-8') + img_base64 = base64.standard_b64encode(f.read()).decode("utf-8") data["img_url"] = "data:image/png;base64," + img_base64 - seanses_table = '' + seanses_table = "
    " for time, price in movie.seanses.items(): - seanses_table += "".format(time, price) + seanses_table += ( + f"" + ) seanses_table += "
    {}    {}
    {time}    {price}
    " @@ -159,20 +159,27 @@ def set_movie(self, movie): - """.format(movie, **data) + """.format( + movie, **data + ) self.browser.setHtml(html) class SchedulerMoviePage(QWidget): - def __init__(self, schedule): + def __init__(self, schedule) -> None: super().__init__() - self.schedule_date_str = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').strftime('%d/%m/%Y') + self.schedule_date_str = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).strftime("%d/%m/%Y") self.movie_list_widget = QListWidget() - self.movie_list_widget.currentItemChanged.connect(lambda current, previous: - self.movie_info.set_movie(current.data(Qt.UserRole))) + self.movie_list_widget.currentItemChanged.connect( + lambda current, previous: self.movie_info.set_movie( + current.data(Qt.UserRole) + ) + ) self.movie_info = MovieInfoWidget() @@ -185,7 +192,7 @@ def __init__(self, schedule): self.setLayout(layout) # Получение фильмов в текущей вкладке (в каждой вкладке будет свой список фильмов на день) - for border in schedule.select('.border'): + for border in schedule.select(".border"): movie = Movie(border) item = QListWidgetItem(movie.title) @@ -196,15 +203,15 @@ def __init__(self, schedule): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('show_schedule_gui.py') + self.setWindowTitle("show_schedule_gui.py") self.tab_widget = QTabWidget() layout = QVBoxLayout() - layout.addWidget(QLabel('Расписание фильмов на:')) + layout.addWidget(QLabel("Расписание фильмов на:")) layout.addWidget(self.tab_widget) widget = QWidget() @@ -212,24 +219,24 @@ def __init__(self): self.setCentralWidget(widget) - def load(self): + def load(self) -> None: with urlopen(URL) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") # Проходим по списку расписаний for schedule in schedule_list: # Если фильмов нет - if not schedule.select('.border'): + if not schedule.select(".border"): continue tab = SchedulerMoviePage(schedule) self.tab_widget.addTab(tab, tab.schedule_date_str) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/jinja2__examples/hello_world.py b/jinja2__examples/hello_world.py index 4f44a8558..afb190cc0 100644 --- a/jinja2__examples/hello_world.py +++ b/jinja2__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from dataclasses import dataclass @@ -16,19 +16,21 @@ class User: url: str -template = jinja2.Template("""\ +template = jinja2.Template( + """\ {{ title }} -""") +""" +) users = [ - User('1', 'https://a.bc/user/1'), - User('2', 'https://a.bc/user/2'), - User('3', 'https://a.bc/user/3'), + User("1", "https://a.bc/user/1"), + User("2", "https://a.bc/user/2"), + User("3", "https://a.bc/user/3"), ] html = template.render(title="Hello World!", users=users) diff --git a/jinja2__examples/table_by_date_name_number.py b/jinja2__examples/table_by_date_name_number.py new file mode 100644 index 000000000..3a1de2392 --- /dev/null +++ b/jinja2__examples/table_by_date_name_number.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from datetime import datetime + +import jinja2 + + +array1 = [ + [datetime(2020, 10, 27, 12, 37), "Саша"], + [datetime(2020, 10, 28, 16, 2), "Олег"], + [datetime(2020, 10, 27, 16, 40), "Саша"], + [datetime(2020, 10, 27, 16, 41), "Саша"], + [datetime(2020, 10, 27, 12, 54), "Костя"], + [datetime(2020, 10, 27, 12, 27), "Костя"], + [datetime(2020, 10, 27, 12, 27), "Олег"], + [datetime(2020, 10, 27, 12, 54), "Саша"], +] + +result = defaultdict(dict) +dates = set() + +for dt, name in array1: + date = dt.date() + + if name not in result: + result[name] = dict() + + if date not in result[name]: + result[name][date] = 0 + + result[name][date] += 1 + dates.add(date) + + +template = jinja2.Template( + """\ + + + + {% for date in dates %} + + {% endfor %} + + {% for name, date_by_number in result.items() %} + + + {% for date in dates %} + + {% endfor %} + + {% endfor %} +
    Отчет{{ date }}
    {{ name }}{{ date_by_number.get(date, "") }}
    +""" +) + +html_tab = template.render( + dates=sorted(dates), + result=result, +) +print(html_tab) diff --git a/jinja2__examples/table_tr_class_by_condition.py b/jinja2__examples/table_tr_class_by_condition.py index b1f2e4f12..d879f4662 100644 --- a/jinja2__examples/table_tr_class_by_condition.py +++ b/jinja2__examples/table_tr_class_by_condition.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install jinja2 import jinja2 -template = jinja2.Template("""\ +template = jinja2.Template( + """\ {% for x in rows_1 %} @@ -21,18 +22,19 @@ {% endfor %}
    -""") +""" +) in_dict = { - 'a': [1, 2, 3], - 'b': [3, 4, 5], - 'c': [99, 3], + "a": [1, 2, 3], + "b": [3, 4, 5], + "c": [99, 3], } html = template.render( rows_1=[k for k, v in in_dict.items()], - rows_2=[sum(v) for k, v in in_dict.items()] + rows_2=[sum(v) for k, v in in_dict.items()], ) print(html) """ @@ -56,4 +58,4 @@ -""" \ No newline at end of file +""" diff --git a/jira_logged_human_time_to_seconds.py b/jira_logged_human_time_to_seconds.py new file mode 100644 index 000000000..1a92ae403 --- /dev/null +++ b/jira_logged_human_time_to_seconds.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from enum import IntEnum + + +class UnitSeconds(IntEnum): + MINUTE = 60 + HOUR = 60 * MINUTE + DAY = 8 * HOUR + WEEK = 5 * DAY + + +def logged_human_time_to_seconds(human_time: str) -> int: + """Конвертирование человеко-читаемого времени в секунды. + + >>> logged_human_time_to_seconds("1 minute") + 60 + >>> logged_human_time_to_seconds("1 hour") + 3600 + >>> logged_human_time_to_seconds("6 hours, 30 minutes") + 23400 + >>> logged_human_time_to_seconds("1 day, 30 minutes") + 30600 + """ + + # Jira help: + # You can specify a time unit after a time value 'X', such as Xw, Xd, Xh or Xm, to represent weeks (w), + # days (d), hours (h) and minutes (m), respectively. + # If you do not specify a time unit, minute will be assumed. + # Your current conversion rates are 1w = 5d and 1d = 8h. + + total_seconds = 0 + for part in human_time.split(", "): + value, metric = part.upper().split() + + value = int(value) + metric = metric.removesuffix("S") # NOTE: "HOURS" -> "HOUR" + + total_seconds += value * UnitSeconds[metric] + + return total_seconds + + +def seconds_to_logged_human_time(seconds: int) -> str: + weeks, seconds = divmod(seconds, UnitSeconds.WEEK) + days, seconds = divmod(seconds, UnitSeconds.DAY) + hours, seconds = divmod(seconds, UnitSeconds.HOUR) + minutes, _ = divmod(seconds, UnitSeconds.MINUTE) + + get_suffix = lambda n: 's' if n > 1 else '' + items = [ + f"{weeks} week{get_suffix(weeks)}" if weeks else '', + f"{days} day{get_suffix(days)}" if days else '', + f"{hours} hour{get_suffix(hours)}" if hours else '', + f"{minutes} minute{get_suffix(minutes)}" if minutes else '', + ] + return ", ".join(filter(None, items)) + + +if __name__ == "__main__": + items = [ + ("2 hours", 2 * 60 * 60), + ("1 hour, 15 minutes", 3600 + 15 * 60), + ("1 hour, 30 minutes", 3600 + 1800), + ("4 hours", 4 * 3600), + ("7 hours", 7 * 3600), + ("1 day, 1 hour", 8 * 3600 + 3600), + ("6 hours, 30 minutes", 6 * 3600 + 1800), + ("30 minutes", 30 * 60), + ("1 day", 8 * 3600), + ("1 hour", 3600), + ("4 hours, 30 minutes", 4 * 3600 + 1800), + ("40 minutes", 40 * 60), + ("45 minutes", 45 * 60), + ("7 hours, 30 minutes", 7 * 3600 + 1800), + ("1 day, 30 minutes", 8 * 3600 + 1800), + ("5 hours", 5 * 3600), + ("5 hours, 30 minutes", 5 * 3600 + 1800), + ("1 minute", 60), + ("1 hour, 20 minutes", 3600 + 20 * 60), + ("6 hours", 6 * 3600), + ("3 hours, 30 minutes", 3 * 3600 + 1800), + ("15 minutes", 15 * 60), + ("3 hours", 3 * 3600), + ("1 week", 5 * 8 * 3600), + ( + "1 week, 2 days, 1 hour", + logged_human_time_to_seconds("5 day") + + logged_human_time_to_seconds("2 day") + + logged_human_time_to_seconds("1 hour") + ), + ] + for value, expected in items: + seconds = logged_human_time_to_seconds(value) + print(f"{value!r} -> {seconds} seconds") + assert expected == seconds + + logged_human = seconds_to_logged_human_time(seconds) + print(f"{seconds} seconds -> {logged_human!r}") + assert value == logged_human + + print() + + assert logged_human_time_to_seconds("1 day") == logged_human_time_to_seconds("8 hours") + assert logged_human_time_to_seconds("5 days") == logged_human_time_to_seconds("1 week") + + value = "1 day, 4 hours, 15 minutes" + assert seconds_to_logged_human_time(logged_human_time_to_seconds(value)) == value diff --git a/jira_time.py b/jira_time.py index 0d3a7c1c7..b83b4eb61 100644 --- a/jira_time.py +++ b/jira_time.py @@ -1,75 +1,81 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def parser_my_jira_time_logs(log): - """ Функция принимает список строк вида: - 7417 10:00-12:00 - 7417 12:19-14:00 - 7417 14:37-15:30 - 7417 15:58-17:50 +import re - 7415 15:58-15:59 +from collections import defaultdict +from datetime import datetime - 7456 14:28-15:59 - То, что перед ' ' -- уникальный номер задания - Диапазон после ' ' -- отрезок времени вида: начало - конец +def seconds_to_jira_str(seconds: int) -> str: + hours, minutes = divmod(seconds, 3600) + minutes //= 60 + days, hours = divmod(hours, 8) + weeks, days = divmod(days, 5) - Далее функция подсчитает количество часов и минут для каждого задания - и выведет их - """ - - # TODO: Защита от копипаста: строки могут повторяться и время подсчитается неправильно - # TODO: Если часы перевалят за 24, то начнется отсчет заного - # TODO: Для джиры дни и недели не астрономические: 1d = 8h и 1w = 5d + items = [ + f"{weeks}w" if weeks else "", + f"{days}d" if days else "", + f"{hours}h" if hours else "", + f"{minutes}m" if minutes else "", + ] + return " ".join(filter(None, items)) - import re - pattern = re.compile(r'(.+) (\d\d:\d\d)-(\d\d:\d\d)') - from datetime import datetime as dt - import time - from collections import defaultdict +def str_to_time(s: str) -> datetime: + return datetime.strptime(s, "%H:%M") - jira_time = defaultdict(int) - for line in log.split('\n'): - if line: - m = pattern.match(line.strip()) +def parser_my_jira_time_logs(log: str) -> str: + pattern = re.compile(r"(.+) (\d\d:\d\d)-(\d\d:\d\d)") - jira = m.group(1) - t1 = m.group(2) - t2 = m.group(3) - delta = dt.strptime(t2, '%H:%M') - dt.strptime(t1, '%H:%M') - seconds = delta.seconds + jira_by_seconds: dict[str, int] = defaultdict(int) - jira_time[jira] += seconds + for line in log.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue - for jira, secs in jira_time.items(): - t = time.gmtime(secs) - h = t.tm_hour - m = t.tm_min - jira_time = None - if h: - jira_time = str(h) + 'h' - if m: - if jira_time: - jira_time += ' ' + str(m) + 'm' - else: - jira_time = str(m) + 'm' + m = pattern.match(line) - print('%s: %s' % (jira, jira_time)) + jira: str = m.group(1) + t1: str = m.group(2) + t2: str = m.group(3) + seconds: int = (str_to_time(t2) - str_to_time(t1)).seconds + jira_by_seconds[jira] += seconds -parser_my_jira_time_logs(""" -7417 10:00-12:00 -7417 12:19-14:00 -7417 14:37-15:30 -7417 15:58-17:50 + return "\n".join( + f"{jira}: {seconds_to_jira_str(seconds)}" + for jira, seconds in jira_by_seconds.items() + ) -7415 15:58-15:59 -7456 14:28-15:59 -""") +if __name__ == "__main__": + print( + parser_my_jira_time_logs( + """ + # Foo + 7417 10:00-18:00 + + # Bar + 7417 10:00-12:00 + 7417 12:19-14:00 + 7417 14:37-15:30 + 7417 15:58-17:50 + + 7415 15:58-15:59 + 7456 14:28-15:59 + 7425 10:00-18:10 + """ + ) + ) + """ + 7417: 1d 6h 26m + 7415: 1m + 7456: 1h 31m + 77425: 1d 10m + """ diff --git a/job_compassplus/.deprecated/job_report/main.py b/job_compassplus/.deprecated/job_report/main.py new file mode 100644 index 000000000..832529c6e --- /dev/null +++ b/job_compassplus/.deprecated/job_report/main.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from itertools import chain +from utils import get_report_persons_info, get_person_info + + +report_dict = get_report_persons_info() + +# Вывести всех сотрудников, отсортировав их по количестве переработанных часов +person_list = set(chain(*report_dict.values())) + +# Проверка того, что сортировка работает (в принципе, думаю можно удалить) +assert sorted(person_list, key=lambda x: x.deviation_of_time) == sorted( + person_list, key=lambda x: x.deviation_of_time.total +) + +sorted_person_list = sorted( + person_list, key=lambda x: x.deviation_of_time, reverse=True +) + +for i, person in enumerate(sorted_person_list, 1): + print(f"{i:>3}. {person.full_name} {person.deviation_of_time}") + +print() +person = get_person_info( + second_name="Петраш", first_name="Илья", report_dict=report_dict +) +if person: + print( + f"#{sorted_person_list.index(person) + 1}. {person.full_name} {person.deviation_of_time}" + ) diff --git a/job_compassplus/.deprecated/job_report/report_person.py b/job_compassplus/.deprecated/job_report/report_person.py new file mode 100644 index 000000000..4d52fc0c9 --- /dev/null +++ b/job_compassplus/.deprecated/job_report/report_person.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from functools import total_ordering + + +class ReportPerson: + """Класс для описания сотрудника в отчете.""" + + def __init__(self, tags: list[str]): + # ФИО + self.second_name, self.first_name, self.middle_name = tags[0].split(maxsplit=2) + + # Невыходов на работу + self.absence_from_work = int(tags[1]) + + # По календарю (смен / ч:мин) + # Для точного значения посещенных дней, может быть указано как "3 = 4- (1 О)", поэтому + # отсекаем правую, от знака равно, сторону, удаляем пробелы и переводим в число + self.need_to_work_days = self.get_work_day(tags[2]) + self.need_to_work_on_time = self.get_work_time(tags[3]) + + # Фактически (смен / ч:мин) + self.worked_days = self.get_work_day(tags[4]) + self.worked_time = self.get_work_time(tags[5]) + + # Отклонение (смен / ч:мин) + self.deviation_of_day = self.get_work_day(tags[6]) + self.deviation_of_time = self.get_work_time(tags[7]) + + @property + def full_name(self) -> str: + return f"{self.second_name} {self.first_name} {self.middle_name}" + + @staticmethod + def get_work_day(day_str) -> int: + return ( + int(day_str) if "=" not in day_str else int(day_str.split("=")[0].strip()) + ) + + @total_ordering + class Time: + """Простой класс для хранения даты работы.""" + + def __init__(self, time_str: str): + self._hours, self._minutes, self._seconds = map(int, time_str.split(":")) + + @property + def total(self) -> int: + """Всего минут""" + + return self._hours * 60 + self._minutes + + def __repr__(self) -> str: + return f"{self._hours:0>2}:{self._minutes:0>2}" + + def __eq__(self, other: "Time") -> bool: + return self.total == other.total + + def __lt__(self, other: "Time") -> bool: + return self.total < other.total + + @staticmethod + def get_work_time(time_str: str) -> Time: + return ReportPerson.Time(time_str) + + def __hash__(self) -> int: + return hash(self.full_name) + + def __eq__(self, other: "ReportPerson") -> bool: + return self.full_name == other.full_name + + def __repr__(self) -> str: + return ( + f"{self.full_name}. Невыходов на работу: {self.absence_from_work}. " + f"По календарю ({self.need_to_work_days} смен / {self.need_to_work_on_time} ч:мин). " + f"Фактически ({self.worked_days} смен / {self.worked_time} ч:мин). " + f"Отклонение ({self.deviation_of_day} смен / {self.deviation_of_time} ч:мин)" + ) diff --git a/job_compassplus/.deprecated/job_report/utils.py b/job_compassplus/.deprecated/job_report/utils.py new file mode 100644 index 000000000..679acf7e0 --- /dev/null +++ b/job_compassplus/.deprecated/job_report/utils.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os.path +import sys + +from datetime import datetime +from collections import defaultdict +from itertools import chain +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +sys.path.append(str(DIR.parent)) +from current_job_report.utils import get_report_context + +from bs4 import BeautifulSoup + +from report_person import ReportPerson + + +LOGGING_DEBUG = False + + +if LOGGING_DEBUG: + import logging + + logging.basicConfig(level=logging.DEBUG) + + +def get_report_persons_info() -> dict[str, set[ReportPerson]]: + today = datetime.today().strftime("%Y-%m-%d") + report_file_name = f"report_{today}.html" + + # Если кэш-файл отчета не существует, загружаем новые данные и сохраняем в кэш-файл + if not os.path.exists(report_file_name): + if LOGGING_DEBUG: + print(f"{report_file_name} not exist") + + context = get_report_context() + + with open(report_file_name, mode="w", encoding="utf-8") as f: + f.write(context) + else: + if LOGGING_DEBUG: + print(f"{report_file_name} exist") + + with open(report_file_name, encoding="utf-8") as f: + context = f.read() + + html = BeautifulSoup(context, "lxml") + report = html.select("#report tbody tr") + + current_dep = None + report_dict = defaultdict(set) + + for row in report: + children = list(row.children) + if len(children) == 1 and children[0].name == "th": + current_dep = children[0].text.strip() + continue + + if children[0].has_attr("class") and children[0].attrs["class"][0] == "person": + person_tags = [children[0].text] + [ + i.text for i in row.nextSibling.select("td")[1:] + ] + if len(person_tags) != 8: + continue + + person = ReportPerson(person_tags) + report_dict[current_dep].add(person) + + return report_dict + + +def get_person_info(second_name, first_name=None, middle_name=None, report_dict=None): + if not report_dict: + report_dict = get_report_persons_info() + + # Вывести всех сотрудников, отсортировав их по количеству переработанных часов + for person in list(chain(*report_dict.values())): + found = person.second_name == second_name + + if first_name is not None: + found = found and person.first_name == first_name + + if middle_name is not None: + found = found and person.middle_name == middle_name + + if found: + return person + + +if __name__ == "__main__": + report_dict = get_report_persons_info() + print(len(report_dict)) + print(sum(map(len, report_dict.values()))) + print() + + for dep, persons in report_dict.items(): + print(f"{dep} ({len(persons)})") + for p in persons: + print(f" {p}") + + print() diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py new file mode 100644 index 000000000..a069def45 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +from bs4 import BeautifulSoup + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from current_job_report.utils import get_report_context + + +def get_all_names(split_name=False): + """ + split_name = False: ["<Фамилия> <Имя> <Отчество>", ...] + split_name = True: [["<Фамилия>", "<Имя>", "<Отчество>"], ...] + + """ + + text = get_report_context() + root = BeautifulSoup(text, "html.parser") + + # Имена описаны как "<Фамилия> <Имя> <Отчество>" + items = sorted( + {" ".join(report.text.split()) for report in root.select("#report .person")} + ) + + if split_name: + return [x.split(maxsplit=2) for x in items] + + return items + + +if __name__ == "__main__": + # Имена описаны как "<Фамилия> <Имя> <Отчество>" + name_list = get_all_names() + + total = len(name_list) + print("Total:", total) + + print_line_format = "{:%s}. {}" % len(str(total)) + + for i, name in enumerate(name_list, 1): + print(print_line_format.format(i, name)) diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py new file mode 100644 index 000000000..d6c2ca880 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для поиска однофамильцев. + +""" + + +from collections import defaultdict + +# pip install pymorphy2 +import pymorphy2 + +from print_statistic_all_names import get_all_names + + +name_list = get_all_names() +total = len(name_list) +print("Total:", total) +print() + + +morph = pymorphy2.MorphAnalyzer() + + +def get_normal_form(word): + return morph.parse(word)[0].normal_form + + +second_name_by_full_name_list = defaultdict(list) + +for name in name_list: + second_name = name.split()[1] + normal_form = get_normal_form(second_name) + + second_name_by_full_name_list[normal_form].append(name) + +# Фильтр по однофамильцам +second_name_by_full_name_list = filter( + lambda x: len(x[1]) > 1, second_name_by_full_name_list.items() +) + +# Сортировка по количеству фамилий +second_name_by_full_name_list = sorted( + second_name_by_full_name_list, key=lambda x: len(x[1]), reverse=True +) + +# Вывод итоговых данных +for second_name, full_name_list in second_name_by_full_name_list: + print(f"{second_name.upper()} ({len(full_name_list)}): {sorted(full_name_list)}") diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py new file mode 100644 index 000000000..f4785dd05 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для подсчета мужчин и женщин по именам. + +""" + + +# pip install pymorphy2 +import pymorphy2 + +from print_statistic_all_names import get_all_names + + +name_list = get_all_names(split_name=True) +total = len(name_list) +print("Total:", total) +print() + +morph = pymorphy2.MorphAnalyzer() + +masc_name_list = [] +femn_name_list = [] + +for _, first_name, _ in name_list: + parsed_word_list = morph.parse(first_name) + parsed_word = None + + if len(parsed_word_list) > 1: + parsed_word_list_filtered = list( + filter( + lambda x: x.normal_form.lower() == first_name.lower(), parsed_word_list + ) + ) + if not parsed_word_list_filtered: + # Алена -> Алёна + parsed_word_list_filtered = list( + filter( + lambda x: x.normal_form.lower() + == first_name.lower().replace("е", "ё"), + parsed_word_list, + ) + ) + if not parsed_word_list_filtered: + print("Error by parsing name:", first_name.upper(), parsed_word_list) + continue + + parsed_word = parsed_word_list_filtered[0] + else: + parsed_word = parsed_word_list_filtered[0] + else: + parsed_word = parsed_word_list[0] + + gender = parsed_word.tag.gender + + if gender == "masc": + masc_name_list.append(first_name) + else: + femn_name_list.append(first_name) + +# Сортировка списка имен +masc_name_list.sort() +femn_name_list.sort() + +print(f"Masc ({len(masc_name_list)}): {masc_name_list}") +print(f"Femn ({len(femn_name_list)}): {femn_name_list}") diff --git a/currrent_job_report/print_statistic_top_first_name.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py similarity index 88% rename from currrent_job_report/print_statistic_top_first_name.py rename to job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py index 565e36089..d11f22212 100644 --- a/currrent_job_report/print_statistic_top_first_name.py +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -29,16 +29,18 @@ """ +from collections import Counter from print_statistic_all_names import get_all_names + + first_name_list = [name[1] for name in get_all_names(split_name=True)] total = len(first_name_list) -print('Total:', total) +print("Total:", total) print() -print('Top 15:') -from collections import Counter +print("Top 15:") counter = Counter(first_name_list) # Сортировка по количеству for name, number in sorted(counter.items(), key=lambda x: x[1], reverse=True)[:15]: - print(' {}: {} ({:.1f}%)'.format(name, number, number * 100 / total)) + print(f" {name}: {number} ({number * 100 / total:.1f}%)") diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py new file mode 100644 index 000000000..2f84c9a99 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для вывода имен без повторений. + +""" + + +from print_statistic_all_names import get_all_names + + +first_name_list = [name[1] for name in get_all_names(split_name=True)] +print("Total:", len(first_name_list)) + +unique_first_name_list = list(set(first_name_list)) +print(f"Total unique ({len(unique_first_name_list)}): {unique_first_name_list}") diff --git a/UpdateTxEnums/README.md b/job_compassplus/UpdateTxEnums/README.md similarity index 100% rename from UpdateTxEnums/README.md rename to job_compassplus/UpdateTxEnums/README.md diff --git a/UpdateTxEnums/UpdateTxEnums.py b/job_compassplus/UpdateTxEnums/UpdateTxEnums.py similarity index 99% rename from UpdateTxEnums/UpdateTxEnums.py rename to job_compassplus/UpdateTxEnums/UpdateTxEnums.py index 34e444277..014b5c416 100644 --- a/UpdateTxEnums/UpdateTxEnums.py +++ b/job_compassplus/UpdateTxEnums/UpdateTxEnums.py @@ -2,7 +2,7 @@ # TXSST-148: # Нужно написать скрипт, который будет автоматически генерировать классы -# перечеслений на C# в соответствии с этими перечислениями в транзаксисе. +# перечислений на C# в соответствии с этими перечислениями в транзаксисе. # Особенно актуально для CustInfoKind, т.к. оно периодически обновляется # и его можно задавать при настройке объектов сценария. @@ -43,7 +43,7 @@ ) ## Комментарии констант перечислений. -# Ключем является id перечисления, а значением словарь: ключ - имя константы, значение - комментарий константы. +# Ключом является id перечисления, а значением словарь: ключ - имя константы, значение - комментарий константы. dict_enum_constants_comments = { # # AppContextType # 'acsIFBAJ3UCLJCJ7GPUOYTFLPG6SA' : { @@ -727,7 +727,7 @@ def rowsToCSharpXmlComment(rows): # @param rows list[unicode] Список строк текста, который нужно "обернуть" в документирущий комментарий C# # @return unicode Текст в комментарии , строки которого разделены '\n' - return u'/// \n%s/// ' % ''.join([u'/// {}\n'.format(row) + '' for row in rows]) + return u'/// \n%s/// ' % ''.join(f'/// {row}\n' + '' for row in rows) def getSortedEnumItems(enum_items): @@ -761,7 +761,7 @@ def addCommentBlockAboutTxEnum(enum): # @param enum TxEnum Перечисление, по которому нужно сгенерировать дополнительную информацию. # @return str Текст с комментарием c#, содержащий информацию об перечислении TX. - return "/// \n" % (enum.getId(), enum.getStrValType()) + return f"/// \n" def addStandartUsingDirective(): diff --git a/job_compassplus/copying_jar_from_radix/main.py b/job_compassplus/copying_jar_from_radix/main.py new file mode 100644 index 000000000..5f30b83cc --- /dev/null +++ b/job_compassplus/copying_jar_from_radix/main.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shutil +import sys +from pathlib import Path + +sys.path.append("..") +from tx_parse_xml.get_layer_version import get_layer_version + + +# Example: org.radixware\kernel\common\bin +KERNEL_COMMON_BIN_JARS: list[str] = [ + "xb_acs.jar", + "xb_adsdef.jar", + "xb_commondef.jar", + "xb_utils.jar", + "general.jar", + "xb_msdl.jar", + "xb_types.jar", +] + +# Example: org.radixware\kernel\common\lib +KERNEL_COMMON_LIB_JARS: list[str] = [ + "log4j-api-2.17.1.jar", + "log4j-core-2.17.1.jar", + "saxon-HE-10.3.jar", + "xbean.jar", + "xbean_xpath.jar", + "xercesImpl-2.9.1.jar", +] + +OTHER_PATHS: dict[str, str] = { + "ads/ServiceBus/bin/common.jar": "ServiceBus-common.jar", +} + + +def process(root_dir: Path): + path_radix = root_dir / "org.radixware" + if not path_radix.exists(): + raise Exception(f'Not found path {str(path_radix)!r}!') + + radix_version = get_layer_version(path_radix) + print("Radix version:", radix_version) + + # NOTE: Текущая папка скрипта + path_to_copy = Path(__file__).resolve().parent / f"radix-{radix_version}" + + path_kernel_common_bin = path_radix / "kernel/common/bin" + if not path_kernel_common_bin.exists(): + raise Exception(f'Not found path {str(path_kernel_common_bin)!r}!') + + path_kernel_common_lib = path_radix / "kernel/common/lib" + if not path_kernel_common_lib.exists(): + raise Exception(f'Not found path {str(path_kernel_common_lib)!r}!') + + old_to_new_paths: dict[Path, Path] = dict() + + for file in KERNEL_COMMON_BIN_JARS: + old_path = path_kernel_common_bin / file + old_to_new_paths[old_path] = path_to_copy / old_path.name + + for file in KERNEL_COMMON_LIB_JARS: + old_path = path_kernel_common_lib / file + old_to_new_paths[old_path] = path_to_copy / old_path.name + + for file, new_file in OTHER_PATHS.items(): + old_path = path_radix / file + old_to_new_paths[old_path] = path_to_copy / new_file + + # Проверка наличия всех путей + for old_path in old_to_new_paths.keys(): + if not old_path.exists(): + raise Exception(f'Not found path {str(old_path)!r}!') + + path_to_copy.mkdir(exist_ok=True, parents=True) + for old_path, new_path in old_to_new_paths.items(): + print(f"Copy {str(old_path)!r} to {str(new_path)!r}") + shutil.copy(old_path, new_path) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Copying jar from radix" + ) + parser.add_argument( + "root_path", + metavar="/PATH/TO/PROJECT", + type=Path, + help="Path to source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.root_path) diff --git a/job_compassplus/current_job_report/.deprecated/get_latecomers.py b/job_compassplus/current_job_report/.deprecated/get_latecomers.py new file mode 100644 index 000000000..88f30d32a --- /dev/null +++ b/job_compassplus/current_job_report/.deprecated/get_latecomers.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from collections import defaultdict +from datetime import date, datetime +from typing import Callable + +from bs4 import BeautifulSoup, Tag +from utils import NotFoundReport, get_report_context + + +def get_latecomers( + dep: str = "dep12", + period_date: date = None, + is_latecome: Callable[[datetime], bool] = lambda day: day.hour >= 11, +) -> dict[str, list[datetime]]: + html = get_report_context( + dep=dep, + rep="rep2", # Детальный отчет + period=period_date.strftime("%Y-%m"), + ) + + root = BeautifulSoup(html, "html.parser") + + person_by_dates: dict[str, list[datetime]] = defaultdict(list) + + report_table = root.select_one("table#report") + if not report_table: # Отчет еще не готов + raise NotFoundReport() + + dates: list[str | None] = [] + for el in report_table.select("thead > tr:nth-child(2) > th"): + # NOTE: "17.08" -> (17, 08) + day, month = map(int, el.get_text(strip=True).split(".")) + + d = date(year=period_date.year, month=month, day=day) + dates.append(d.isoformat() if d.isoweekday() not in (6, 7) else None) + + tr_list = report_table.select("tr:has(td.person)") + assert tr_list, "Другой формат таблицы. Не нашлось строк с пользователями" + + for tr in tr_list: + tds: list[Tag] = tr.select("td") + if len(tds) < 3: + continue + + # NOTE: Содержит ячейки с временем входа, типа "08:41:18" + td_person, td_total, td_enter, *td_enter_times = tds + assert ( + td_enter.get_text(strip=True) == "Вход" + ), "Другой формат таблицы. Не строка с временем входа" + assert len(td_enter_times) == len( + dates + ), "Другой формат таблицы. Количество столбцов в заголовке и в строке не совпадает" + + # NOTE: Содержит ячейки с временем нахождения, типа "08:41:18" или отсутствия: + # "О\n-" - отпуск, "Т\n07:00:00" - временный отпуск и т.п. Время есть, если в офис входили в этот день + # (исключение для Т) + td_time, *td_total_day_times = ( + tr.find_next_sibling("tr").find_next_sibling("tr").select("td") + ) + assert ( + td_time.get_text(strip=True) == "Время" + ), "Другой формат таблицы. Не строка с временем нахождения" + assert len(td_total_day_times) == len( + dates + ), "Другой формат таблицы. Количество столбцов в заголовке и в строке не совпадает" + + person: str = td_person.get_text(strip=True) + + # Применительно для текущего пользователя + if person in person_by_dates: + continue + + enter_times: list[str | None] = [ + el if re.search(r"\d{2}:\d{2}:\d{2}", el) else None + for el in map(lambda el: el.get_text(strip=True), td_enter_times) + ] + total_day_times: list[str | None] = [ + # Проверка, что в строке есть только цифры и двоеточие + None if re.sub(r"[\d:\s]", "", el) else el + for el in map(lambda el: el.get_text(strip=True), td_total_day_times) + ] + + for date_str, enter_time_str, total_day_time_str in zip( + dates, enter_times, total_day_times + ): + if not date_str or not enter_time_str or not total_day_time_str: + continue + + day: datetime = datetime.fromisoformat(f"{date_str} {enter_time_str}") + if is_latecome(day): + person_by_dates[person].append(day) + + return person_by_dates + + +if __name__ == "__main__": + DEP: str = "dep12" + + period_date: date = date.today() + # NOTE: За конкретную дату + # period_date: date = date(year=2024, month=9, day=1) + print(f"Отчет за {period_date:%Y-%m}") + + try: + person_by_dates: dict[str, list[datetime]] = get_latecomers( + dep=DEP, period_date=period_date + ) + print(f"Опоздавшие ({len(person_by_dates)}):") + + for person, days in person_by_dates.items(): + print(f" {person} ({len(days)}):") + for day in days: + print(f" {day}") + + print() + + except NotFoundReport: + print("Не готов") + + # NOTE: За период + # print() + # + # person_by_dates: dict[str, list[datetime]] = defaultdict(list) + # for month in [7, 8, 9]: + # try: + # result = get_latecomers( + # dep=DEP, period_date=date(year=2024, month=month, day=1) + # ) + # except NotFoundReport: + # continue + # + # for person, days in result.items(): + # person_by_dates[person] += days + # + # print(f"Опоздавшие ({len(person_by_dates)}):") + # for person, days in person_by_dates.items(): + # print(f" {person} ({len(days)}):") + # for day in days: + # print(f" {day}") + # + # print() diff --git a/currrent_job_report/build_exe.bat b/job_compassplus/current_job_report/build_exe.bat similarity index 100% rename from currrent_job_report/build_exe.bat rename to job_compassplus/current_job_report/build_exe.bat diff --git a/job_compassplus/current_job_report/favicon.png b/job_compassplus/current_job_report/favicon.png new file mode 100644 index 000000000..7fbeb22cf Binary files /dev/null and b/job_compassplus/current_job_report/favicon.png differ diff --git a/job_compassplus/current_job_report/get_hours_worked.py b/job_compassplus/current_job_report/get_hours_worked.py new file mode 100644 index 000000000..89c05ffad --- /dev/null +++ b/job_compassplus/current_job_report/get_hours_worked.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from bs4 import BeautifulSoup, Tag +from utils import ( + NotFoundReport, + ReportTypeEnum, + PeriodTypeEnum, + get_report, + clear_hours, +) + + +def get_text_children(el: Tag, idx: int) -> str: + try: + return el.find_all(recursive=False)[idx].text.strip() + except Exception: + return "" + + +def get_tr_for_current_user(html: str) -> Tag: + root = BeautifulSoup(html, "html.parser") + + tbody_el = root.select_one("#report > tbody") + if not tbody_el: + tbody_el = root.select_one(".report > tbody") + if not tbody_el: + raise NotFoundReport() + + pattern_current_user: re.Pattern = re.compile(".*Текущий пользователь.*") + + th_el = tbody_el.find("th", string=pattern_current_user) + if not th_el: + raise NotFoundReport() + + return th_el.parent + + +def parse_current_user_deviation_hours(html: str) -> tuple[str, str]: + current_user_tr: Tag = get_tr_for_current_user(html) + + # Получение следующего элемента после текущего, у него получение первого ребенка, у которого вытаскивается текст + tr_name: Tag = current_user_tr.find_next_sibling() + name: str = get_text_children(tr_name, idx=0) + + # NOTE: В новом отчете в первой строке указано отклонение + deviation_hours: str = get_text_children(tr_name, idx=7) + if not deviation_hours: # Если старый отчет, то в самом низу + # Получение следующего элемента после текущего, у него получение последнего + # ребенка, у которого вытаскивается текст + # Ищем последнюю строку текущего пользователя -- в ней и находится время работы + # Ее легко найти -- ее первая ячейка пустая + deviation_tr = current_user_tr.find_next_sibling() + + # Ищем строку с пустой ячейкой + while get_text_children(deviation_tr, idx=0): + deviation_tr = deviation_tr.find_next_sibling() + + deviation_hours: str = get_text_children(deviation_tr, idx=-1) + + return name, clear_hours(deviation_hours) + + +def get_user_and_deviation_hours() -> tuple[str, str]: + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.MONTH, + ) + return parse_current_user_deviation_hours(report) + + +def get_quarter_user_and_deviation_hours() -> tuple[str, str]: + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.QUARTER, + ) + return parse_current_user_deviation_hours(report) + + +if __name__ == "__main__": + name, deviation_hours = get_user_and_deviation_hours() + print(name) + print( + ("Недоработка" if deviation_hours[0] == "-" else "Переработка") + + " " + + deviation_hours + ) + """ + Петраш Илья Андреевич + Переработка 04:19:57 + """ + + print() + + name, deviation_hours = get_quarter_user_and_deviation_hours() + print(name) + print( + ("Недоработка" if deviation_hours[0] == "-" else "Переработка") + + " за квартал " + + deviation_hours + ) + """ + Петраш Илья Андреевич + Переработка за квартал 15:09 + """ + + print() + + from get_worklog import get_worklog + + print(get_worklog()) + """ + Worklog(actually='103:05:14', logged='66:35', logged_percent=65) + """ diff --git a/job_compassplus/current_job_report/get_time_spent_in_office.py b/job_compassplus/current_job_report/get_time_spent_in_office.py new file mode 100644 index 000000000..b344c7a91 --- /dev/null +++ b/job_compassplus/current_job_report/get_time_spent_in_office.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from dataclasses import dataclass + +from bs4 import BeautifulSoup + +from utils import session, HOST, NotFoundReport + + +URL = f"{HOST}/pa-reports-new/report/" + + +@dataclass +class TimeSpent: + first_enter: str + today: str + + +def get_time_spent_in_office() -> TimeSpent: + rs = session.get(URL) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + soup = BeautifulSoup(rs.content, "html.parser") + text = soup.get_text(strip=True) + + def _find(pattern: str, about: str) -> str: + if m := re.search(pattern, text, flags=re.IGNORECASE): + return m.group(1) + raise Exception(f"Not found {about!r}") + + return TimeSpent( + first_enter=_find( + pattern=r"First enter:\s*([\d+:]+)", + about="First enter", + ), + today=_find( + pattern=r"Today\s*\(Possible\):\s*([\d+:]+)", + about="Today(Possible)", + ), + ) + + +if __name__ == "__main__": + print(get_time_spent_in_office()) + """ + TimeSpent(first_enter='10:53:30', today='07:31:36') + """ diff --git a/job_compassplus/current_job_report/get_worklog.py b/job_compassplus/current_job_report/get_worklog.py new file mode 100644 index 000000000..5a80ddd21 --- /dev/null +++ b/job_compassplus/current_job_report/get_worklog.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from bs4 import BeautifulSoup +from utils import get_report, ReportTypeEnum, PeriodTypeEnum, NotFoundReport + + +@dataclass +class Worklog: + actually: str # Отработано фактически (чч:мм:сс) + logged: str # Зафиксировано трудозатрат (чч:мм) + logged_percent: int # Процент зафиксированного времени + + @classmethod + def parse_from(cls, data: tuple[str, str, str]) -> "Worklog": + try: + logged_percent: int = int(data[2].replace("%", "")) + except Exception: + raise NotFoundReport() + + return cls( + actually=data[0], + logged=data[1], + logged_percent=logged_percent, + ) + + +def get_worklog() -> Worklog: + report: str = get_report( + report_type=ReportTypeEnum.WORKLOG, + period_type=PeriodTypeEnum.MONTH, + ) + + soup = BeautifulSoup(report, "html.parser") + current_user_tr = soup.select_one("table > tbody > tr.current") + if not current_user_tr: + raise NotFoundReport() + + td_list = current_user_tr.select("td") + return Worklog.parse_from( + ( + # Отработано фактически (чч:мм:сс) + td_list[1].get_text(strip=True), + # Зафиксировано трудозатрат (чч:мм) + td_list[-2].get_text(strip=True), + # Процент зафиксированного времени + td_list[-1].get_text(strip=True), + ) + ) + + +if __name__ == "__main__": + print(get_worklog()) + """ + Worklog(actually='103:05:14', logged='66:35', logged_percent=65) + """ diff --git a/job_compassplus/current_job_report/main.py b/job_compassplus/current_job_report/main.py new file mode 100644 index 000000000..eccf2e245 --- /dev/null +++ b/job_compassplus/current_job_report/main.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import sys +import time +import traceback + +from pathlib import Path +from threading import Thread + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QLabel, + QToolButton, + QPlainTextEdit, + QVBoxLayout, + QHBoxLayout, + QSystemTrayIcon, + QMenu, + QWidgetAction, + QMessageBox, +) +from PyQt5.QtGui import QColor, QPainter, QIcon, QPixmap, QCursor +from PyQt5.QtCore import Qt, pyqtSignal, QThread, QRectF + +from get_hours_worked import ( + get_user_and_deviation_hours, + get_quarter_user_and_deviation_hours, +) +from utils import NotFoundReport, get_quarter_roman + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/405f08fcbf8b99ea64a58a73ee699cb1c0b5230e/qt__pyqt__pyside__pyqode/pyqt__QPainter__dynamic_draw_emoji_on_img/main.py#L44-L66 +def draw_text_to_bottom_right( + img: QPixmap, text: str, scale_text_from_img: float = 0.5 +): + p = QPainter(img) + + factor = (img.width() * scale_text_from_img) / p.fontMetrics().width(text) + if factor < 1 or factor > 1.25: + f = p.font() + point_size = f.pointSizeF() * factor + if point_size > 0: + f.setPointSizeF(point_size) + p.setFont(f) + + # Bottom + right + text_rect = p.fontMetrics().boundingRect(text) + rect = QRectF( + img.width() - text_rect.width(), + img.height() - text_rect.height(), + img.width(), + img.height(), + ) + + p.drawText(rect, text) + + p = None # NOTE: Иначе, почему-то будет ошибка + + +# Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print("Error: ", text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +DIR: Path = Path(__file__).parent.resolve() +TRAY_ICON: str = str(DIR / "favicon.png") + + +class CheckJobReportThread(QThread): + about_new_text = pyqtSignal(str) + about_ok = pyqtSignal(bool) + about_log = pyqtSignal(str) + + def __init__(self) -> None: + super().__init__() + + self.last_text = None + self.ok = None + + def do_run(self) -> None: + def _get_title(deviation_hours): + ok = deviation_hours[0] != "-" + return "Переработка" if ok else "Недоработка" + + today = dt.datetime.today().strftime("%d.%m.%Y %H:%M:%S") + self.about_log.emit(f"Проверка за {today}") + + text = "" + deviation_hours = None + quarter_deviation_hours = None + + try: + name, deviation_hours = get_user_and_deviation_hours() + ok = deviation_hours[0] != "-" + text += name + "\n\n" + _get_title(deviation_hours) + " " + deviation_hours + + _, quarter_deviation_hours = get_quarter_user_and_deviation_hours() + if quarter_deviation_hours.count(":") == 1: + quarter_deviation_hours += ":00" + + text += f"\n{_get_title(quarter_deviation_hours)} за квартал {get_quarter_roman()} {quarter_deviation_hours}" + + except NotFoundReport: + text = "Отчет на сегодня еще не готов." + ok = True + + # Если часы за месяц не готовы, но часы за квартал есть + if not deviation_hours and quarter_deviation_hours: + ok = True + + if self.last_text != text: + self.last_text = text + + text = f"Обновлено {today}\n{self.last_text}" + self.about_new_text.emit(text) + self.about_log.emit(" " + self.last_text + "\n") + else: + self.about_log.emit(" Ничего не изменилось\n") + + self.ok = ok + self.about_ok.emit(self.ok) + + def run(self) -> None: + while True: + try: + # Между 08:00 и 20:00 + now_hour = dt.datetime.now().hour + if now_hour in range(8, 20 + 1): + self.do_run() + time.sleep(3600) + + except Exception as e: + self.about_log.emit(f"Error: {e}") + self.about_log.emit("Wait 60 secs") + time.sleep(60) + + +class JobReportWidget(QWidget): + def __init__(self) -> None: + super().__init__() + + self.info = QLabel() + self.info.setWordWrap(True) + + self.ok: bool | None = None + + self.quit_button = QToolButton() + self.quit_button.setText("Quit") + self.quit_button.setAutoRaise(True) + self.quit_button.clicked.connect(QApplication.instance().quit) + + self.hide_button = QToolButton() + self.hide_button.setText("Hide") + self.hide_button.setAutoRaise(True) + self.hide_button.clicked.connect(lambda x=None: self.parent().hide()) + + self.log = QPlainTextEdit() + self.log.setWindowTitle("Log") + self.log.setMaximumBlockCount(500) + self.log.hide() + + button_visible_log = QToolButton() + button_visible_log.setText("+") + button_visible_log.setToolTip("Show log") + button_visible_log.setAutoRaise(True) + button_visible_log.clicked.connect(self.log.show) + + button_refresh = QToolButton() + button_refresh.setText("🔄") + button_refresh.setToolTip("Refresh") + button_refresh.setAutoRaise(True) + + layout = QVBoxLayout() + layout.setSpacing(0) + + hlayout = QHBoxLayout() + hlayout.addWidget(self.info) + hlayout.addWidget(button_visible_log, alignment=Qt.AlignTop) + layout.addLayout(hlayout) + + layout.addStretch() + + layout_buttons = QHBoxLayout() + layout_buttons.addWidget(button_refresh) + layout_buttons.addStretch() + layout_buttons.addWidget(self.quit_button) + layout_buttons.addWidget(self.hide_button) + + layout.addLayout(layout_buttons) + + self.setLayout(layout) + + self.thread = CheckJobReportThread() + self.thread.about_new_text.connect(self.set_text) + self.thread.about_ok.connect(self._set_ok) + self.thread.about_log.connect(self._add_log) + self.thread.start() + + button_refresh.clicked.connect(self.refresh) + + def set_text(self, text: str) -> None: + print(text) + self.info.setText(text) + + def refresh(self) -> None: + # Выполнение метода в отдельном потоке, а не в GUI + Thread(target=self.thread.do_run, daemon=True).start() + + def _set_ok(self, val: bool) -> None: + self.ok = val + self.update() + + def _add_log(self, val: str) -> None: + print(val) + self.log.appendPlainText(val) + + def paintEvent(self, event) -> None: + super().paintEvent(event) + + if self.ok is None: + return + + color = QColor("#29AB87") if self.ok else QColor(255, 0, 0, 128) + + painter = QPainter(self) + painter.setBrush(color) + painter.setPen(color) + painter.drawRect(self.rect()) + + +if __name__ == "__main__": + app = QApplication([]) + app.setQuitOnLastWindowClosed(False) + + tray = QSystemTrayIcon(QIcon(TRAY_ICON)) + + def _on_about_log_or_ok(value: str | bool) -> None: + if isinstance(value, str): + text = "🔄" + else: + text = "✅" if value else "⛔" + + img = QPixmap(TRAY_ICON) + draw_text_to_bottom_right(img, text, 0.65) + tray.setIcon(QIcon(img)) + + job_report_widget = JobReportWidget() + job_report_widget.thread.about_log.connect(_on_about_log_or_ok) + job_report_widget.thread.about_ok.connect(_on_about_log_or_ok) + job_report_widget.refresh() + + job_report_widget_action = QWidgetAction(job_report_widget) + job_report_widget_action.setDefaultWidget(job_report_widget) + + menu = QMenu() + menu.addAction(job_report_widget_action) + + tray.setContextMenu(menu) + tray.activated.connect( + lambda _: ( + tray.contextMenu().resize(job_report_widget.sizeHint()), + tray.contextMenu().popup(QCursor.pos()), + ) + ) + + tray.setToolTip("Compass Plus. Рапорт учета рабочего времени") + tray.show() + + app.exec() diff --git a/job_compassplus/current_job_report/utils.py b/job_compassplus/current_job_report/utils.py new file mode 100644 index 000000000..ede069598 --- /dev/null +++ b/job_compassplus/current_job_report/utils.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import re +import sys + +from datetime import datetime +from pathlib import Path + +from bs4 import BeautifulSoup + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from root_config import JIRA_HOST as HOST +from root_common import session + +sys.path.append(str(DIR.parent.parent)) +from get_quarter import ( + get_quarter_num, + get_quarter_roman, # NOTE: оставить get_quarter_roman для main.py +) + + +@enum.unique +class PeriodTypeEnum(enum.IntEnum): + MONTH = 0 + QUARTER = 1 + PERIOD = 2 + + +@enum.unique +class ReportTypeEnum(enum.IntEnum): + WORKLOG = 5 # Отчет по зафиксированным трудозатратам + SUMMARY = 6 # Сводный отчет + + +class NotFoundReport(Exception): + pass + + +URL: str = f"{HOST}/pa-reports-new/report/" + + +def clear_hours(hours: str) -> str: + return re.sub(r"[^\d:-]", "", hours) + + +def _send_data(data: dict[str, str | int]) -> str: + # В какой-то момент адрес временно поменялся, тогда предварительный GET поможет получить актуальный адрес + rs = session.get(URL) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + # Добавление полей, типа токенов, если явно не были заданы + soup = BeautifulSoup(rs.content, "html.parser") + for el in soup.select('input[name][value][type="hidden"]'): + name: str = el["name"] + if name not in data: + data[name] = el["value"] + + rs = session.post(rs.url, data=data) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + return rs.text + + +def get_report(report_type: ReportTypeEnum, period_type: PeriodTypeEnum) -> str: + today = datetime.today() + + data = { + "reporttype": report_type.value, + "PeriodType": period_type.value, + "Month": today.month, + "Year": today.year, + "QuarterNum": get_quarter_num(today) - 1, # NOTE: I квартал в отчете это 0 + "FromMonth": 1, + "ToMonth": 12, + } + + match report_type: + case PeriodTypeEnum.QUARTER: + data["quarter"] = "quarter" + case PeriodTypeEnum.PERIOD: + data["total"] = "total" + + return _send_data(data) + + +if __name__ == "__main__": + dt = datetime.now() + print(dt, get_quarter_num(dt), get_quarter_roman(dt)) + + print() + + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.MONTH, + ) + soup = BeautifulSoup(report, "html.parser") + for tr in soup.select("table.report tr:has(> td)"): + print([td.get_text(strip=True) for td in tr.select("td")]) diff --git a/job_compassplus/cyr_lat_audit.py b/job_compassplus/cyr_lat_audit.py new file mode 100644 index 000000000..f85977092 --- /dev/null +++ b/job_compassplus/cyr_lat_audit.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import argparse +import re + +from collections import Counter +from datetime import datetime +from pathlib import Path + + +def check_mixed_layout(text: str) -> list[tuple[str, list[str]]]: + words: list[str] = re.findall(r"\b[a-zA-Zа-яА-ЯёЁ]+\b", text) + corrupted_words: list[tuple[str, list[str]]] = [] + + for word in words: + has_lat: bool = bool(re.search(r"[a-zA-Z]", word)) + has_cyr: bool = bool(re.search(r"[а-яА-ЯёЁ]", word)) + + if has_lat and has_cyr: + cyr_letters: list[str] = re.findall(r"[а-яА-ЯёЁ]", word) + corrupted_words.append((word, cyr_letters)) + + return corrupted_words + + +def process( + path: Path, + extensions: set[str], + ignore_words: set[str], + ignore_files: set[str], + ignore_dirs: set[str], +) -> None: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Path {str(path)!r} must be a directory") + + total_files_count: int = 0 + checked_files_count: int = 0 + corrupted_files_count: int = 0 + corrupted_words_count: int = 0 + ignored_words_count: int = 0 + + suffix_total_counter: dict[str, int] = Counter() + suffix_checked_counter: dict[str, int] = Counter() + suffix_corrupted_files_counter: dict[str, int] = Counter() + suffix_corrupted_words_counter: dict[str, int] = Counter() + + start_dt: datetime = datetime.now() + + for f in path.rglob("*.*"): + if not f.is_file() or f.name.lower() in ignore_files: + continue + + has_any: bool = any( + parent_dir.name.lower() in ignore_dirs for parent_dir in f.parents + ) + if has_any: + continue + + total_files_count += 1 + suffix_total_counter[f.suffix] += 1 + + if f.suffix not in extensions: + continue + + try: + checked_files_count += 1 + suffix_checked_counter[f.suffix] += 1 + + text: str = f.read_text("utf-8") + corrupted_words: list[tuple[str, list[str]]] = check_mixed_layout(text) + if corrupted_words: + corrupted_files_count += 1 + suffix_corrupted_files_counter[f.suffix] += 1 + + valid_corrupted_words: list[tuple[str, list[str]]] = [] + for word, cyr_letters in corrupted_words: + if word.lower() in ignore_words: + ignored_words_count += 1 + else: + valid_corrupted_words.append((word, cyr_letters)) + + if valid_corrupted_words: + corrupted_words_count += len(valid_corrupted_words) + suffix_corrupted_words_counter[f.suffix] += len( + valid_corrupted_words + ) + + print(f"File: {str(f)!r}") + for word, cyr_letters in valid_corrupted_words: + print(f" Word: {word!r}, letters: {cyr_letters}") + print() + + except UnicodeError as e: + print(f"{f} skip, error: {e}") + + print(f"Elapsed: {datetime.now() - start_dt}") + print() + + print("=== SUMMARY STATISTICS ===") + print(f"Total files found: {total_files_count}") + print(f"Total checked files: {checked_files_count}") + print(f"Files with mixed layout: {corrupted_files_count}") + print(f"Words with mixed layout: {corrupted_words_count}") + print(f"Ignored words: {ignored_words_count}") + print() + print(f"All extensions in path: {dict(suffix_total_counter)}") + print(f"Checked extensions: {dict(suffix_checked_counter)}") + print(f"Corrupted files by suffix: {dict(suffix_corrupted_files_counter)}") + print(f"Corrupted words by suffix: {dict(suffix_corrupted_words_counter)}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Cyrillic and Latin alphabet audit", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "path", + metavar="/PATH/TO/DIRECTORY", + type=Path, + help="Path to directory", + ) + parser.add_argument( + "-e", + "--ext", + nargs="+", + default=[".xml", ".java", ".xsd"], + help="File extensions to check", + ) + parser.add_argument( + "-iw", + "--ignore-words", + nargs="+", + default=[], + help="Words to ignore (case-insensitive)", + ) + parser.add_argument( + "-if", + "--ignore-files", + nargs="+", + default=[], + help="Files to ignore (case-insensitive)", + ) + parser.add_argument( + "-id", + "--ignore-dirs", + nargs="+", + default=["build"], + help="Directories to ignore (case-insensitive)", + ) + args = parser.parse_args() + + target_extensions: set[str] = { + ex if ex.startswith(".") else f".{ex}" for ex in args.ext + } + target_ignore_words: set[str] = {word.lower() for word in args.ignore_words} + target_ignore_files: set[str] = {word.lower() for word in args.ignore_files} + target_ignore_dirs: set[str] = {word.lower() for word in args.ignore_dirs} + + process( + path=args.path, + extensions=target_extensions, + ignore_words=target_ignore_words, + ignore_files=target_ignore_files, + ignore_dirs=target_ignore_dirs, + ) diff --git a/job_compassplus/designer/common.py b/job_compassplus/designer/common.py new file mode 100644 index 000000000..ae63b4ff4 --- /dev/null +++ b/job_compassplus/designer/common.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + +from dataclasses import dataclass, field +from pathlib import Path + + +PATTERN: re.Pattern = re.compile( + r"\* \[(?P\w+)\] (?P\d+) - (?P[^\n]+)\n\s*at\s*(?P[^\n]+)" +) + + +PATH_PROBLEMS_TAB: Path = Path(__file__).parent / "problems_tab.txt" + + +@dataclass +class Problem: + severity: str + code: int + error: str + at: str + maintainers: list[str] = field(default_factory=list) + + +def parse_text(text: str) -> list[Problem]: + items: list[Problem] = [] + + for m in PATTERN.finditer(text): + maintainers: list[str] = [] + for brackets in re.findall(r"\[(.*?)]", m.group()): + if "@" in brackets: + maintainers += [ + email.split("@")[0] + for email in brackets.split(", ") + ] + + items.append( + Problem( + severity=m.group("severity"), + code=int(m.group("code")), + error=m.group("error"), + at=m.group("at"), + maintainers=maintainers, + ) + ) + + return items + + +if __name__ == "__main__": + from pathlib import Path + + text = PATH_PROBLEMS_TAB.read_text("utf-8") + problems: list[Problem] = parse_text(text) + + print(f"Problems ({len(problems)}):") + for i, p in enumerate(problems, 1): + print(f"{i}. {p}") diff --git a/job_compassplus/designer/parse_problems_tab__code_by_number.py b/job_compassplus/designer/parse_problems_tab__code_by_number.py new file mode 100644 index 000000000..d28eb8535 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__code_by_number.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import PATH_PROBLEMS_TAB, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +total: int = 0 +code_by_number: dict[int, int] = dict() + +for p in parse_text(text): + if p.code not in code_by_number: + code_by_number[p.code] = 0 + code_by_number[p.code] += 1 + + total += 1 + +print(f"Code ({len(code_by_number)}):") +for code, number in sorted(code_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" {code}: {number}") +print(f"Total: {total}") +""" +Code (4): + 51: 4 + 137: 3 + 132: 2 + 999: 1 +Total: 10 +""" diff --git a/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py b/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py new file mode 100644 index 000000000..4fb387851 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import PATH_PROBLEMS_TAB, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +# TODO: +filter_by_codes: list[int] = [ + # 127, 137 +] + +user_by_number: dict[str, int] = dict() + +for p in parse_text(text): + if filter_by_codes and p.code not in filter_by_codes: + continue + + users = p.maintainers.copy() + if not users: + users.append("") + + for user in users: + if user not in user_by_number: + user_by_number[user] = 0 + user_by_number[user] += 1 + +print(f"Users ({len(user_by_number)}):") +for user, number in sorted(user_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" {user!r}: {number}") +""" +Users (3): + 'bar': 7 + 'foo': 3 + '': 2 +""" diff --git a/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py b/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py new file mode 100644 index 000000000..893dc9167 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from common import PATH_PROBLEMS_TAB, Problem, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +# TODO: +filter_by_codes: list[int] = [ + # 127, 137 +] +print(f"Filter by codes: {filter_by_codes}") + +user_by_number_details: dict[str, list[Problem]] = defaultdict(list) +code_by_number: dict[int, int] = defaultdict(int) + +for p in parse_text(text): + code: int = p.code + if filter_by_codes and code not in filter_by_codes: + continue + + code_by_number[code] += 1 + + users = p.maintainers.copy() + if not users: + users.append("") + + user_by_number_details[", ".join(users)].append(p) + +print(f"Total users: {len(user_by_number_details)}") +print(f"Stats (codes {len(code_by_number)}, total {sum(code_by_number.values())}):") +for code, number in sorted(code_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" Code {code}: {number}") +print() + +INDENT_1: str = " " + +for user, all_problems in sorted( + user_by_number_details.items(), + key=lambda x: len(x[1]), + reverse=True, +): + print(f"{user!r} ({len(all_problems)}):") + + code_by_problems: dict[int, list[Problem]] = defaultdict(list) + for p in all_problems: + code_by_problems[p.code].append(p) + + for code, problems in code_by_problems.items(): + print(f"{INDENT_1}Code {code} ({len(problems)}):") + + for p in problems: + print(f"{INDENT_1 * 2}[{p.severity}] {p.error} at {p.at}") + + print() + +""" +Filter by codes: [] +Total users: 4 +Stats (codes 4, total 10): + Code 51: 4 + Code 137: 3 + Code 132: 2 + Code 999: 1 + +'bar' (5): + Code 137 (1): + [NOTE] XXX [bar@example.com] [Fix available] at ABC::Common::Foo::Bar + Code 51 (4): + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + +'' (2): + Code 132 (2): + [NOTE] Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] at ABC::FOO::BAR:General + [NOTE] Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] at ABC::FOO::BAR:General + +'foo, bar' (2): + Code 137 (1): + [NOTE] XXX [foo@example.com, bar@example.com] [Fix available] at ABC::Common::Foo::Bar + Code 999 (1): + [ERROR] XXX [foo@example.com, bar@example.com] at ABC::Common::Foo::Bar + +'foo' (1): + Code 137 (1): + [NOTE] XXX [foo@example.com] [Fix available] at ABC::Interfacing.Foo::Bar +""" diff --git a/job_compassplus/designer/problems_tab.txt b/job_compassplus/designer/problems_tab.txt new file mode 100644 index 000000000..5d2e25805 --- /dev/null +++ b/job_compassplus/designer/problems_tab.txt @@ -0,0 +1,29 @@ +Cursor Class 'ABC::Interfacing.Foo::Bar' +* [NOTE] 137 - XXX [foo@example.com] [Fix available] + at ABC::Interfacing.Foo::Bar + +Statement Class 'ABC::Common::Foo::Bar' +* [NOTE] 137 - XXX [bar@example.com] [Fix available] + at ABC::Common::Foo::Bar + +User Method 'ABC::Common::Foo:Model:Bar' +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar + +Editor Page 'ABC::FOO::BAR:General' +* [NOTE] 132 - Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] + at ABC::FOO::BAR:General +* [NOTE] 132 - Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] + at ABC::FOO::BAR:General + +Statement Class 'ABC::Common::Foo::Bar' +* [NOTE] 137 - XXX [foo@example.com, bar@example.com] [Fix available] + at ABC::Common::Foo::Bar +* [ERROR] 999 - XXX [foo@example.com, bar@example.com] + at ABC::Common::Foo::Bar diff --git a/job_compassplus/doc_vs_docx.py b/job_compassplus/doc_vs_docx.py new file mode 100644 index 000000000..d74a1bd0b --- /dev/null +++ b/job_compassplus/doc_vs_docx.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install python-docx==1.1.0 +from docx import Document + + +PATH_DIR: str = r"C:\DOC\Specifications\Interfaces" + + +docx_num = 0 +doc_num = 0 +for f in Path(PATH_DIR).glob("*.doc*"): + if f.name.startswith("~"): + continue + + try: + Document(f) + ok = True + docx_num += 1 + except: + ok = False + doc_num += 1 + + print(f"[{'+' if ok else '-'}]", f) + +print() +print("Total:", docx_num + doc_num) +print("Docx:", docx_num) +print("Doc:", doc_num) diff --git a/job_compassplus/find_duplicated_specs.py b/job_compassplus/find_duplicated_specs.py new file mode 100644 index 000000000..374129698 --- /dev/null +++ b/job_compassplus/find_duplicated_specs.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from pathlib import Path +from zlib import crc32 + + +DIR = Path(r"C:\DOC\W4") + +crc_by_files: dict[int, list[Path]] = defaultdict(list) +for f in DIR.rglob("*"): + if not f.is_file(): + continue + + crc = crc32(f.read_bytes()) + crc_by_files[crc].append(f) + +for files in crc_by_files.values(): + if len(files) == 1: + continue + + print(*files, sep="\n") + print() diff --git a/job_compassplus/find_invalid_utf_8_files_java.py b/job_compassplus/find_invalid_utf_8_files_java.py new file mode 100644 index 000000000..3deca20d0 --- /dev/null +++ b/job_compassplus/find_invalid_utf_8_files_java.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +def process(path: str): + if isinstance(path, str): + path = Path(path) + + if not path.exists(): + raise Exception(f"Not exists: {path}") + + for file in path.rglob("*.java"): + try: + file.read_text("utf-8") + except UnicodeError as e: + print(f"{file}, error: {e}") + for line in file.read_bytes().splitlines(): + try: + str(line, "utf-8") + except UnicodeError: + print(f" Line: {line}") + + print() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Find invalid utf-8 files java" + ) + parser.add_argument( + "path", + metavar="/PATH/TO", + help="Path to source tree", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/jira_avatars/get_avatars.py b/job_compassplus/jira_avatars/get_avatars.py new file mode 100644 index 000000000..a714a0cde --- /dev/null +++ b/job_compassplus/jira_avatars/get_avatars.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Self + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +# Examples: +""" +{ + "system": [ + {"id":"10122","isSystemAvatar":true,"isSelected":false,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=10122","24x24":"https://.../useravatar?size=small&avatarId=10122","16x16":"https://.../useravatar?size=xsmall&avatarId=10122","32x32":"https://.../useravatar?size=medium&avatarId=10122"},"selected":false}, + ... + {"id":"17241","isSystemAvatar":true,"isSelected":true,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=17241","24x24":"https://.../useravatar?size=small&avatarId=17241","16x16":"https://.../useravatar?size=xsmall&avatarId=17241","32x32":"https://.../useravatar?size=medium&avatarId=17241"},"selected":true}, + ... + {"id":"17252","isSystemAvatar":true,"isSelected":false,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=17252","24x24":"https://.../useravatar?size=small&avatarId=17252","16x16":"https://.../useravatar?size=xsmall&avatarId=17252","32x32":"https://.../useravatar?size=medium&avatarId=17252"},"selected":false}], + "custom": [] +} +""" + + +@dataclass(frozen=True) +class AvatarUrls: + size_48x48: str + size_32x32: str + size_24x24: str + size_16x16: str + + @classmethod + def from_dict(cls, data: dict[str, str]) -> Self: + return cls( + size_48x48=data.get("48x48"), + size_24x24=data.get("24x24"), + size_32x32=data.get("32x32"), + size_16x16=data.get("16x16"), + ) + + +@dataclass(frozen=True) +class JiraAvatar: + id: str + is_system_avatar: bool + is_selected: bool + is_deletable: bool + urls: AvatarUrls + selected: bool # NOTE: Дублирует isSelected + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + return cls( + id=data["id"], + is_system_avatar=data["isSystemAvatar"], + is_selected=data["isSelected"], + is_deletable=data["isDeletable"], + urls=AvatarUrls.from_dict(data["urls"]), + selected=data["selected"], + ) + + +@dataclass(frozen=True) +class JiraAvatarResponse: + system: list[JiraAvatar] + custom: list[JiraAvatar] + + @classmethod + def from_dict(cls, data: dict[str, list[dict[str, Any]]]) -> Self: + def _get_avatars(key: str) -> list[JiraAvatar]: + return [JiraAvatar.from_dict(d) for d in data[key]] + + return cls( + system=_get_avatars("system"), + custom=_get_avatars("custom"), + ) + + +def get_avatars(username: str) -> JiraAvatarResponse: + url = f"{JIRA_HOST}/rest/api/latest/user/avatars?username={username}" + + rs = session.get(url) + rs.raise_for_status() + + return JiraAvatarResponse.from_dict(rs.json()) + + +if __name__ == "__main__": + rs: JiraAvatarResponse = get_avatars("ipetrash") + + print(f"System ({len(rs.system)}):") + for i, avatar in enumerate(rs.system, 1): + print(f" {i}. {avatar}") + + print() + + print(f"Custom ({len(rs.custom)}):") + for i, avatar in enumerate(rs.custom, 1): + print(f" {i}. {avatar}") diff --git a/job_compassplus/jira_dump_issues.py b/job_compassplus/jira_dump_issues.py new file mode 100644 index 000000000..13a3008dc --- /dev/null +++ b/job_compassplus/jira_dump_issues.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import sqlite3 + +from pathlib import Path +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +DIR: Path = Path(__file__).resolve().parent +FILE_NAME_DB: Path = DIR / "jira_local.sqlite" + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + + +def init_sqlite(filename: Path) -> sqlite3.Connection: + conn = sqlite3.connect(filename) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS Task ( + id INTEGER PRIMARY KEY, + key TEXT, + summary TEXT, + description TEXT, + status TEXT, + issue_type TEXT, + created TEXT, + is_indexed INTEGER DEFAULT 0 + ) + """ + ) + conn.commit() + return conn + + +def get_last_id(conn: sqlite3.Connection, project: str) -> int: + res = conn.execute( + "SELECT MAX(id) FROM Task WHERE key LIKE ?", (f"{project}-%",) + ).fetchone() + return res[0] if res[0] else 0 + + +def sync_jira_to_sqlite(conn: sqlite3.Connection, projects: list[str]): + max_results: int = 100 + + for project in projects: + last_id: int = get_last_id(conn, project) + + while True: + print(f"[*] Поиск задач в Jira проекта {projects} от ID > {last_id}...") + + filter_id: str = f"AND id > {last_id}" if last_id > 0 else "" + jql: str = f"project = {project} {filter_id} ORDER BY id ASC" + + query: dict[str, Any] = { + "jql": jql, + "fields": "key,summary,description,status,issuetype,created", + "maxResults": max_results, + } + + rs = session.get(URL_SEARCH, params=query) + try: + rs.raise_for_status() + except Exception as e: + print(rs.json()) + raise e + + issues: list[dict] = rs.json().get("issues") + if not issues: + print("Больше нет задач") + break + + for issue in issues: + print(issue) + + issue_id: int = int(issue["id"]) + issue_key: str = issue["key"] + issue_summary: str = issue["fields"].get("summary", "") + issue_desc: str = str(issue["fields"].get("description")) + issue_status: str = issue["fields"]["status"]["name"] + issue_type: str = issue["fields"]["issuetype"]["name"] + issue_created: str = issue["fields"]["created"] + + conn.execute( + """ + INSERT OR IGNORE INTO Task + (id, key, summary, description, status, issue_type, created) + VALUES + (?, ?, ?, ?, ?, ?, ?) + """, + ( + issue_id, + issue_key, + issue_summary, + issue_desc, + issue_status, + issue_type, + issue_created, + ), + ) + + last_id = issue_id + + conn.commit() + + print(f"[+] Загружено {len(issues)} задач...") + + time.sleep(5) + + +if __name__ == "__main__": + # TODO: Возможность задавать аргументом + projects: list[str] = [ + # "OPTT", + "TXI", + "TWRBS", + "TXCORE", + "TXACQ", + ] + + # TODO: Возможность задавать аргументом путь к БД + # FILE_NAME_DB: Path = DIR / "jira_local_OPTT.sqlite" + + db_conn = init_sqlite(FILE_NAME_DB) + sync_jira_to_sqlite(db_conn, projects) diff --git a/job_compassplus/jira_get_current_user.py b/job_compassplus/jira_get_current_user.py new file mode 100644 index 000000000..32078bc7f --- /dev/null +++ b/job_compassplus/jira_get_current_user.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +URL_API_MYSELF = f"{JIRA_HOST}/rest/api/latest/myself" + + +def get_current_user() -> dict[str, Any]: + rs = session.get(URL_API_MYSELF) + rs.raise_for_status() + + return rs.json() + + +if __name__ == "__main__": + user = get_current_user() + print("Current user:", user) + print("Current user name:", user["name"]) + print() + + import json + print(f"Current user (pretty):\n{json.dumps(user, indent=4)}") diff --git a/job_compassplus/jira_get_total_resolved.py b/job_compassplus/jira_get_total_resolved.py new file mode 100644 index 000000000..87eb9fcaa --- /dev/null +++ b/job_compassplus/jira_get_total_resolved.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from dataclasses import dataclass + +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + +JQL_RESOLUTION_DATE = "assignee = currentUser() AND resolutiondate" +JQL_TOTAL = f"{JQL_RESOLUTION_DATE} IS NOT EMPTY" +JQL_LAST_WEEK = f"{JQL_RESOLUTION_DATE} >= startOfDay(-7)" +JQL_LAST_MONTH = f"{JQL_RESOLUTION_DATE} >= startOfMonth(-1)" +JQL_LAST_YEAR = f"{JQL_RESOLUTION_DATE} >= startOfMonth(-12)" + + +@dataclass +class Stats: + last_7_days: int + last_month: int + last_year: int + total: int + + +def get_total(jql: str) -> int: + query = { + "jql": jql, + "fields": "key", + "maxResults": 0, + } + + rs = session.get(URL_SEARCH, params=query) + rs.raise_for_status() + + return rs.json()["total"] + + +def get_stats(sleep: float = 0.5) -> Stats: + last_7_days = get_total(JQL_LAST_WEEK) + time.sleep(sleep) + + last_month = get_total(JQL_LAST_MONTH) + time.sleep(sleep) + + last_year = get_total(JQL_LAST_YEAR) + time.sleep(sleep) + + total = get_total(JQL_TOTAL) + + return Stats( + last_7_days=last_7_days, + last_month=last_month, + last_year=last_year, + total=total, + ) + + +if __name__ == "__main__": + print(get_stats()) diff --git a/job_compassplus/jira_search_by_avatar/main.py b/job_compassplus/jira_search_by_avatar/main.py new file mode 100644 index 000000000..a080f57e3 --- /dev/null +++ b/job_compassplus/jira_search_by_avatar/main.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +URL_PROFILE_FORMAT = f"{JIRA_HOST}/secure/ViewProfile.jspa?name={{name}}" +URL_USER_SEARCH = f"{JIRA_HOST}/rest/api/latest/user/search" + + +def search_by_avatar(avatar_id: int) -> list[dict]: + start_at = 0 + max_results = 500 + + avatar_id = str(avatar_id) + + items = [] + while True: + params = { + "username": ".", + "startAt": start_at, + "maxResults": max_results, + } + + rs = session.get(URL_USER_SEARCH, params=params) + rs.raise_for_status() + + result = rs.json() + if not result: + break + + for user in result: + if avatar_id in user["avatarUrls"]["48x48"]: + items.append(user) + + time.sleep(5) + + start_at += max_results + + return items + + +if __name__ == "__main__": + avatar_id = 17250 # pirate + users = search_by_avatar(avatar_id) + print(f"Users ({len(users)}):") + for i, user in enumerate(users, 1): + display_name = user["displayName"] + user_url = URL_PROFILE_FORMAT.format(name=user["key"]) + print(f"{i}. '{display_name}': {user_url}") diff --git a/job_compassplus/jira_show_last_issue/main.py b/job_compassplus/jira_show_last_issue/main.py new file mode 100644 index 000000000..062ee8a21 --- /dev/null +++ b/job_compassplus/jira_show_last_issue/main.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + + +def get_last_issue_key(project: str) -> str | None: + query = { + "jql": f"project={project} ORDER BY created DESC", + "fields": "key", + "maxResults": 1, + } + + rs = session.get(URL_SEARCH, params=query) + if rs.status_code == 400: + return + + rs.raise_for_status() + + try: + return rs.json()["issues"][0]["key"] + except Exception: + return + + +if __name__ == "__main__": + import time + + for project in [ + "OPTT", + "NOT_FOUND", + "RADIX", + "TXI", + "TXACQ", + "TXCORE", + "TXISS", + "TXPG", + "TWO", + "FLORA", + ]: + last_issue_key: str | None = get_last_issue_key(project=project) + print(f"{project}: {last_issue_key if last_issue_key else '-'}", ) + time.sleep(0.5) diff --git a/job_compassplus/jira_show_last_issue/notify_me.py b/job_compassplus/jira_show_last_issue/notify_me.py new file mode 100644 index 000000000..35401d6d2 --- /dev/null +++ b/job_compassplus/jira_show_last_issue/notify_me.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from datetime import datetime + +from tkinter.messagebox import showinfo + +from main import get_last_issue_key + + +NEED_PROJECT: str = "OPTT" +NEED_ISSUE_ID: int = 999 + + +while True: + last_issue_key: str = get_last_issue_key(NEED_PROJECT) + print(f'[{datetime.now():%d/%m/%Y %H:%M:%S}] last_issue_key: {last_issue_key}') + + # NOTE: "OPTT-1234" -> 1234 + issue_id: int = int(last_issue_key.split("-")[-1]) + if issue_id >= NEED_ISSUE_ID: + showinfo(title="Информация", message=f"Появилась задача {last_issue_key}!") + + time.sleep(30) diff --git a/job_compassplus/jira_sprint_get_total_overtime_hours.py b/job_compassplus/jira_sprint_get_total_overtime_hours.py new file mode 100644 index 000000000..4c7b48ccd --- /dev/null +++ b/job_compassplus/jira_sprint_get_total_overtime_hours.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from dataclasses import dataclass +from datetime import datetime + +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + +FIELD_OVERTIME_HOURS = "customfield_13440" + +QUERY = { + "jql": ( + "assignee = currentUser()" + " AND project = Sprint AND type = Sub-task" + " AND created >= startOfYear() AND created <= endOfYear()" + " ORDER BY created DESC" + ), + "fields": f"key,created,{FIELD_OVERTIME_HOURS}", +} + + +@dataclass +class Sprint: + key: str + created: datetime + overtime_hours: int + + +default_handler = logging.StreamHandler(stream=sys.stdout) +default_handler.setFormatter( + logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) +) +logger = logging.getLogger("jira_sprint_get_total_overtime_hours") +logger.setLevel(logging.WARNING) +logger.addHandler(default_handler) + + +def get_sprints_with_overtime_hours() -> list[Sprint]: + logger.debug(f"Load: {URL_SEARCH}") + + rs = session.get(URL_SEARCH, params=QUERY) + logger.debug(f"Response: {rs}") + rs.raise_for_status() + + items: list[Sprint] = [] + + issues = rs.json()["issues"] + logger.info(f"Total issues: {len(issues)}") + + for issue in issues: + key = issue["key"] + created_str = issue["fields"]["created"] + + overtime_hours = issue["fields"][FIELD_OVERTIME_HOURS] + overtime_hours: int = int(overtime_hours) if overtime_hours else 0 + + logger.info(f"Issue: {key}, created_str: {created_str}, overtime hours: {overtime_hours}") + + items.append( + Sprint( + key=key, + created=datetime.strptime(created_str, "%Y-%m-%dT%H:%M:%S.%f%z"), + overtime_hours=overtime_hours, + ) + ) + + return items + + +if __name__ == "__main__": + # NOTE: Debug + # logger.setLevel(logging.DEBUG) + + total_overtime_hours = 0 + sprints = get_sprints_with_overtime_hours() + print(f"Sprints ({len(sprints)}):") + for i, sprint in enumerate(sprints, 1): + print(f" {i}. {sprint}") + total_overtime_hours += sprint.overtime_hours + + print() + print(f"Total overtime hours: {total_overtime_hours}") diff --git a/job_compassplus/merge_tc33a_files.py b/job_compassplus/merge_tc33a_files.py new file mode 100644 index 000000000..7c6fbe9fb --- /dev/null +++ b/job_compassplus/merge_tc33a_files.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +DIR = Path(__file__).parent.resolve() +path_output = DIR / "tc33a-all-files.txt" + +with open(path_output, "w", encoding="ascii") as f_out: + path_dir = Path(r"C:\DOC\Visa спецификации\Base II TC33A\TC33_Capture_Transaction") + for f in path_dir.rglob("*.*"): + if not f.is_file(): + print(f"[#] Не файл: {str(f)!r}") + continue + + if any( + name in str(f) + for name in [ + # NOTE: Замечены файлы с кривым форматом, типа дат 222222 и 123456. + # В задаче не было таких, вероятно кривой пример + "TWICH-4378", + # NOTE: Файл был дополнен, переделан и отдельно занесен в тест + "TC33_CaptureTran_All_CP_Test.ctf", + # NOTE: Была ошибка при парсинге, постфикс имени файла вызывает сомнения + "TC33_CaptureTran_All_CP_Test (No CP07, CP05 TCR7)_VISA_err.ctf", + ] + ): + print(f"[#] Пропуск файла: {str(f)!r}") + continue + + print(f) + print(f.relative_to(path_dir)) + + try: + text: str = f.read_text("ascii") + print("Длина:", len(text)) + + first_line_length = len(text.splitlines()[0]) + print("Длина первой строки:", first_line_length) + if first_line_length != 168: + print("[#] Не CTF, пропуск файла") + continue + + # NOTE: Кривой формат даты + if "_MC" in str(f) and "CP12" in text and "12345678 " in text: + print(f"[#] Замена кривой даты в {str(f)!r}") + text = text.replace("12345678 ", "1030140815") + + if "inctf TC33A.EPIN.txt" in str(f) and "262012" in text: + print(f"[#] Замена кривой даты в {str(f)!r}") + text = text.replace("262012", "261220") + + lines = text.splitlines() + for i, line in enumerate(lines): + if line.startswith("3301") and re.search("^3300.+?CP06", lines[i - 1]): + print( + f"[#] Комментирование строки CP06 TCR1 - не поддерживается запись" + ) + lines[i] = f"# NOTE: Not supported: {line}" + + elif line.startswith("3309170"): + print( + f"[#] Комментирование строки CP01 TCR9 - не поддерживается запись" + ) + lines[i] = f"# NOTE: Not supported: {line}" + + text = "\n".join(lines) + + f_out.write(f"# START FILE: {f.relative_to(path_dir)}\n") + f_out.write(text) + f_out.write(f"\n# END FILE: {f.relative_to(path_dir)}\n\n") + + except UnicodeDecodeError as e: + print("[#]", e) + + print() diff --git a/job_compassplus/mysite_compassplus_com/common.py b/job_compassplus/mysite_compassplus_com/common.py new file mode 100644 index 000000000..147655507 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/common.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path +from typing import Any + +from bs4 import Tag + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post +from root_config import JIRA_HOST + + +URL_BASE = "https://mysite.compassplus.com" +URL = f"{URL_BASE}/Person.aspx?accountname={{}}" + + +def get_profile_organization(full_username: str) -> dict[str, Any]: + url = f"{URL_BASE}/_vti_bin/SilverlightProfileService.json/GetUserSLProfileData" + rs = do_post(url, json={"AccountNames": [full_username]}) + return rs.json() + + +def is_active_profile(full_username: str) -> bool: + organization = get_profile_organization(full_username) + data = organization["d"][0] + # Если хотя бы один из них имеет значение, отличное от [] или None + return bool(data["Parent"] or data["Siblings"] or data["Children"]) + + +def get_jira_user_active(username: str) -> bool: + url = f"{JIRA_HOST}/rest/api/latest/user?username={username}" + return do_get(url).json()["active"] + + +def get_text(el: Tag) -> str: + return el.get_text(strip=True) + + +if __name__ == "__main__": + full_username = r"CP\ipetrash" + + rs = do_get(URL.format(full_username)) + print(rs) + + organization = get_profile_organization(full_username) + print(organization) + + data = organization["d"][0] + print("Parent:", data["Parent"]) + print("Siblings:", data["Siblings"]) + print("Children:", data["Children"]) + print() + + print("Is active (mysite):", is_active_profile(full_username)) + print("Is active (jira):", get_jira_user_active(full_username.rsplit("\\")[-1])) diff --git a/job_compassplus/mysite_compassplus_com/get_all_department_employees.py b/job_compassplus/mysite_compassplus_com/get_all_department_employees.py new file mode 100644 index 000000000..94cf26894 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_all_department_employees.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from bs4 import BeautifulSoup +from common import URL, do_get, get_text + + +@dataclass +class Employee: + full_name: str + user_name: str + position: str + url: str + + +def get_employees(boss_username: str) -> list[Employee]: + url: str = URL.format(boss_username) + print("Load", url) + + rs = do_get(url) + + root = BeautifulSoup(rs.content, "html.parser") + root_table = root.select_one("#ReportingHierarchy") + + items = [] + + for td in root_table.select(".ms-orgname"): + a = td.a + url = a["href"] + items.append( + Employee( + full_name=get_text(a), + # Example: "https://.../Person.aspx?accountname=CP%5Cipetrash" -> "ipetrash" + user_name=url.split("%5C")[-1], + url=url, + position=get_text(td.select_one(".ms_metadata")), + ) + ) + + return items + + +if __name__ == "__main__": + from base64 import b64decode + + for boss_username in [ + b64decode("Q1BcbnZheW5lcg==").decode("utf-8"), + b64decode("Q1BcYXZvc3RyaWtvdg==").decode("utf-8"), + b64decode("Q1BceXJlbWl6b3Y=").decode("utf-8"), + ]: + print(f"Boss: {boss_username!r}") + + employees = get_employees(boss_username) + print(f"Employees ({len(employees)}):") + for i, employee in enumerate(employees, 1): + print(f"{i}. {employee}") + """ + ... + 9. Employee(full_name='Ilya A. Petrash', user_name='ipetrash', position='Senior Software Engineer', url='https://mysite.compassplus.com:443/Person.aspx?accountname=CP%5Cipetrash') + ... + """ + print() + + usernames = sorted(x.user_name for x in employees) + print(usernames) + print(", ".join(usernames)) + + print("\n" + "-" * 100 + "\n") diff --git a/job_compassplus/mysite_compassplus_com/get_person_info.py b/job_compassplus/mysite_compassplus_com/get_person_info.py new file mode 100644 index 000000000..6a26aa2eb --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_person_info.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from urllib.parse import urljoin + +from bs4 import BeautifulSoup +from common import URL, do_get, get_text, is_active_profile, get_jira_user_active + + +@dataclass +class Person: + name: str + position: str + department: str + img_url: str + location: str + birthday: str + is_active: bool | None = None + + def download_img(self) -> bytes: + return do_get(self.img_url).content + + +def get_person_info(name: str, domain: str = "CP") -> Person | None: + full_username = rf"{domain}\{name}" + + url = URL.format(full_username) + rs = do_get(url) + soup = BeautifulSoup(rs.content, "html.parser") + + img_el = soup.select_one("#ctl00_PictureUrlImage") + if not img_el: # Сайт не умеет показывать 404 при отсутствующем пользователе + return + + default_value = "N/A" + + try: + position = get_text(soup.select_one("#ProfileViewer_ValueTitle")) + if not position: + position = default_value + except Exception: + position = default_value + + try: + department = get_text(soup.select_one("#ProfileViewer_ValueDepartment")) + if not department: + department = default_value + except Exception: + department = default_value + + try: + css_path = 'div[id *= "_ProfileViewer_SPS-Location"] > .ms-profile-detailsValue' + location = get_text(soup.select_one(css_path)) + if not location: + location = default_value + except Exception: + location = default_value + + try: + css_path = 'div[id *= "_ProfileViewer_SPS-Birthday"] > .ms-profile-detailsValue' + birthday = get_text(soup.select_one(css_path)) + if not birthday: + birthday = default_value + except Exception: + birthday = default_value + + try: + # NOTE: is_active_profile для mysite не подходит - не всегда оттуда убирают информацию + # В джире эта информация, похоже, всегда актуальная + is_active = get_jira_user_active(name) + except Exception: + try: + is_active = is_active_profile(full_username) + except Exception: + is_active = None + + return Person( + name=name, + position=position, + department=department, + img_url=urljoin(rs.url, img_el["src"]), + location=location, + birthday=birthday, + is_active=is_active, + ) + + +if __name__ == "__main__": + username = "ipetrash" + + info = get_person_info(username) + print(info) + # Person(name='ipetrash', position='Senior Software Engineer', department='TX SPD, Application Platforms Division', img_url='https://portal.compassplus.com/my/User%20Photos/Profile%20Pictures/ipetrash.jpg', location='Magnitogorsk', birthday='August 18', is_active=True) + + import json + from dataclasses import asdict + + print(json.dumps(asdict(info), ensure_ascii=False, indent=4)) + """ + { + "name": "ipetrash", + "position": "Senior Software Engineer", + "department": "TX SPD, Application Platforms Division", + "img_url": "https://portal.compassplus.com/my/User%20Photos/Profile%20Pictures/ipetrash.jpg", + "location": "Magnitogorsk", + "birthday": "August 18", + "is_active": true + } + """ diff --git a/job_compassplus/mysite_compassplus_com/get_profile_image.py b/job_compassplus/mysite_compassplus_com/get_profile_image.py new file mode 100644 index 000000000..01e0e9edf --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_image.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from urllib.parse import urljoin +from bs4 import BeautifulSoup +from common import URL, do_get + + +def get_profile_image(username: str, domain: str = "CP") -> bytes | None: + url = URL.format(fr"{domain}\{username}") + + rs = do_get(url) + + root = BeautifulSoup(rs.content, "html.parser") + img_el = root.select_one("#ctl00_PictureUrlImage") + if not img_el: # Сайт не умеет показывать 404 при отсутствующем пользователе + return + + img_url = urljoin(rs.url, img_el["src"]) + + rs = do_get(img_url) + return rs.content + + +if __name__ == "__main__": + username = "ipetrash" + + img_data = get_profile_image(username) + print(len(img_data), img_data[:20]) + + from pathlib import Path + img_path = Path(__file__).parent / f"{username}.jpg" + img_path.write_bytes(img_data) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py new file mode 100644 index 000000000..0031cc312 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" + +DIR_DB_BACKUP: Path = DIR / "database-backup" +DIR_DB_BACKUP.mkdir(parents=True, exist_ok=True) + +MAX_LAST_CHECK_DATE_DAYS: int = 30 diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py new file mode 100644 index 000000000..ca72e6789 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import time +import sys + +from datetime import date +from typing import Type, Iterable, Optional, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, + DateField, + BlobField, + BooleanField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DIR, DB_FILE_NAME + +sys.path.append(str(DIR.parent.parent.parent)) +from shorten import shorten + + +class NotDefinedParameterException(ValueError): + def __init__(self, parameter_name: str) -> None: + self.parameter_name = parameter_name + text = f'Parameter "{self.parameter_name}" must be defined!' + + super().__init__(text) + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + if isinstance(field, BlobField) and v is not None: + v = f"<{len(v)} bytes>" + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Person(BaseModel): + name = TextField() + position = TextField() + department = TextField() + img = BlobField(default=True) + location = TextField() + birthday = TextField() + create_date = DateField(default=date.today) + last_check_date = DateField(default=date.today) + is_active = BooleanField(default=None, null=True) + prev_name = TextField(default=None, null=True) + + class Meta: + indexes = ( + # Уникальный индекс по name и дате создания + (("name", "create_date"), True), + ) + + @classmethod + def get_last_by_name(cls, name: str) -> Optional["Person"]: + items: list[Person] = cls.get_all(name) + return items[0] if items else None + + @classmethod + def get_all(cls, name: str) -> list["Person"]: + prev_names: set[str] = set() + + persons: set[Person] = set() + for p in cls.select().where(cls.name == name): + persons.add(p) + + if p.prev_name: + prev_names.add(p.prev_name) + + for prev_name in prev_names: + if prev_name in persons: + continue + + for p in Person.get_all(prev_name): + persons.add(p) + + return sorted(persons, key=lambda p: p.id, reverse=True) + + @classmethod + def get_all_name(cls) -> list[str]: + return [p.name for p in cls.select(cls.name).distinct()] + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + print() + + names: list[str] = Person.get_all_name() + persons: list[Person] = [ + Person.get_last_by_name(name) + for name in names + ] + print(f"Total: {len(persons)}") + print(f"Active: {len([p for p in persons if p.is_active])}") + print(f"Non active: {len([p for p in persons if not p.is_active])}") + + # Поиск людей с одинаковой картинкой в профиле + # from collections import defaultdict + # img_by_persons: defaultdict[bytes, set[str]] = defaultdict(set) + # + # for p in Person: + # img_by_persons[p.img].add(p.name) + # + # for img, persons in sorted(img_by_persons.items(), key=lambda x: len(x[1]), reverse=True): + # if len(persons) > 1: + # print(len(persons), persons) + # diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py new file mode 100644 index 000000000..3b465039a --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +import traceback +import time +import zipfile + +from datetime import date +from pathlib import Path + +from config import DIR_DB_BACKUP +from db import db + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/b144a9c8fa6e737ef177fe1f7ae07d61794fc037/sqlite3__examples/backup__examples/common.py#L15 +def create_zip_for_file( + file_name_zip: str | Path, + file_name: Path, + delete_file_name: bool = True, +): + with zipfile.ZipFile( + file_name_zip, mode="w", compression=zipfile.ZIP_DEFLATED + ) as f: + f.write(file_name, arcname=file_name.name) + + if delete_file_name: + file_name.unlink() + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/bfac99ef66038562f16c94341535e0ce02f1dec2/sqlite3__examples/backup__examples/backup_via_api.py#L17 +def backup( + connect: sqlite3.Connection, + file_name: Path, + use_zip: bool = True, + delete_file_name_after_zip: bool = True, +) -> Path: + dst = sqlite3.connect(file_name) + connect.backup(dst) + dst.close() + + if use_zip: + file_name_zip = Path(f"{file_name}.zip") + create_zip_for_file( + file_name_zip, file_name, delete_file_name=delete_file_name_after_zip + ) + return file_name_zip + + return file_name + + +def do_backup_db() -> None: + prefix: str = "[do_backup_db]" + + print(f"{prefix} Start") + + while True: + print(f"{prefix} Check") + try: + file_name_backup = backup( + connect=db.connection(), + file_name=DIR_DB_BACKUP / f"{date.today().isoformat()}.sqlite", + ) + print(f"{prefix} Backup saved to {file_name_backup}") + + except Exception: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(f"{prefix} Error:\n{tb}") + + finally: + time.sleep(30 * 24 * 60 * 60) # Раз в 30 дней + + +if __name__ == "__main__": + do_backup_db() diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py new file mode 100644 index 000000000..3fc3275cf --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time +import traceback +from datetime import date, timedelta + +import db + +from config import DIR, MAX_LAST_CHECK_DATE_DAYS + +sys.path.append(str(DIR.parent)) +from get_person_info import Person, get_person_info, get_jira_user_active + + +def create_person_from_info(info: Person) -> db.Person: + return db.Person.create( + name=info.name, + position=info.position, + department=info.department, + img=info.download_img(), + location=info.location, + birthday=info.birthday, + is_active=info.is_active, + ) + + +def is_person_eq_info(person: db.Person, info: Person) -> bool: + if person.img != info.download_img(): + return False + + if person.department != info.department: + return False + + if person.position != info.position: + return False + + if person.location != info.location: + return False + + if person.birthday != info.birthday: + return False + + if person.is_active != info.is_active: + return False + + return True + + +def is_need_to_check(person: db.Person, d: date) -> bool: + return d > person.last_check_date + timedelta(days=MAX_LAST_CHECK_DATE_DAYS) + + +def add_or_get_db(name: str, forced: bool = False) -> db.Person | None: + person = db.Person.get_last_by_name(name) + + # Если нет, то создать + if not person: + info = get_person_info(name) + if not info: + return + + # Пусть первые пользователи всегда будут активными + info.is_active = True + + return create_person_from_info(info) + + # Проверить дату проверку + # Если с даты последней проверки прошло больше MAX_LAST_CHECK_DATE_DAYS дней, то + # нужно проверить изменения полей + today = date.today() + if is_need_to_check(person, d=today) or forced: + info = get_person_info(name) + if info: + if is_person_eq_info(person, info): + person.last_check_date = today + person.save() + else: + # Создание новой записи с актуальными полями + person = create_person_from_info(info) + + elif person.is_active: # Если пользователь был в БД и активным, а потом его удалили из mysite + # Создание новой записи + return db.Person.create( + name=person.name, + position=person.position, + department=person.department, + img=person.img, + location=person.location, + birthday=person.birthday, + is_active=False, + ) + + return person + + +def do_update_db(forced: bool = False) -> None: + prefix: str = "[do_update_db]" + + print(f"{prefix} Start") + + while True: + print(f"{prefix} Check all") + try: + # Запрос для получения ников + names: list[str] = db.Person.get_all_name() + for i, name in enumerate(names, 1): + print(f"{prefix} Check {name} ({i}/{len(names)})") + + person: db.Person | None = None + try: + person = add_or_get_db(name, forced=forced) + except Exception as e: + print(f"{prefix} Error: {e}") + finally: + # Оптимизация, чтобы не делать лишней задержку, если не было запроса по сети в add_or_get_db + # (это косвенно можно понять по last_check_date) + if not forced and person and not is_need_to_check(person, d=date.today()): + continue + + time.sleep(10) + + except Exception: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(f"{prefix} Error:\n{tb}") + + finally: + time.sleep(24 * 60 * 60) # Раз в сутки + + +if __name__ == "__main__": + user_name = "akrylov" + print(get_person_info(user_name)) + print(add_or_get_db(user_name, forced=True)) + + # do_update_db(forced=True) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore new file mode 100644 index 000000000..41bca1bd2 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore @@ -0,0 +1,3 @@ +cert.pem +key.pem +keyStore.p12 \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat new file mode 100644 index 000000000..a540f6036 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat @@ -0,0 +1 @@ +openssl pkcs12 -export -out keyStore.p12 -inkey key.pem -in cert.pem \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat new file mode 100644 index 000000000..4bbe769e3 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat @@ -0,0 +1 @@ +openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 1095 -config openssl.cnf \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf new file mode 100644 index 000000000..4d23e835a --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf @@ -0,0 +1,72 @@ +# +# OpenSSL configuration file. +# + +# Establish working directory. + +dir = . + +[ ca ] +default_ca = CA_default + +[ CA_default ] +serial = $dir/serial +database = $dir/certindex.txt +new_certs_dir = $dir/certs +certificate = $dir/cacert.pem +private_key = $dir/private/cakey.pem +default_days = 365 +default_md = md5 +preserve = no +email_in_dn = no +nameopt = default_ca +certopt = default_ca +policy = policy_match + +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +default_bits = 2048 # Size of keys +default_keyfile = key.pem # name of generated keys +default_md = md5 # message digest algorithm +string_mask = nombstr # permitted characters +distinguished_name = req_distinguished_name +req_extensions = v3_req + +[ req_distinguished_name ] +# Variable name Prompt string +#------------------------- ---------------------------------- +0.organizationName = Organization Name (company) +organizationalUnitName = Organizational Unit Name (department, division) +emailAddress = Email Address +emailAddress_max = 40 +localityName = Locality Name (city, district) +stateOrProvinceName = State or Province Name (full name) +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +commonName = Common Name (hostname, IP, or your name) +commonName_max = 64 + +# Default values for the above, for consistency and less typing. +# Variable name Value +#------------------------ ------------------------------ +0.organizationName_default = My Company +localityName_default = My Town +stateOrProvinceName_default = State or Providence +countryName_default = US + +[ v3_ca ] +basicConstraints = CA:TRUE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always + +[ v3_req ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py new file mode 100644 index 000000000..b213e8a46 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import os +from datetime import date, datetime + +# pip install flask==2.3.3 +from flask import Flask, Response, abort, jsonify +from flask.json.provider import DefaultJSONProvider + +# pip install flask-cors==4.0.0 +from flask_cors import CORS + +from requests.exceptions import RequestException + +import db +from db_updater import add_or_get_db + + +config = { + # "DEBUG": True, # some Flask specific configs + "CACHE_TYPE": "SimpleCache", # Flask-Caching related configs + "CACHE_DEFAULT_TIMEOUT": 300, +} + + +class UpdatedJSONProvider(DefaultJSONProvider): + sort_keys = False + + def default(self, o): + if isinstance(o, (date, datetime)): + return o.isoformat() + return super().default(o) + + +app = Flask(__name__) + +# Tell Flask to use the above defined config +app.config.from_mapping(config) + +app.json = UpdatedJSONProvider(app) + +CORS(app) + + +@app.errorhandler(RequestException) +def handle_requests_error(e: RequestException) -> Response: + return Response( + response=str(e), + status=e.response.status_code if e.response else 404, + ) + + +@app.route("/api/get_all_person_info/") +def api_get_all_person_info(username: str) -> tuple[Response, int]: + has_person: bool = db.Person.get_last_by_name(username) is not None + + # Проверка наличия и попытка добавить в базу для первого раза + person: db.Person = add_or_get_db(username) + if not person: + abort(404) + + img_by_idx: dict[str, int] = dict() + items: list[dict] = [] + + for i, person in enumerate(db.Person.get_all(username)): + data = person.to_dict() + + # NOTE: Картинки возвращаются в одном запросе + data_base64 = base64.b64encode(person.img).decode("utf-8") + img_base64 = f"data:image/jpg;base64,{data_base64}" + + # Оптимизация, чтобы не возвращать одинаковые картинки + if img_base64 not in img_by_idx: + img_by_idx[img_base64] = i + else: + img_base64 = f"={img_by_idx[img_base64]}" # Символа "=" нет в base64 + + data["img"] = img_base64 + + items.append(data) + + return jsonify(items), 200 if has_person else 201 + + +if __name__ == "__main__": + from threading import Thread + from db_backup import do_backup_db + from db_updater import do_update_db + + Thread(target=do_backup_db, daemon=True).start() + Thread(target=do_update_db, daemon=True).start() + + app.run( + host="0.0.0.0", + port=int(os.environ.get("FLASK_PORT", 50000)), + # TODO: for https + # ssl_context=("for_https/cert.pem", "for_https/key.pem"), + ) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py new file mode 100644 index 000000000..c428fa821 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations + + +from playhouse.migrate import SqliteDatabase, SqliteMigrator, migrate +from db import TextField, DB_FILE_NAME, Person + + +db = SqliteDatabase(DB_FILE_NAME) +migrator = SqliteMigrator(db) + + +with db.atomic(): + migrate( + migrator.add_column( + Person._meta.table_name, + "prev_name", + TextField(default=None, null=True), + ), + ) diff --git a/job_compassplus/outlook_jira_issues_autoread.py b/job_compassplus/outlook_jira_issues_autoread.py new file mode 100644 index 000000000..eeeecee44 --- /dev/null +++ b/job_compassplus/outlook_jira_issues_autoread.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import argparse +import email +import imaplib +import re +import time + +from dataclasses import dataclass +from datetime import datetime, timedelta +from email.header import decode_header +from email.utils import parseaddr +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +PATTERN_JIRA: re.Pattern = re.compile(r"\((\w+-\d+)\)") + + +@dataclass +class IssueInfo: + key: str + is_done: bool + updated: datetime | None + resolution_date: datetime | None + + +def get_issue_info(issue_key: str) -> IssueInfo: + def _get_datetime(value: str) -> datetime | None: + if value: + return datetime.fromisoformat(value).replace(tzinfo=None) + + url_issue: str = f"{JIRA_HOST}/rest/api/latest/issue/{issue_key}" + + rs = session.get(url_issue) + rs.raise_for_status() + + rs_fields: dict[str, Any] = rs.json()["fields"] + + return IssueInfo( + key=issue_key, + is_done=rs_fields["status"]["statusCategory"]["key"] == "done", + updated=_get_datetime(rs_fields.get("updated")), + resolution_date=_get_datetime(rs_fields.get("resolutiondate")), + ) + + +def get_args(): + parser = argparse.ArgumentParser( + description="Скрипт для автоматического прочтения неактуальных писем из Jira в Outlook." + ) + + # Обязательные параметры + parser.add_argument( + "-u", + "--user", + required=True, + help="Логин (email) от почты", + ) + parser.add_argument( + "-p", + "--password", + required=True, + help="Пароль от почты (или пароль приложения)", + ) + parser.add_argument( + "-f", + "--folder", + default="INBOX", + help="Папка для поиска (например, INBOX или 'Jira/Issues', по умолчанию: %(default)s))", + ) + + # Опциональные параметры + parser.add_argument( + "--server", + default="mail.compassplus.com", + help="Адрес IMAP сервера (по умолчанию: %(default)s)", + ) + + parser.add_argument( + "--limit", + type=int, + default=None, + help="Ограничение количества обрабатываемых писем (для тестирования, например 100)", + ) + + # Флаг прочтения (action='store_true' делает его булевым: если указан - True, если нет - False) + parser.add_argument( + "--mark", + action="store_true", + help="Если указан, письма при совпадении условий будут отмечены как прочитанные на сервере", + ) + + return parser.parse_args() + + +def process( + user: str, + password: str, + server: str, + folder: str, + limit: int | None = None, + mark_as_read: bool = False, +) -> None: + print(f"Подключение к {server} для пользователя {user}...") + + mail = imaplib.IMAP4_SSL(server) + mail.login(user, password) + + mail.select(f'"{folder}"') + + # Письма от старых к новым + status, response = mail.search(None, "(UNSEEN)") + + issue_by_info: dict[str, Any] = dict() + + msg_ids = response[0].split() + if limit: + print(f"Лимит обработки: {limit} писем") + msg_ids = msg_ids[:limit] + + for num in msg_ids: + # BODY.PEEK[] - читаем, не меняя статус на "Прочитано" + status, data = mail.fetch(num, "(BODY.PEEK[HEADER])") + raw_email: bytes = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Декодируем заголовок + decoded_parts = decode_header(msg["Subject"]) + + subject = "" + for content, encoding in decoded_parts: + if isinstance(content, bytes): + # Используем кодировку из письма, если её нет — пробуем utf-8 + enc = encoding if encoding else "utf-8" + try: + subject += content.decode(enc) + except (UnicodeDecodeError, LookupError): + # Если всё равно ошибка, декодируем с заменой битых символов + subject += content.decode("utf-8", errors="replace") + else: + # Если это уже строка (например, ASCII) + subject += content + + raw_from: str = msg.get("From") + name, email_address = parseaddr(raw_from) + + raw_date: str = msg.get("Date") + date_tuple = email.utils.parsedate_tz(raw_date) + local_date: datetime | None = None + if date_tuple: + local_date = datetime.fromtimestamp( + email.utils.mktime_tz(date_tuple) + ).replace(tzinfo=None) + else: + print(f"[#] Неправильная дата письма: {raw_date!r}") + + read_it: bool = False + + # NOTE: Можно оптимизировать и не ходить в API Jira, если письмо старше такой-то даты + # Но хочется посмотреть информацию по задачам из писем + m: re.Match | None = PATTERN_JIRA.search(subject) + if m: + issue_key: str = m.group(1) + print( + "[+]", + num, + f"{local_date.strftime('%d.%m.%Y %H:%M:%S') if local_date else None}", + repr(subject), + repr(name), + email_address, + issue_key, + ) + + issue: IssueInfo | None = issue_by_info.get(issue_key) + if not issue: + try: + issue = get_issue_info(issue_key) + issue_by_info[issue_key] = issue + except Exception as e: + print(f"[#] Не удалось получить информацию по задаче {issue_key!r}: {e}") + + time.sleep(1) + + print(f" Информация по задаче: {issue if issue else '<неизвестно>'}") + + if issue: + # Определяем запас времени (например, 10 минут) + buffer = timedelta(minutes=10) + + # Если письма приходили до даты решения задачи, то уже не актуальные + if ( + issue.is_done + and issue.resolution_date + and local_date + # Если дата решения задачи больше даты отправки письма + and (issue.resolution_date - buffer) > local_date + ): + print(" Письмо приходило раньше решения задачи") + read_it = True + + else: + print( + "[?]", + num, + f"{local_date.strftime('%d.%m.%Y %H:%M:%S') if local_date else None}", + repr(subject), + repr(name), + email_address, + ) + + # Если прошло больше 1 месяца + if local_date and (datetime.now() - timedelta(weeks=4)) > local_date: + print(" Прошло больше 1 месяца") + read_it = True + + if read_it: + status_msg = ( + "[ПРОЧИТАНО]" + if mark_as_read + else "[DRY-RUN: БЫЛО БЫ ПРОЧИТАНО С ФЛАГОМ --mark]" + ) + print(f" {status_msg}") + + if mark_as_read: + mail.store(num, "+FLAGS", r"\Seen") + + mail.logout() + + +if __name__ == "__main__": + args = get_args() + + process( + user=args.user, + password=args.password, + server=args.server, + folder=args.folder, + limit=args.limit, + mark_as_read=args.mark, + ) diff --git a/parse__radix__CacheContent_log/CacheContent_log.txt b/job_compassplus/parse__radix__CacheContent_log/CacheContent_log.txt similarity index 100% rename from parse__radix__CacheContent_log/CacheContent_log.txt rename to job_compassplus/parse__radix__CacheContent_log/CacheContent_log.txt diff --git a/job_compassplus/parse__radix__CacheContent_log/main.py b/job_compassplus/parse__radix__CacheContent_log/main.py new file mode 100644 index 000000000..d70a7bed9 --- /dev/null +++ b/job_compassplus/parse__radix__CacheContent_log/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +__author__ = "ipetrash" + + +import re + + +def get_cache_object_info(file_name) -> dict[str, dict[str, int]]: + # new_object: Tran[180409000010273756] + # existing_object: Hold[13708] + pattern_object = re.compile(r"^(\w+_object): (.+)\[.+$") + + object_by_number = dict() + + for line in open(file_name): + match = pattern_object.search(line) + if match: + type_cache = match[1] + name = match[2] + + if type_cache not in object_by_number: + object_by_number[type_cache] = dict() + + if name not in object_by_number[type_cache]: + object_by_number[type_cache][name] = 0 + + object_by_number[type_cache][name] += 1 + + return object_by_number + + +def get_cache_existing_object_list( + file_name, top_values=None +) -> list[tuple[str, int]]: + object_by_number = get_cache_object_info(file_name) + + existing_object_list = sorted( + object_by_number["existing_object"].items(), key=lambda x: x[1], reverse=True + ) + if top_values: + existing_object_list = existing_object_list[:top_values] + + rows = [(name, number) for name, number in existing_object_list] + return rows + + +if __name__ == "__main__": + file_name = "CacheContent_log.txt" + print(file_name) + + rows = get_cache_existing_object_list(file_name, top_values=5) + + # pip install tabulate + from tabulate import tabulate + print(tabulate(rows, headers=("NAME", "NUMBER"), tablefmt="grid")) diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py new file mode 100644 index 000000000..a653ed575 --- /dev/null +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +# pip install tabulate +from tabulate import tabulate + + +def get_table(assigned_open_issues_per_project: dict[str, int]) -> str: + return tabulate( + list(assigned_open_issues_per_project.items()), + headers=("PROJECT", "Issues"), + tablefmt="grid", + ) + + +def print_table(assigned_open_issues_per_project: dict[str, int]) -> None: + print(get_table(assigned_open_issues_per_project)) + # PROJECT | Issues + # --------+------- + # xxx | 1 + # yyy | 2 + # zzz | 3 + + +def get_logger( + name, file="log.txt", encoding="utf-8", log_stdout=True, log_file=True +) -> logging.Logger: + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) + + if log_file: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + if log_stdout: + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +logger = get_logger("parse_jira_Assigned_Open_Issues_per_Project") diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py new file mode 100644 index 000000000..5b6f7fe69 --- /dev/null +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import os +import shutil +import sys + +from peewee import ( + SqliteDatabase, + Model, + TextField, + CharField, + ForeignKeyField, + DateField, + IntegerField, +) + +from common import ROOT_DIR, DIR, print_table + +# Для импортирования shorten +sys.path.append(str(ROOT_DIR.parent)) +from shorten import shorten + + +# Absolute file name +DB_FILE_NAME = str(DIR / "database.sqlite") + + +def db_create_backup(backup_dir=DIR / "backup") -> None: + os.makedirs(backup_dir, exist_ok=True) + + file_name = str(dt.datetime.today().date()) + ".sqlite" + file_name = os.path.join(backup_dir, file_name) + + shutil.copy(DB_FILE_NAME, file_name) + + +# Ensure foreign-key constraints are enforced. +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) + + +class BaseModel(Model): + class Meta: + database = db + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Run(BaseModel): + date = DateField(default=dt.date.today) + + def get_total_issues(self) -> int: + return sum(x.value for x in self.issue_numbers) + + def get_project_by_issue_numbers(self) -> dict[str, int]: + return {issue.project.name: issue.value for issue in self.issue_numbers} + + def __str__(self) -> str: + return f"{self.__class__.__name__}(id={self.id}, date={self.date}, total_issues={self.get_total_issues()})" + + +class Project(BaseModel): + name = TextField() + + +class IssueNumber(BaseModel): + value = IntegerField() + run = ForeignKeyField(Run, backref="issue_numbers") + project = ForeignKeyField(Project, backref="issue_numbers") + + +def add(assigned_open_issues_per_project: dict[str, int]) -> bool: + run, created = Run.get_or_create(date=dt.date.today()) + if not created: + return False + + for project_name, issue_numbers in assigned_open_issues_per_project.items(): + project, _ = Project.get_or_create(name=project_name) + + IssueNumber.create(value=issue_numbers, run=run, project=project) + + db_create_backup() + + return True + + +db.connect() +db.create_tables([Run, Project, IssueNumber]) + + +if __name__ == "__main__": + projects = [p.name for p in Project.select()] + print(f"Projects ({len(projects)}): {projects}\n") + + # Print last rows + for run in Run.select().order_by(Run.id.desc()).limit(5): + print(run) + print_table(run.get_project_by_issue_numbers()) + + print("\n" + "-" * 100 + "\n") diff --git a/parse_jira_Assigned_Open_Issues_per_Project/favicon.ico b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/favicon.ico similarity index 100% rename from parse_jira_Assigned_Open_Issues_per_Project/favicon.ico rename to job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/favicon.ico diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py new file mode 100644 index 000000000..88dcfc1cd --- /dev/null +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from bs4 import BeautifulSoup + +from common import ROOT_DIR, print_table + +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +URL = f"{JIRA_HOST}/secure/ViewProfile.jspa?name=ipetrash" + + +def get_assigned_open_issues_per_project() -> dict[str, int]: + rs = session.get(URL) + rs.raise_for_status() + + root = BeautifulSoup(rs.content, "html.parser") + + data = dict() + for item in root.select("#assigned-and-open > .mod-content > .stat-list > li"): + name = item.select_one("a[title]").get_text(strip=True) + value = int(item.select_one(".stat").get_text(strip=True)) + + data[name] = value + + return data + + +def get_and_prints() -> dict[str, int]: + assigned_open_issues_per_project = get_assigned_open_issues_per_project() + # {'xxx': 1, 'yyy': 2, 'zzz': 3} + + print("Total issues:", sum(assigned_open_issues_per_project.values())) + print() + + print_table(assigned_open_issues_per_project) + # PROJECT | Issues + # --------+------- + # xxx | 1 + # yyy | 2 + # zzz | 3 + + return assigned_open_issues_per_project + + +if __name__ == "__main__": + get_and_prints() diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py new file mode 100644 index 000000000..11dc03b4d --- /dev/null +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import calendar +import math +import sys +import time +import traceback + +from datetime import datetime, date, timedelta + +from PyQt5.QtWidgets import ( + QApplication, + QMessageBox, + QMainWindow, + QSystemTrayIcon, + QTabWidget, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, + QPushButton, + QSplitter, + QLabel, + QGridLayout, + QHeaderView, + QProgressBar, + QMenu, + QComboBox, +) +from PyQt5.QtGui import QIcon, QPainter, QCloseEvent +from PyQt5.QtCore import QEvent, QTimer, Qt, QThread, pyqtSignal +from PyQt5.QtChart import QChart, QLineSeries, QDateTimeAxis, QValueAxis + +from common import ROOT_DIR, DIR, get_table +from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project +from db import Run + +sys.path.append(str(ROOT_DIR.parent / "qt__pyqt__pyside__pyqode")) +from chart_line__show_tooltip_on_series__QtChart import ChartViewToolTips + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print(text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +WINDOW_TITLE: str = DIR.name + +DATE_FORMAT: str = "%d.%m.%Y" +TIME_FORMAT: str = "%H:%M:%S" + + +def get_human_datetime(dt: datetime | None = None) -> str: + if not dt: + dt = datetime.now() + return dt.strftime(f"{DATE_FORMAT} {TIME_FORMAT}") + + +def get_human_date(d: datetime | date | None = None) -> str: + if not d: + d = date.today() + return d.strftime(DATE_FORMAT) + + +def get_human_time(dt: datetime | None = None) -> str: + if not dt: + dt = datetime.now() + return dt.strftime(TIME_FORMAT) + + +def get_table_widget(header_labels: list) -> QTableWidget: + table = QTableWidget() + table.setAlternatingRowColors(True) + table.setEditTriggers(QTableWidget.NoEditTriggers) + table.setSelectionBehavior(QTableWidget.SelectRows) + table.setSelectionMode(QTableWidget.SingleSelection) + table.setColumnCount(len(header_labels)) + table.setHorizontalHeaderLabels(header_labels) + table.horizontalHeader().setStretchLastSection(True) + return table + + +class GetAssignedOpenIssuesPerProjectThread(QThread): + about_items = pyqtSignal(dict) + about_error = pyqtSignal(Exception) + + def run(self) -> None: + try: + items: dict[str, int] = get_assigned_open_issues_per_project() + self.about_items.emit(items) + + # Даем время на отображение и анимацию прогресс-бара + time.sleep(0.3) + + except Exception as e: + self.about_error.emit(e) + + +class TableWidgetRun(QWidget): + def __init__(self) -> None: + super().__init__() + + self.table_run = get_table_widget(["DATE", "TOTAL ISSUES"]) + self.table_run.selectionModel().selectionChanged.connect( + self._on_table_run_item_clicked + ) + + self.table_issues = get_table_widget(["PROJECT", "NUMBER"]) + self.table_run.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table_issues.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + splitter = QSplitter(Qt.Horizontal) + splitter.addWidget(self.table_run) + splitter.addWidget(self.table_issues) + + main_layout = QVBoxLayout() + main_layout.addWidget(splitter) + + self.setLayout(main_layout) + + def refresh(self, items: list[Run]) -> None: + # Удаление строк таблицы + while self.table_run.rowCount(): + self.table_run.removeRow(0) + + for i, run in enumerate(items): + self.table_run.setRowCount(self.table_run.rowCount() + 1) + + item = QTableWidgetItem(get_human_date(run.date)) + item.setData(Qt.UserRole, run.get_project_by_issue_numbers()) + self.table_run.setItem(i, 0, item) + + item = QTableWidgetItem(str(run.get_total_issues())) + self.table_run.setItem(i, 1, item) + + self.table_run.setCurrentCell(0, 0) + self.table_run.setFocus() + self._on_table_run_item_clicked() + + def _on_table_run_item_clicked(self) -> None: + # Удаление строк таблицы + while self.table_issues.rowCount(): + self.table_issues.removeRow(0) + + item = self.table_run.item(self.table_run.currentRow(), 0) + if not item: + return + + for i, (project_name, number) in enumerate(item.data(Qt.UserRole).items()): + self.table_issues.setRowCount(self.table_issues.rowCount() + 1) + + self.table_issues.setItem(i, 0, QTableWidgetItem(project_name)) + self.table_issues.setItem(i, 1, QTableWidgetItem(str(number))) + + +class CurrentAssignedOpenIssues(QWidget): + def __init__(self) -> None: + super().__init__() + + self.table = get_table_widget(["PROJECT", "NUMBER"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) + self.progress_bar.setTextVisible(False) + sp_progress_bar = self.progress_bar.sizePolicy() + sp_progress_bar.setRetainSizeWhenHidden(True) + self.progress_bar.setSizePolicy(sp_progress_bar) + self.progress_bar.hide() + + self.label_total = QLabel() + font = self.label_total.font() + font.setPixelSize(30) + self.label_total.setFont(font) + + self.label_last_refresh_date = QLabel() + font = self.label_last_refresh_date.font() + font.setPixelSize(18) + self.label_last_refresh_date.setFont(font) + + main_layout = QGridLayout() + main_layout.addWidget(self.label_total, 0, 0, Qt.AlignLeft | Qt.AlignCenter) + main_layout.addWidget(self.progress_bar, 0, 1) + main_layout.addWidget( + self.label_last_refresh_date, 0, 2, Qt.AlignRight | Qt.AlignCenter + ) + main_layout.addWidget(self.table, 1, 0, 2, 0) + + self.setLayout(main_layout) + + self.thread = GetAssignedOpenIssuesPerProjectThread() + self.thread.started.connect(self.progress_bar.show) + self.thread.finished.connect(self.progress_bar.hide) + self.thread.about_items.connect(self._on_set_items) + self.thread.about_error.connect(self._on_error) + + self.current_items: dict[str, int] | None = None + self._update_total_issues("-") + + def _update_total_issues(self, value) -> None: + self.label_total.setText(f"Total issues: {value}") + + def _on_set_items(self, items: dict[str, int]) -> None: + self.current_items = items + + self._update_total_issues(sum(self.current_items.values())) + + # Удаление строк таблицы + while self.table.rowCount(): + self.table.removeRow(0) + + for i, (project_name, number) in enumerate(self.current_items.items()): + self.table.setRowCount(self.table.rowCount() + 1) + + self.table.setItem(i, 0, QTableWidgetItem(project_name)) + self.table.setItem(i, 1, QTableWidgetItem(str(number))) + + self.label_last_refresh_date.setText( + f"Last refresh date: {get_human_datetime()}" + ) + + def _on_error(self, e: Exception) -> None: + tb_str = "".join(traceback.format_tb(e.__traceback__)) + print(tb_str) + QMessageBox.warning(self, "ERROR", str(e)) + + def refresh(self) -> None: + self.current_items = None + + self.thread.start() + + +class MyChartViewToolTips(ChartViewToolTips): + def __init__(self, timestamp_by_info: dict[int, dict[str, int]]) -> None: + super().__init__() + + self._callout_font_family = "Courier" + self.timestamp_by_info: dict[int, dict[str, int]] = timestamp_by_info + + def show_series_tooltip(self, point, state: bool) -> None: + # value -> pos + point = self.chart().mapToPosition(point) + + if not self._tooltip: + self._tooltip = self._add_Callout() + + if not state: + self._tooltip.hide() + return + + distance = 25 + + for series in self.chart().series(): + for p_value in series.pointsVector(): + p = self.chart().mapToPosition(p_value) + + current_distance = math.sqrt( + (p.x() - point.x()) * (p.x() - point.x()) + + (p.y() - point.y()) * (p.y() - point.y()) + ) + + if current_distance < distance: + time_ms = int(p_value.x()) + info: dict[str, int] = self.timestamp_by_info[time_ms] + table = get_table(info) + text = ( + f"{get_human_date(date.fromtimestamp(time_ms / 1000))}" + "\n\n" + f"Total issues: {sum(info.values())}" + "\n" + f"{table}" + ) + + self._tooltip.setText(text) + self._tooltip.setAnchor(p_value) + self._tooltip.setZValue(11) + self._tooltip.updateGeometry() + self._tooltip.show() + + +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(WINDOW_TITLE) + + file_name = str(DIR / "favicon.ico") + icon = QIcon(file_name) + self.setWindowIcon(icon) + + self.timestamp_by_info: dict[int, dict[str, int]] = dict() + + menu = QMenu() + menu.addAction("Show / hide", (lambda: self.setVisible(not self.isVisible()))) + menu.addSeparator() + menu.addAction("Quit", QApplication.instance().quit) + + self.tray = QSystemTrayIcon(icon) + self.tray.setContextMenu(menu) + self.tray.setToolTip(self.windowTitle()) + self.tray.activated.connect(self._on_tray_activated) + self.tray.show() + + self.chart_view = MyChartViewToolTips(self.timestamp_by_info) + self.chart_view.setRenderHint(QPainter.Antialiasing) + + self.cb_chart_filter = QComboBox() + self.cb_chart_filter.addItem("", userData=0) + self.cb_chart_filter.activated.connect(self.refresh) + layout_chart_view = QVBoxLayout(self.chart_view) + layout_chart_view.addWidget(self.cb_chart_filter) + layout_chart_view.setAlignment( + self.cb_chart_filter, Qt.AlignTop | Qt.AlignRight + ) + + self.table_run = TableWidgetRun() + self.table_run.layout().setContentsMargins(0, 0, 0, 0) + + self.current_assigned_open_issues = CurrentAssignedOpenIssues() + + self.tab_widget = QTabWidget() + self.tab_widget.addTab(self.chart_view, "CHART") + self.tab_widget.addTab(self.table_run, "TABLE RUN") + self.tab_widget.addTab( + self.current_assigned_open_issues, "Current Assigned Open Issues" + ) + + self.pb_refresh = QPushButton("REFRESH") + self.pb_refresh.clicked.connect(self.refresh) + + main_layout = QVBoxLayout() + main_layout.addWidget(self.tab_widget) + main_layout.addWidget(self.pb_refresh) + + central_widget = QWidget() + central_widget.setLayout(main_layout) + + self.setCentralWidget(central_widget) + + @staticmethod + def _get_timegm(date: date) -> int: + return calendar.timegm(date.timetuple()) * 1000 + + def _get_datetime(self, date: date, delta: timedelta = None) -> datetime: + dt = datetime.combine(date, datetime.min.time()) + if delta: + dt += delta + return dt + + def _fill_chart_filter(self, items: list[Run]) -> None: + years: list[int] = sorted({run.date.year for run in items}) + filter_years: list[int] = [ + self.cb_chart_filter.itemData(i) + for i in range(self.cb_chart_filter.count()) + ] + for year in years: + if year not in filter_years: + self.cb_chart_filter.addItem(f"{year}", userData=year) + + def _fill_chart(self, items: list[Run]) -> None: + # Фильтрация данных из графика + year: int = self.cb_chart_filter.currentData() + if year: + items = [run for run in items if run.date.year == year] + + series = QLineSeries() + series.setPointsVisible(True) + series.setPointLabelsVisible(True) + series.setPointLabelsFormat("@yPoint") + series.hovered.connect(self.chart_view.show_series_tooltip) + + self.timestamp_by_info.clear() + + issues_number = [] + for run in items: + date_value = self._get_timegm(run.date) + total_issues = run.get_total_issues() + series.append(date_value, total_issues) + issues_number.append(total_issues) + + self.timestamp_by_info[date_value] = run.get_project_by_issue_numbers() + + now_date_timestamp = self._get_timegm(date.today()) + if now_date_timestamp not in self.timestamp_by_info: + # Использование данных из соседнего виджета + self.current_assigned_open_issues.refresh() + while not self.current_assigned_open_issues.current_items: + QApplication.instance().processEvents() + + self.timestamp_by_info[now_date_timestamp] = self.current_assigned_open_issues.current_items + series.append(now_date_timestamp, sum(self.current_assigned_open_issues.current_items.values())) + + chart = QChart() + chart.setTheme(QChart.ChartThemeDark) + chart.setAnimationOptions(QChart.SeriesAnimations) + chart.addSeries(series) + chart.legend().hide() + + # No margin + chart.layout().setContentsMargins(0, 0, 0, 0) + chart.setBackgroundRoundness(0) + + axisX = QDateTimeAxis() + if items: + axisX.setRange( + self._get_datetime(items[0].date, timedelta(days=-30)), + self._get_datetime(items[-1].date, timedelta(days=30)), + ) + axisX.setFormat("dd/MM/yyyy") + axisX.setTitleText("Date") + chart.addAxis(axisX, Qt.AlignBottom) + series.attachAxis(axisX) + + axisY = QValueAxis() + if issues_number: + axisY.setRange(min(issues_number) * 0.8, max(issues_number) * 1.2) + axisY.setLabelFormat("%d") + axisY.setTitleText("Total issues") + chart.addAxis(axisY, Qt.AlignLeft) + series.attachAxis(axisY) + + self.chart_view.clear_all_tooltips() + self.chart_view.setChart(chart) + + def refresh(self) -> None: + self.current_assigned_open_issues.refresh() + + items = list(Run.select()) + + self._fill_chart_filter(items) + + self._fill_chart(items) + + items.reverse() + self.table_run.refresh(items) + + self.setWindowTitle( + f"{WINDOW_TITLE}. Last refresh date: {get_human_datetime()}" + ) + + def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None: + # Если запрошено меню + if reason == QSystemTrayIcon.Context: + return + + self.setVisible(not self.isVisible()) + + if self.isVisible(): + self.showNormal() + self.activateWindow() + + def changeEvent(self, event: QEvent) -> None: + if event.type() == QEvent.WindowStateChange: + # Если окно свернули + if self.isMinimized(): + # Прячем окно с панели задач + QTimer.singleShot(0, self.hide) + + def closeEvent(self, event: QCloseEvent) -> None: + self.hide() + event.ignore() + + +if __name__ == "__main__": + app = QApplication([]) + app.setQuitOnLastWindowClosed(False) + + mw = MainWindow() + mw.resize(1200, 800) + mw.show() + + mw.refresh() + + app.exec() diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py new file mode 100644 index 000000000..79bb07dcd --- /dev/null +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import sys + +# pip install schedule +import schedule + +# pip install simple-wait +from simple_wait import wait + +import db +from common import get_table, logger +from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project + + +IS_SINGLE: bool = "--single" in sys.argv + + +def run(): + attempts_for_single: int = 5 + + while True: + try: + logger.info(f"Начало") + + assigned_open_issues_per_project = get_assigned_open_issues_per_project() + logger.info( + "Всего задач: %s\n\n%s\n", + sum(assigned_open_issues_per_project.values()), + get_table(assigned_open_issues_per_project), + ) + + ok = db.add(assigned_open_issues_per_project) + if ok: + logger.info("Добавляю запись") + else: + logger.info("Сегодня запись уже была добавлена. Пропускаю...") + + logger.info("\n" + "-" * 100 + "\n") + break + + except Exception as e: + if IS_SINGLE: + attempts_for_single -= 1 + if attempts_for_single <= 0: + raise e + + logger.exception("Ошибка:") + + logger.info("Через 15 минут попробую снова...") + wait(minutes=15) + + +if __name__ == "__main__": + if IS_SINGLE: + run() + sys.exit() + + # Каждую неделю, в субботу, в 12:00 + schedule.every().week.saturday.at("12:00").do(run) + + while True: + schedule.run_pending() + time.sleep(60) diff --git a/job_compassplus/parse_total_efforts.py b/job_compassplus/parse_total_efforts.py new file mode 100644 index 000000000..cf660badc --- /dev/null +++ b/job_compassplus/parse_total_efforts.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + + +PATTERN_ARG: re.Pattern = re.compile(r"<(\w+)>") +PATTERN_RESULT: re.Pattern = re.compile(r"<=([\w +]+)>") + +DEFAULT_ARG_VALUE: str = "NaN" + +SAMPLE_TEMPLATE = """ +*Итоговая трудоемкость в человеко-днях:* <=a+b+c+d+e> ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)| ч/д| +|Разработка (программирование)| ч/д| +|Рецензирование решения и кода| ч/д| +|Тестирование (только силами разработчика)| ч/д| +|Резерв на непредвиденные работы и риски| ч/д| + +*Total efforts in man-days:* <= a+b+c+d+e> m/d, including: +|Analysis (design and tech specs)| m/d| +|Implementation (including coding)| m/d| +|Code and solution review| m/d| +|Testing (only by the developer)| m/d| +|Reserve for potential gaps and risks| m/d| +""".strip() + + +def get_args(template: str) -> list[str]: + args: list[str] = [] + for m in PATTERN_ARG.finditer(template): + value: str = m.group(1) + if value not in args: + args.append(value) + + return args + + +def process(template: str, arg_by_value: dict[str, str]) -> str: + def _get_norm_str_float(value: str) -> str: + return DEFAULT_ARG_VALUE if value == "nan" else value.removesuffix(".0") + + def _get_value_from_arg( + arg: str, + default_value: str = DEFAULT_ARG_VALUE, + get_float: bool = True, + ) -> str: + value = arg_by_value.get(arg) + if value: + value = value.strip() + value = _get_norm_str_float(value) + + # Проверка валидности значения + try: + float(value) + except Exception: + value = None + + if not value: + value = default_value + + if get_float: + value = f"float({value!r})" + + return value + + text: str = PATTERN_ARG.sub( + lambda m: _get_value_from_arg( + arg=m.group(1), + get_float=False, + ), + template, + ) + + def _process_result(m: re.Match) -> str: + template_expr: str = m.group(1) + + expr: str = re.sub( + r"\w+", + lambda m: _get_value_from_arg(arg=m.group()), + template_expr, + ) + + try: + value = eval(expr) + except SyntaxError: + value = float(DEFAULT_ARG_VALUE) + + result: str = f"{value:.1f}" + return _get_norm_str_float(result) + + return PATTERN_RESULT.sub(_process_result, text) + + +if __name__ == "__main__": + args: list[str] = get_args(SAMPLE_TEMPLATE) + print(args) + assert args == ["a", "b", "c", "d", "e"] + + print() + + arg_by_value: dict[str, str] = dict( + a="1.1", + b="0.1", + c="0.5", + d="12", + e="1.0", + ) + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* 14.7 ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|1.1 ч/д| +|Разработка (программирование)|0.1 ч/д| +|Рецензирование решения и кода|0.5 ч/д| +|Тестирование (только силами разработчика)|12 ч/д| +|Резерв на непредвиденные работы и риски|1 ч/д| + +*Total efforts in man-days:* 14.7 m/d, including: +|Analysis (design and tech specs)|1.1 m/d| +|Implementation (including coding)|0.1 m/d| +|Code and solution review|0.5 m/d| +|Testing (only by the developer)|12 m/d| +|Reserve for potential gaps and risks|1 m/d| + """.strip() + + print("\n" + "-" * 10 + "\n") + + arg_by_value["e"] = "1.3" + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* 15 ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|1.1 ч/д| +|Разработка (программирование)|0.1 ч/д| +|Рецензирование решения и кода|0.5 ч/д| +|Тестирование (только силами разработчика)|12 ч/д| +|Резерв на непредвиденные работы и риски|1.3 ч/д| + +*Total efforts in man-days:* 15 m/d, including: +|Analysis (design and tech specs)|1.1 m/d| +|Implementation (including coding)|0.1 m/d| +|Code and solution review|0.5 m/d| +|Testing (only by the developer)|12 m/d| +|Reserve for potential gaps and risks|1.3 m/d| + """.strip() + + print("\n" + "-" * 10 + "\n") + + arg_by_value: dict[str, str] = dict( + a="", + b="b", + c=" 3 ", + d="0", + ) + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* NaN ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|NaN ч/д| +|Разработка (программирование)|NaN ч/д| +|Рецензирование решения и кода|3 ч/д| +|Тестирование (только силами разработчика)|0 ч/д| +|Резерв на непредвиденные работы и риски|NaN ч/д| + +*Total efforts in man-days:* NaN m/d, including: +|Analysis (design and tech specs)|NaN m/d| +|Implementation (including coding)|NaN m/d| +|Code and solution review|3 m/d| +|Testing (only by the developer)|0 m/d| +|Reserve for potential gaps and risks|NaN m/d| + """.strip() diff --git a/job_compassplus/portal_compassplus_com/employees_gui/config.py b/job_compassplus/portal_compassplus_com/employees_gui/config.py new file mode 100644 index 000000000..f6e4fb044 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/config.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +URL = "https://portal.compassplus.com/Employees/Pages/OfficeReferenceBook.aspx" +URL_GET_EMPLOYEES_LIST = ( + "https://portal.compassplus.com/_layouts/15/tbi/employees.ashx?" + "p=50&c=1&s={}&fl=MPhotoUrl;NameLink;JobTitle;Department;WorkPhone" +) + +URL_GET_EMPLOYEE_INFO = ( + "https://portal.compassplus.com/_layouts/15/tbi/ui.ashx?u={}" + "&ctrl=TBI.SharePoint.Employees.WebParts/EmployeeFlyout" +) + +SETTINGS_FILE_NAME = "settings" + +PERSON_PLACEHOLDER_PHOTO = "iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAkbSURBVHhe7Z0LUxNJFIX3//8bRPDBW1FUFA0g+AAEBN+KBQHxgeOe1LBbu7nTmSQzSd/Od6q+Klezydx0n8x09+3bf42NjWUAYINBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBBsjU1FS2srKSbW9vZ+/fv8+Ojo5afP/+/Z8/v3v3rvXvep1eb70PDA4MUjPXr1/PXrx4kZ2enma9SP/fy5cvs9nZWfP9oV4wSE3Mz89n+/v72cXFRd7V+9eXL1+ypaUl8/OgHjBIxczNzWUfP37Mu3Q9klFkQOvzoVowSEWMj49nz58/r/SOEdLv37+zra2t7OrVq+b1QDVgkAq4ceNG61d9GNLnapxjXRf0Dwbpk4WFhez8/DzvrsPR2dlZ69HOuj7oDwzSB7dv385+/vyZd9PhStfBAL56MEiP3Lp1K/v161fePeOQxiWPHj0yrxd6A4P0wPT0dGtxL0ZpkuDu3bvmdUP3YJAumZiYyE5OTvLuGKd0Z2NhsRowSJfs7u7m3TBuHR8fMwVcARikC5aXl/Pu50N7e3tmHFAeDFISPVp9+/Yt73o+pEE7K+79gUFKooRDj/r8+bMZD5QDg5Tg2rVr0ax39KIHDx6YcUFnMEgJvN49LtVsNlu5YlZsEAaDdEBjD893j0utrq6a8UEYDNKBx48f513MtzTta8UHYTBIB4aVpVuHFhcXzRihGAwS4ObNm3nXSkPa4WjFCcVgkACNRiPvWmlI+WNXrlwxYwUbDBLg7du3eddKR0rRt2IFGwwSINaM3X707NkzM1awwSAFpDb+uJQKSljxgg0GKeD+/ft5l0pLP378MOMFGwxSwPr6et6l0pOKTFgxQzsYpACV/0xVd+7cMWOGdjBIAYeHh3l3Sk+q+2vFDO1gkAJURDpVra2tmTFDOxikAO2jSFWbm5tmzNAOBikgZYMofd+KGdrBIAXo/I5UpeMUrJihHQxSQMqDdBXZtmKGdjBIAcp8TVUbGxtmzNAOBilAv7Kp6smTJ2bM0A4GKUA1blMVpUnLg0EKUHHqVDUzM2PGDO1gkAJUtlOF11KT6vayaao8GCSAjmVOTdpjb8UKNhgkQIoJi4rJihVsMEiAe/fu5d0qHSkmK1awwSABNA5Jadutxh+Tk5NmrGCDQTqQ0oKhilBYMUIxGKQDKU33UsS6ezBICVKYzaImVm9gkBLol9e7KPfTGxikJJ73h6g6vc44seKCMBikJDrKzOvKOuntvYNBuuD169d5l/MjnauoM06seKAzGKQLVE/K22E6ykq2YoFyYJAuUckcLzo4ODBjgPJgkB7w8Kh1dnbGwLwCMEgPKAUl5pOnlFKiBU7r2qE7MEiP6Nf55OQk75LxSDNtKrxtXTN0DwbpAw3aYzKJzEFZ0WrBIH2iO0kMqSgXFxfZ8vKyeY3QOxikAjQmGebA/fz8nKPVagKDVIjK6eiAmkFKJ0Zx3kd9YJCK0SPXq1evak9LkRGpb1U/GKQmZmdna6nvq9QRnX7FzsDBgEFqRusRe3t7faeofP36tZU2wp6OwYJBCtBzvY4q02OMsmHVOZXRa722DBrILy0ttSqra5ExZBg9nuk1qkCiz52enjbfs1sWFhZa8ezs7GS7u7utuDQtrLud9XrAIP9BFT+0b1sr0UXSukdVCYAyoTrt/7Fe2w8aF3WaZdOOQ+2/13fAXepfRt4gU1NTrQNlNFXajT59+tQ6S916z5jQ3afZbOZXXU76Lp4+fUou19+MpEH0C6m7gDp5P9JMUsyFEBRjP2Mf3Uk1I6cfEev9R4GRM4g69PHxcd4FqpGe5zXGsD5vGGiGq+qFS71fVWMhT4yMQdS4/d4xQtLYZG5uzvzsQaKJAE0F1yGls+iOMkqPXiNhEM3UDGInoB5JVldXzWuoG22r1Z1sENKAflTOGEnaIBprDKMyombCBvkru7i4mJ2enuafPjg1Gg3zelIiWYOMj49nb968yZty8NKvbFXTwUXIhIO6axRJs13WtaVCkgbRnUO/4jFIC35Vz3RdTk0POjHSkhY1U64Yn6RBtFIcm7RHfGtrq6dndxleq/qqjhhjATsZNdUZruQMok1DsUuPX0pT1+OROr3SP7Sf43IlXf+tv9e/6w6k2aPYpanzFBMokzKInsm7XRFH1SnF4xWSMojm6NFwVffExKBJxiAauNa9SQl1lqabNYNotZFHkjGI9lygOLS2tma2kUeSMIgGh6EUdTRYKe3GaiePJGEQreiiuJRKlZUkDBJzGdBRlR55rbbyhnuDKIMWxadUzkR0bxDtq0ZxKoUC2u4NUuceD9SfNjY2zDbzhGuD6Bbu7cSnUZLqglnt5gnXBtHiIIpXGodY7eYJ1wbRORgobnnP8nVtEG3WQXHr4cOHZtt5wbVBhr2bDnWW9zPaXRuEBcL4pW3PVtt5wbVB2PsRv7SRymo7L7g1iAq1ofil3ZBW+3nBrUFUkRz5kOcTsNwaRJU0kA95TjlxaxBVMEQ+5Pn0XbcG0UE0yIc87zB0a5DDw8P860exy/NaiFuDsAbiR6o2Y7WhB9wahDUQPzo4ODDb0AMuDaKyMpT48aMPHz6Y7egBlwYhzd2Xjo6OzHb0gEuDqGIG8iMVk7Pa0QMuDaLylsiPtOvTakcPuDTI+vp6/tUjL4rpkNNucGmQ7e3t/GtHXuQ1H8ulQVgk9Kf5+XmzLWPHpUFiPGUJhaUTsqy2jB2XBmk2m/nXjrzIa8KiS4NQyd2fdKyc1Zax484gOmYN+dPm5qbZnrHjziAUq/YpnfBrtWfsuDOIjlFG/rS/v2+2Z+y4M8jKykr+lSNP8noCrjuDqGI48idV4bfaM3bcGWRnZyf/ypEnea2P5c4gqtSH/Ekb3Kz2jB13BmGrrU95LSDnziCsovvVxMSE2aYx484grKL7lceMXlcGmZyczL9q5FEzMzNmu8aMK4PoC0Z+5bEEqSuDsBfdtzymvLsyiFKmkV/pTEmrXWPGlUFU4xX5lYptWO0aM64MohqvyK8ajYbZrvEylv0BRxk2BoRZQcsAAAAASUVORK5CYII=" diff --git a/job_compassplus/portal_compassplus_com/employees_gui/db.py b/job_compassplus/portal_compassplus_com/employees_gui/db.py new file mode 100644 index 000000000..7634add2a --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/db.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import os + +from urllib.parse import urljoin + +from lxml import etree + +from sqlalchemy import Column, String, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +import config + + +# TODO: добавить модуль logging + + +Base = declarative_base() + + +class Employee(Base): + """ + Класс описывает сотрудника. + + """ + + __tablename__ = "Employees" + + id = Column(String, primary_key=True) + url = Column(String) + name = Column(String) + short_name = Column(String) + birthday = Column(String) + job = Column(String) + department = Column(String) + photo = Column(String) + work_phone = Column(String) + mobile_phone = Column(String) + email = Column(String) + + @staticmethod + def parse(xml, session): + """Функция парсит строку xml с информацией о сотруднике и возвращает заполненный объект Employee.""" + + root = etree.HTML(xml) + + employee = Employee() + + # Если одно из них не будет определено, прекратить парсинг + try: + employee.id = root.xpath('//node()[@class="lookup-item"]/@id')[0].replace( + "user-", "" + ) + employee.short_name = root.xpath('//node()[@class="lookup-item"]/@value')[0] + employee.name = root.xpath('//node()[@class="lookup-item"]/@name')[0] + employee.url = root.xpath('//node()[@class="employee-name"]//a/@href')[0] + except IndexError as e: + print("error", "employee.id/short_name/name/url", e) + raise Exception("Bad parsing!") + + try: + # Загрузка страницы пользователя и вытаскивание дня его рождения + rs = session.get(employee.url) + user_root = etree.HTML(rs.text) + rs = user_root.xpath( + '//node()[contains(@id, "Birthday")]' + '/node()[@class="ms-tableCell ms-profile-detailsValue"]/text()' + ) + employee.birthday = rs[0] + except Exception as e: + print("error", "employee.birthday", e, employee.url) + employee.birthday = "" + + try: + photo_url = root.xpath('//node()[@class="employee-photo"]/img/@src')[0] + + # Относительный адрес делаем абсолютным + if photo_url.startswith("/"): + photo_url = urljoin(config.URL, photo_url) + + rs = session.get(photo_url) + if rs.ok: + employee.photo = base64.b64encode(rs.content).decode() + + except Exception as e: + print("warn", "employee.photo", e) + employee.photo = "" + + try: + employee.job = root.xpath( + '//node()[@class="employee-jobtitle"]/span[2]/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.job", e) + employee.job = "" + + try: + employee.department = root.xpath( + '//node()[@class="employee-department"]/span[2]/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.department", e) + employee.department = "" + + try: + employee.work_phone = "" + employee.mobile_phone = "" + + for div_phone in root.xpath('//node()[@class="employee-workphone"]'): + title, text = [el.text.strip() for el in div_phone.getchildren()] + if "Work" in title: + employee.work_phone = text + + elif "Mobile" in title: + employee.mobile_phone = text + + except Exception as e: + print("warn", "employee.work_phone/mobile_phone", e) + pass + + try: + employee.email = root.xpath( + '//node()[@class="employee-email"]/a/span/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.email", e) + employee.email = "" + + return employee + + def __str__(self) -> str: + return f'' + + def __repr__(self) -> str: + return self.__str__() + + +def get_session(): + DIR = os.path.dirname(__file__) + DB_FILE_NAME = "sqlite:///" + os.path.join(DIR, "database") + # DB_FILE_NAME = 'sqlite:///:memory:' + + # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) + engine = create_engine( + DB_FILE_NAME, + # echo=True, + pool_recycle=7200, + ) + + Base.metadata.create_all(engine) + + Session = sessionmaker(bind=engine) + return Session() + + +db_session = get_session() + + +def exists(employee_id) -> bool: + return ( + db_session.query(Employee).filter(Employee.id == employee_id).scalar() + is not None + ) diff --git a/job_compassplus/portal_compassplus_com/employees_gui/main.py b/job_compassplus/portal_compassplus_com/employees_gui/main.py new file mode 100644 index 000000000..d77012be3 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/main.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import sys +import traceback + +import requests + +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * + +from requests_ntlm2 import HttpNtlmAuth + +from sqlalchemy import or_ + +import config +from db import * + + +# Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print("Error: ", text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +def get_url(page): + return config.URL_GET_EMPLOYEES_LIST.format((page - 1) * 50) + + +# # TODO: показывать короткое имя пользователя: ipetrash, ypaliy и т.п. +# +# if __name__ == '__main__': +# fill_db() + + +def pixmap_from_base64(base64_text): + pixmap = QPixmap() + pixmap.loadFromData(base64.b64decode(base64_text)) + pixmap = pixmap.scaledToWidth(192, Qt.SmoothTransformation) + + return pixmap + + +class EmployeeInfo(QWidget): + def __init__(self) -> None: + super().__init__() + + self.photo = QLabel() + self.name = QLabel() + self.short_name = QLabel() + self.job = QLabel() + self.department = QLabel() + # TODO: показывать также сколько осталось до др + self.birthday = QLabel() + + self.url = QLabel() + self.url.setOpenExternalLinks(True) + + self.work_phone = QLabel() + self.mobile_phone = QLabel() + self.id = QLabel() + + self.email = QLabel() + self.email.setOpenExternalLinks(True) + + form_layout = QFormLayout() + form_layout.addRow("Name:", self.name) + form_layout.addRow("Short Name:", self.short_name) + form_layout.addRow("Job:", self.job) + form_layout.addRow("Department:", self.department) + form_layout.addRow("Birthday:", self.birthday) + form_layout.addRow("Url:", self.url) + form_layout.addRow("Work Phone:", self.work_phone) + form_layout.addRow("Mobile Phone:", self.mobile_phone) + form_layout.addRow("Id:", self.id) + form_layout.addRow("Email:", self.email) + + layout = QVBoxLayout() + layout.addWidget(self.photo) + layout.addLayout(form_layout) + layout.addStretch() + + scroll_area_widget = QWidget() + scroll_area_widget.setLayout(layout) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setWidget(scroll_area_widget) + + layout = QVBoxLayout() + layout.addWidget(scroll_area) + self.setLayout(layout) + + # Все QLabel на форме умеет поддержку выделения и при наличии ссылок на них можно кликать + for label in self.findChildren(QLabel): + label.setTextInteractionFlags(Qt.TextBrowserInteraction) + + def set_employee(self, employee) -> None: + if not employee: + self.photo.setPixmap(pixmap_from_base64(config.PERSON_PLACEHOLDER_PHOTO)) + + self.name.setText("None") + self.short_name.setText("None") + self.job.setText("None") + self.department.setText("None") + self.birthday.setText("None") + self.url.setText("None") + self.work_phone.setText("None") + self.mobile_phone.setText("None") + self.id.setText("None") + self.email.setText("None") + return + + self.photo.setPixmap(pixmap_from_base64(employee.photo)) + + self.name.setText(employee.name) + self.short_name.setText(employee.short_name) + self.job.setText(employee.job) + self.department.setText(employee.department) + self.birthday.setText(employee.birthday) + self.url.setText(f'{employee.url}') + self.work_phone.setText(employee.work_phone) + self.mobile_phone.setText(employee.mobile_phone) + self.id.setText(employee.id) + self.email.setText(f'{employee.email}') + + +# TODO: ввод с клавы при фокусе на таблицу меняет редактор фильтра +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Compass Plus Employees") + self.setContextMenuPolicy(Qt.NoContextMenu) + + # TODO: окно с информацией о выделенном сотруднике умеет показывать его переработку/недоработку и прочее + self.filter_line_edit = QLineEdit() + # При изменении окна происходит вызов run_filter и отображение/скрытие кнопки очистки текста + self.filter_line_edit.textChanged.connect(self.run_filter) + self.filter_line_edit.installEventFilter(self) + + try: + # Добавление в редактор фильтра кнопки очищения содержимого + clear_icon = self.style().standardIcon(QStyle.SP_LineEditClearButton) + + clear_action = self.filter_line_edit.addAction( + clear_icon, QLineEdit.TrailingPosition + ) + clear_action.setVisible(len(self.filter_line_edit.text()) > 0) + clear_action.triggered.connect(self.filter_line_edit.clear) + + self.filter_line_edit.textChanged.connect( + lambda text: clear_action.setVisible(len(text) > 0) + ) + + # Если SP_LineEditClearButton не найден + except AttributeError: + # TODO: сделать реализацию для Qt4 + # clear_icon = pixmap_from_base64(config.LINE_EDIT_CLEAR_BUTTON_32x32) + pass + + self.employees_table = QTableWidget() + self.employees_table.setSelectionBehavior(QTableWidget.SelectRows) + self.employees_table.setSelectionMode(QTableWidget.SingleSelection) + self.employees_table.currentItemChanged.connect( + lambda item, _: self._item_click(item) + ) + + layout_filter = QHBoxLayout() + layout_filter.addWidget(QLabel("Filter:")) + layout_filter.addWidget(self.filter_line_edit) + + layout = QVBoxLayout() + layout.addLayout(layout_filter) + layout.addWidget(self.employees_table) + + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + employee_info_dock_widget = QDockWidget("Employee Info") + employee_info_dock_widget.setObjectName("employee_info_dock_widget") + employee_info_dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures) + + self.employee_info = EmployeeInfo() + employee_info_dock_widget.setWidget(self.employee_info) + self.addDockWidget(Qt.RightDockWidgetArea, employee_info_dock_widget) + + tool_bar = self.addToolBar("General") + tool_bar.setObjectName("General") + action_refill = tool_bar.addAction("Parse and refill") + action_refill.setToolTip( + "Clear database, parse site with employees, and fill database" + ) + action_refill.setStatusTip(action_refill.toolTip()) + action_refill.triggered.connect(self.refill) + + self.setStatusBar(QStatusBar()) + + def refill(self) -> None: + # TODO: можно в отдельный класс вынести + dialog = QDialog() + dialog.setWindowTitle("Auth and refill database") + + info = QLabel() + info.setText( + """When you click on OK, you will be cleansing a database of employees, +start parsing for the collection of employees and populate the database.""" + ) + + login = QLineEdit("CP\\") + password = QLineEdit() + password.setEchoMode(QLineEdit.Password) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + + form_layout = QFormLayout() + form_layout.addRow("Login:", login) + form_layout.addRow("Password:", password) + + layout = QVBoxLayout() + layout.addWidget(info) + layout.addLayout(form_layout) + layout.addStretch() + layout.addWidget(button_box) + + dialog.setLayout(layout) + if not dialog.exec(): + return + + login, password = login.text(), password.text() + + # Сначала пытаемся авторизоваться + session = requests.Session() + session.auth = HttpNtlmAuth(login, password, session) + + # Авторизация + rs = session.get(config.URL) + if not rs.ok: + QMessageBox.information(self, "Info", "Failed to login") + print("Не удалось авторизоваться") + print(f"rs.status_code = {rs.status_code}") + print(f"rs.headers = {rs.headers}") + return + + # TODO: move to db.py + # Очищение базы данных + for i in db_session.query(Employee): + db_session.delete(i) + db_session.commit() + + self.run_filter() + self.fill_db(session) + self.run_filter() + + def fill_db(self, session) -> None: + page = 1 + + rs = session.get(get_url(page)) + data = rs.json() + + max_page = data["Pages"] + + # TODO: наверное тоже нужно в QProgressDialog обернуть, хоть сбор и быстрый + employee_list = list() + employee_list += data["Properties"] + + while page < max_page: + page += 1 + + rs = session.get(get_url(page)) + data = rs.json() + + employee_list += data["Properties"] + + # Для отображения диалога парсинга и заполнения базы + progress = QProgressDialog( + "Operation in progress...", "Cancel", 0, len(employee_list), self + ) + progress.setWindowTitle("Parsing") + progress.setWindowModality(Qt.WindowModal) + + for i, row in enumerate(employee_list, 1): + progress.setValue(i) + + if progress.wasCanceled(): + break + + employee_id = row["Id"] + + if exists(employee_id): + print(f"Employee with id = {employee_id} already exist.") + continue + + rs = session.get(config.URL_GET_EMPLOYEE_INFO.format(employee_id)) + if not rs.ok: + print( + f"Request getting employee info (id={employee_id}) not ok." + ) + print(f"rs.status_code = {rs.status_code}") + print(f"rs.headers = {rs.headers}") + continue + + employee = Employee.parse(rs.text, session) + print(i, employee) + + db_session.add(employee) + + db_session.commit() + + progress.setValue(len(employee_list)) + + def _item_click(self, item) -> None: + employee = None + + if item and self.employees_table.rowCount() > 0: + item = self.employees_table.item(item.row(), 0) + employee = item.data(Qt.UserRole) + + self.employee_info.set_employee(employee) + + def run_filter(self) -> None: + # TODO: лучше использовать модель + # TODO: лучше использовать стандартный фильтр qt + # TODO: поиграться с делегатами для красивого отображения описания + ссылки на гист + + self.employees_table.clear() + self._item_click(None) + + # TODO: db.py + filter_text = self.filter_line_edit.text() + filter_text = f"%{filter_text}%" + sql_filter = or_( + Employee.name.like(filter_text), + Employee.short_name.like(filter_text), + Employee.job.like(filter_text), + Employee.department.like(filter_text), + Employee.birthday.like(filter_text), + Employee.work_phone.like(filter_text), + Employee.mobile_phone.like(filter_text), + Employee.id.like(filter_text), + Employee.email.like(filter_text), + ) + + query = db_session.query(Employee).filter(sql_filter) + rows = query.count() + + self.employees_table.setRowCount(rows) + + headers = [ + "Name", + "Short Name", + "Job", + "Department", + "Birthday", + "Url", + "Work Phone", + "Mobile Phone", + "Id", + "Email", + "Photo", + ] + self.employees_table.setColumnCount(len(headers)) + self.employees_table.setHorizontalHeaderLabels(headers) + + row = 0 + for employee in query: + self.employees_table.setItem(row, 0, QTableWidgetItem(employee.name)) + self.employees_table.setItem(row, 1, QTableWidgetItem(employee.short_name)) + self.employees_table.setItem(row, 2, QTableWidgetItem(employee.job)) + self.employees_table.setItem(row, 3, QTableWidgetItem(employee.department)) + self.employees_table.setItem(row, 4, QTableWidgetItem(employee.birthday)) + self.employees_table.setItem(row, 5, QTableWidgetItem(employee.url)) + self.employees_table.setItem(row, 6, QTableWidgetItem(employee.work_phone)) + self.employees_table.setItem( + row, 7, QTableWidgetItem(employee.mobile_phone) + ) + self.employees_table.setItem(row, 8, QTableWidgetItem(employee.id)) + self.employees_table.setItem(row, 9, QTableWidgetItem(employee.email)) + self.employees_table.setItem(row, 10, QTableWidgetItem(employee.photo)) + + self.employees_table.item(row, 0).setData(Qt.UserRole, employee) + + row += 1 + + # Запрет редактирования ячеек таблицы + for row in range(self.employees_table.rowCount()): + for column in range(self.employees_table.columnCount()): + self.employees_table.item(row, column).setFlags( + Qt.ItemIsSelectable | Qt.ItemIsEnabled + ) + + # Показываем информацию о первом сотруднике + if self.employees_table.rowCount() > 0: + item = self.employees_table.item(0, 0) + self.employees_table.setCurrentItem(item) + + def read_settings(self) -> None: + ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) + + state = ini.value("MainWindow_State") + if state: + self.restoreState(state) + + geometry = ini.value("MainWindow_Geometry") + if geometry: + self.restoreGeometry(geometry) + + def write_settings(self) -> None: + ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) + ini.setValue("MainWindow_State", self.saveState()) + ini.setValue("MainWindow_Geometry", self.saveGeometry()) + + def eventFilter(self, object, event): + # В окне вводе при клике на стрелку вниз фокус переходит в таблицу + if object == self.filter_line_edit: + if event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Down: + self.employees_table.setFocus() + return False + + return super().eventFilter(object, event) + + def closeEvent(self, _) -> None: + self.write_settings() + + QApplication.instance().quit() + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.resize(1000, 750) + mw.read_settings() + mw.show() + mw.run_filter() + # TODO: + mw.employees_table.setFocus() + + app.exec_() diff --git a/job_compassplus/portal_compassplus_com/get_vacation_forecast.py b/job_compassplus/portal_compassplus_com/get_vacation_forecast.py new file mode 100644 index 000000000..dd2f35a2f --- /dev/null +++ b/job_compassplus/portal_compassplus_com/get_vacation_forecast.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post + + +def get_vacation_forecast() -> int: + url = "https://helpdesk.compassluxe.com/pa-reports-new/vacation/VacationForecast" + rs = do_get(url) + data: dict[str, int] = rs.json() + return data["id"] + + +if __name__ == "__main__": + vacation_forecast: int = get_vacation_forecast() + print(f"Vacations:", vacation_forecast) diff --git a/job_compassplus/portal_compassplus_com/get_vacations.py b/job_compassplus/portal_compassplus_com/get_vacations.py new file mode 100644 index 000000000..25ed9e379 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/get_vacations.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from dataclasses import dataclass +from datetime import datetime, date, timezone +from pathlib import Path +from typing import Any + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post + + +@dataclass +class Vacation: + id: int + subject: str + subject_email: str + start_date: date + end_date: date + deputy: str + + @classmethod + def parse_from_dict(cls, data: dict[str, Any]) -> "Vacation": + def utc2local(dt_utc: datetime) -> datetime: + epoch = dt_utc.timestamp() + dt_local = datetime.fromtimestamp(epoch).replace(tzinfo=None) + + offset = dt_local - dt_utc.replace(tzinfo=None) + return (dt_utc + offset).replace(tzinfo=None) + + def parse_datetime(date_time_str: str) -> datetime: + # NOTE: Разбор даты из UTC "2024-08-18T19:00:00Z" в локальную дату + return utc2local(datetime.strptime(date_time_str, "%Y-%m-%dT%H:%M:%S%z")) + + return cls( + id=data["Id"], + subject=data["Subject"], + subject_email=data["EmailEmployee"], + start_date=parse_datetime(data["StartDate"]), + end_date=parse_datetime(data["EndDate"]), + deputy=data["Location"], + ) + + +def get_vacations() -> list[Vacation]: + url = "https://portal.compassplus.com/_api/web/lists/GetByTitle('Employee%20Vacations')/items" + rs = do_get( + url, + headers={ + # NOTE: С такими заголовками сервер вернет JSON, а не XML + "Accept": "application/json;odata.metadata=minimal", + "odata-version": "4.0", + }, + params={ + "$select": "Id,EndDate,Subject,Location,StartDate,EmailEmployee", + "$top": "5000", + "$orderby": "Subject asc", + "$filter": f"(EndDate ge '{datetime.now(timezone.utc).date()}T00:00:00')", + }, + ) + return [Vacation.parse_from_dict(value) for value in rs.json()["value"]] + + +if __name__ == "__main__": + vacations = get_vacations() + print(f"Vacations ({len(vacations)}):") + for i, vacation in enumerate(vacations, 1): + print(f" {i}. {vacation}") diff --git a/job_compassplus/portal_compassplus_com/notify_about_new_employees.py b/job_compassplus/portal_compassplus_com/notify_about_new_employees.py new file mode 100644 index 000000000..737926e0e --- /dev/null +++ b/job_compassplus/portal_compassplus_com/notify_about_new_employees.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import traceback +import time + +from urllib.parse import urljoin + +import requests +from playwright.sync_api import sync_playwright, Page + + +URL_PORTAL: str = os.getenv("URL_PORTAL") +URL_NOTIFY: str = os.getenv("URL_NOTIFY") + +if not URL_PORTAL: + raise Exception("URL_PORTAL environment is not set!") + +if not URL_NOTIFY: + raise Exception("URL_NOTIFY environment variable is not set!") + + +def parse(page: Page, url_portal: str = URL_PORTAL, url_notify: str = URL_NOTIFY) -> None: + print(f"Opening page: {url_portal}") + page.goto(url_portal) + + css_selector = "a#NameFieldLink[href]" + + items = page.locator(css_selector).all() + print("Users:", len(items)) + + for user_el in items: + url_mysite: str = user_el.get_attribute("href") + username: str = url_mysite.rsplit("\\")[-1] + + full_name: str = user_el.text_content().strip() + + print(f"Check {full_name!r} ({username})") + + url_check: str = urljoin(url_notify, username) + print(f"Sending a notification: {url_check}") + + rs = requests.get(url_check) + rs.raise_for_status() + + # 201 вернется, если в ходе запроса был добавлен пользователь + print(f"[{'+' if rs.status_code == 201 else '='}] {full_name!r} ({username})") + print() + + +with sync_playwright() as p: + print("Launching a browser") + browser = p.firefox.launch() + + try: + page = browser.new_page() + + max_attempts = 3 + attempt = 0 + while True: + try: + attempt += 1 + parse(page) + break + except Exception as e: + print(f"#{attempt}.\n{traceback.format_exc()}") + + if attempt >= max_attempts: + raise e + + time.sleep(30) + + finally: + browser.close() diff --git a/job_compassplus/print_connections__radixware_explorer.py b/job_compassplus/print_connections__radixware_explorer.py new file mode 100644 index 000000000..d20b36e50 --- /dev/null +++ b/job_compassplus/print_connections__radixware_explorer.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from bs4 import BeautifulSoup + +# pip install tabulate +from tabulate import tabulate + + +FILE_NAME = r"%APPDATA%\radixware.org\explorer\connections.xml" +ABS_FILE_NAME = os.path.expandvars(FILE_NAME) + + +with open(ABS_FILE_NAME, "rb") as f: + root = BeautifulSoup(f.read(), "html.parser") + + +def get_tag_text(tag): + return "" if tag is None else tag.text + + +headers = [ + "NAME", + "ID", + "COMMENT", + "USERNAME", + "STATIONNAME", + "INITIALADDRESS", + "LANGUAGE", + "COUNTRY", + "EXPLORERROOTID", + "TRACELEVEL", +] +rows = [] + +for connection in root.select("connection"): + rows.append( + [ + connection["name"], + connection["id"], + get_tag_text(connection.comment), + get_tag_text(connection.username), + get_tag_text(connection.stationname), + get_tag_text(connection.initialaddress), + get_tag_text(connection.language), + get_tag_text(connection.country), + get_tag_text(connection.explorerrootid), + get_tag_text(connection.tracelevel), + ] + ) + + +print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/job_compassplus/print_statistics_by_img.py b/job_compassplus/print_statistics_by_img.py new file mode 100644 index 000000000..c07c1779b --- /dev/null +++ b/job_compassplus/print_statistics_by_img.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter, defaultdict +from hashlib import sha1 +from pathlib import Path + + +def process(path: Path) -> None: + files: list[Path] = [ + p + for p in path.glob("*/ads/*/img/*") + if p.suffix.lower() in [".svg", ".png", ".img", ".gif", ".jpg"] + ] + + suffix_by_number = Counter(p.suffix.lower() for p in files) + print(f"Suffixes ({len(suffix_by_number)}):") + for suffix, number in suffix_by_number.items(): + print(f" {suffix}: {number}") + + print() + + duplicated: dict[str, list[Path]] = defaultdict(list) + for p in files: + hash_file = sha1(p.read_bytes()).hexdigest() + duplicated[hash_file].append(p) + + duplicated = {k: v for k, v in duplicated.items() if len(v) > 1} + + print(f"Duplicates ({len(duplicated)}):") + for hash_file, items in sorted(duplicated.items(), key=lambda x: len(x[1]), reverse=True): + print(f" Hash {hash_file} ({len(items)}):") + for p in items: + print(f" {p}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Search in locale" + ) + parser.add_argument( + "path_trunk", + type=Path, + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/root_common.py b/job_compassplus/root_common.py new file mode 100644 index 000000000..bad178e83 --- /dev/null +++ b/job_compassplus/root_common.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import ssl + +import requests + +from root_config import PATH_CERT + + +class TLSAdapter(requests.adapters.HTTPAdapter): + def init_poolmanager(self, *args, **kwargs): + ctx = ssl.create_default_context() + ctx.set_ciphers("DEFAULT@SECLEVEL=1") + kwargs["ssl_context"] = ctx + return super(TLSAdapter, self).init_poolmanager(*args, **kwargs) + + +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" +) + + +session = requests.session() +session.cert = str(PATH_CERT) +session.mount("https://", TLSAdapter()) +session.headers["User-Agent"] = USER_AGENT + + +if __name__ == "__main__": + from root_config import JIRA_HOST + + # Check + rs = session.get(f"{JIRA_HOST}/pa-reports/") + print(rs) + rs.raise_for_status() + + rs = session.get(f"{JIRA_HOST}/secure/ViewProfile.jspa?name=ipetrash") + print(rs) + rs.raise_for_status() diff --git a/job_compassplus/root_config.py b/job_compassplus/root_config.py new file mode 100644 index 000000000..658a260af --- /dev/null +++ b/job_compassplus/root_config.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +JIRA_HOST = "https://helpdesk.compassluxe.com" + +# NOTE: Get : openssl pkcs12 -nodes -out ipetrash.pem -in ipetrash.p12 +NAME_CERT = "ipetrash.pem" + +ROOT_DIR = Path(__file__).resolve().parent +PATH_LOCAL_CERT = ROOT_DIR / NAME_CERT + +DIR_CURRENT_KEYS = Path(r"C:\keys\bin\current") +PATH_COMMON_CERT = DIR_CURRENT_KEYS / NAME_CERT + +if PATH_LOCAL_CERT.exists(): + PATH_CERT = PATH_LOCAL_CERT +elif PATH_COMMON_CERT.exists(): + PATH_CERT = PATH_COMMON_CERT +else: + raise Exception( + f"File {NAME_CERT} not found in: {PATH_LOCAL_CERT}, {PATH_COMMON_CERT}!" + ) + + +if __name__ == "__main__": + print(f"ROOT_DIR: {ROOT_DIR}") + print(f"PATH_LOCAL_CERT: {PATH_LOCAL_CERT} (exists: {PATH_LOCAL_CERT.exists()})") + print(f"PATH_COMMON_CERT: {PATH_COMMON_CERT} (exists: {PATH_COMMON_CERT.exists()})") + print(f"PATH_CERT: {PATH_CERT} (exists: {PATH_CERT.exists()})") diff --git a/job_compassplus/site_common.py b/job_compassplus/site_common.py new file mode 100644 index 000000000..84c281b99 --- /dev/null +++ b/job_compassplus/site_common.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +# pip install requests_ntlm==1.3.0 +from requests_ntlm import HttpNtlmAuth + +from requests import Response +from requests.exceptions import RequestException + +from root_common import session +from site_config_token import USERNAME, PASSWORD + + +def do_request(method, url: str, *args, **kwargs) -> Response: + attempts = 0 + max_attempts = 5 + + while True: + attempts += 1 + try: + rs = method( + url, + auth=HttpNtlmAuth(USERNAME, PASSWORD), + *args, + **kwargs + ) + + # Через какое-то отваливается доступ, почистим куки и заново авторизуемся + if rs.status_code == 401: + session.cookies.clear() + + rs.raise_for_status() + + return rs + + except RequestException as e: + if attempts >= max_attempts: + raise e + + time.sleep(5) # 5 seconds + + +def do_get(url: str, *args, **kwargs) -> Response: + return do_request(session.get, url, *args, **kwargs) + + +def do_post(url: str, *args, **kwargs) -> Response: + return do_request(session.post, url, *args, **kwargs) + + +if __name__ == "__main__": + rs = do_get("https://mysite.compassplus.com:443/Person.aspx?accountname=CP%5Cipetrash") + print(rs) + + rs = do_get("https://portal.compassplus.com/Pages/default.aspx") + print(rs) diff --git a/job_compassplus/site_config_token.py b/job_compassplus/site_config_token.py new file mode 100644 index 000000000..328724b8b --- /dev/null +++ b/job_compassplus/site_config_token.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import sys + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +TOKEN_FILE_NAME = DIR / "TOKEN.txt" +SEP = "|" + +try: + TOKEN = os.environ.get("TOKEN") + if not TOKEN: + TOKEN = TOKEN_FILE_NAME.read_text("utf-8").strip() + if not TOKEN: + raise Exception("TOKEN пустой!") + + USERNAME, PASSWORD = TOKEN.split(SEP) + +except: + print( + f"Нужно в {TOKEN_FILE_NAME.name} или в переменную окружения " + f"TOKEN добавить логин/пароль (формат <домен>\\<логин>{SEP}<пароль>)" + ) + TOKEN_FILE_NAME.touch() + sys.exit() diff --git a/job_compassplus/svn/find_release_version.py b/job_compassplus/svn/find_release_version.py new file mode 100644 index 000000000..cf783e534 --- /dev/null +++ b/job_compassplus/svn/find_release_version.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + +from get_last_release_version import get_last_release_version, URL_DEFAULT_SVN_PATH + + +def find_release_version( + text: str, + version: str, + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> str: + url = f"{url_svn_path}/{version}" + + end_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + # "--verbose", + "--xml", + "--search", + text, + "--revision", + # Если в паре значений первым идет большее значение, то поиск будет идти от большего к меньшему + f"HEAD:{{{end_date}}}", + url, + ] + ) + root = ET.fromstring(data) + + last_revision = None + for logentry_el in root.findall(".//logentry"): + last_revision = logentry_el.attrib["revision"] + break + + if not last_revision: + raise Exception("Не удалось найти ревизию!") + + last_release_version: str = get_last_release_version( + version=version, + start_revision=last_revision, + last_days=last_days, + url_svn_path=url_svn_path, + ) + + # Первый коммит, который искали попал уже в следующую версию, поэтому + # нужно добавить 1 к последней версии: + # 3.2.35.10.10 -> 3.2.35.10.11 + last_release_version = re.sub( + r"\.(\d+)$", # Последнее число в версии + lambda m: f".{int(m.group(1)) + 1}", # Увеличение числа на 1 + last_release_version + ) + + return last_release_version + + +if __name__ == '__main__': + text = "TXI-8197" + print(find_release_version(text=text, version="3.2.35.10")) + # 3.2.35.10.11 + + print(find_release_version(text=text, version="3.2.34.10")) + # 3.2.34.10.18 diff --git a/job_compassplus/svn/get_last_release_version.py b/job_compassplus/svn/get_last_release_version.py new file mode 100644 index 000000000..886ec6a6b --- /dev/null +++ b/job_compassplus/svn/get_last_release_version.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + + +TEXT_RELEASE_VERSION = "Release version " +PATTERN_RELEASE_VERSION = re.compile(rf"{TEXT_RELEASE_VERSION}([\d.]+) ") + +URL_DEFAULT_SVN_PATH = "svn+cplus://svn2.compassplus.ru/twrbs/trunk/dev" + + +def get_last_release_version( + version: str, + start_revision: str = "HEAD", + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> str: + url = f"{url_svn_path}/{version}" + + end_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + "--xml", + "--search", + TEXT_RELEASE_VERSION, + "--revision", + # Если в паре значений первым идет большее значение, то поиск будет идти от большего к меньшему + f"{start_revision}:{{{end_date}}}", + url, + ] + ) + root = ET.fromstring(data) + + last_release_version_msg = None + for logentry_el in root.findall(".//logentry"): + last_release_version_msg = logentry_el.find("msg").text + break + + if not last_release_version_msg: + raise Exception("Не удалось найти коммит релиза!") + + m = PATTERN_RELEASE_VERSION.search(last_release_version_msg) + if not m: + raise Exception( + f"Не удалось вытащить версию релиза из {last_release_version_msg!r}" + ) + + return m.group(1) + + +if __name__ == "__main__": + print(get_last_release_version(version="trunk", last_days=60)) + # 3.2.36.10 + + print(get_last_release_version(version="3.2.35.10")) + # 3.2.35.10.10 + + print(get_last_release_version(version="3.2.34.10")) + # 3.2.34.10.17 diff --git a/job_compassplus/svn/search_by_versions.py b/job_compassplus/svn/search_by_versions.py new file mode 100644 index 000000000..a225af75b --- /dev/null +++ b/job_compassplus/svn/search_by_versions.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + + +PATTERN_VERSION = re.compile(r"/dev/(.+?)/") +URL_DEFAULT_SVN_PATH = "svn+cplus://svn2.compassplus.ru/twrbs/trunk/dev" + + +def search( + text: str, + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> list[str]: + start_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + "--verbose", + "--xml", + "--search", + text, + "--revision", + # Порядок имеет значение - выдача ревизий тут будет от меньшей к большей + f"{{{start_date}}}:HEAD", + url_svn_path, + ] + ) + + root = ET.fromstring(data) + + versions = [] + for logentry_el in root.findall(".//logentry"): + for path_el in logentry_el.findall("./paths/path"): + if m := PATTERN_VERSION.search(path_el.text): + version = m.group(1) + if version not in versions: + versions.append(version) + + return versions + + +if __name__ == "__main__": + versions: list[str] = search(text="TXI-8210") + print(versions) + # ['trunk', '3.2.36.10', '3.2.35.10'] + + versions: list[str] = search( + text="OPTT-441", + last_days=365, + url_svn_path="svn+cplus://svn2.compassplus.ru/twrbs/csm/optt", + ) + print(versions) + # ['trunk', '2.1.12.1'] diff --git a/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py b/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py new file mode 100644 index 000000000..dd5f1b993 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") + +for prop_id in PROP_IDS: + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + + if not prop_el.Presentation.has_attr("IsPresentable"): + prop_el.Presentation["IsPresentable"] = "false" + +with open(FILE_NAME_ACL, "w", encoding="utf-8") as f: + f.write(str(root_acl)) diff --git a/job_compassplus/tx_parse_xml/acl__generate_new_Property.py b/job_compassplus/tx_parse_xml/acl__generate_new_Property.py new file mode 100644 index 000000000..cbaad3548 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__generate_new_Property.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import uuid + +from pathlib import Path + +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +FILE_NAME_ACL_LOCALE = ( + FILE_NAME_ACL.parent.parent / "locale" / "en" / ("mlb" + FILE_NAME_ACL.name) +) + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_locale = BeautifulSoup(open(FILE_NAME_ACL_LOCALE, "rb"), "xml") + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() +items = [] + +new_prop_ids = [] + +with open("new_props.txt", "w", encoding="utf-8") as f: + for prop_id in PROP_IDS: + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + prop_name = prop_el["Name"] + + title_id = prop_el.Presentation["TitleId"] + title = root_acl_locale.select_one(f'[Id="{title_id}"]').Value.text + + # print(name, title) + prop_el["Id"] = f"prd{uuid.uuid4().hex.upper()[:26]}" + prop_el["Name"] = f"{prop_name}_Title_{title}" + + new_prop_ids.append(prop_el["Id"]) + + new_src = f"""\ + + + return + + + + {prop_name} + + + + ; + + \ + """ + + new_prop_el_str = re.sub( + ".+?", new_src, str(prop_el), flags=re.DOTALL + ) + print(new_prop_el_str, file=f) + + f.write("\n\n") + f.write('new_prop_ids: "' + " ".join(new_prop_ids) + '"') diff --git a/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py b/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py new file mode 100644 index 000000000..cd308284d --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# Example: +# case "{OPERATION}": { +# Crypto::CryptoWsdl:{OPERATION}Document doc = Crypto::CryptoWsdl:{OPERATION}Document.Factory.newInstance(); +# doc.ensure{OPERATION}().ensure{OPERATION}Rq(); +# return doc; +# } +TEMPLATE_CASE = """\ + + case "{OPERATION}": { + + + + + Crypto::CryptoWsdl:{OPERATION}Document + + + + doc = + + + + Crypto::CryptoWsdl:{OPERATION}Document + + + + .Factory.newInstance(); + doc.ensure{OPERATION}().ensure{OPERATION}Rq(); + return doc; + } + +""" + +# Example: +# switch (operationName) { +# {SWITCHES} +# +# default: { +# return null; +# } +# } +TEMPLATE = """ + + + switch (operationName) { +{SWITCHES} + + default: { + return null; + } +} + + +""" + +ITEMS = [ + "CalcArpc", + "CalcArqc", + "CalcCsc", + "CalcCvv", + "CalcEmvMac", + "CalcHmac", + "CalcKeyCheck", + "CalcMac", + "CalcMacDukpt", + "CalcPinOffset", + "CalcPvv", + "CheckArpc", + "CheckArqc", + "CheckCapToken", + "CheckCsc", + "CheckCvc3", + "CheckCvv", + "CheckDcsc", + "CheckDcvv", + "CheckHmac", + "CheckKey", + "CheckMac", + "CheckMacDukpt", + "CheckPinOffset", + "CheckPvv", + "CheckRsaKey", + "DecryptDukpt", + "DesCrypt", + "DesRecrypt", + "EncryptDukpt", + "ExportEmvPinBlock", + "ExportHmacKey", + "ExportKey", + "ExportPinBlock", + "ExportRsaKey", + "GenHmacKey", + "GenKey", + "GenRsaKey", + "GeneratePin", + "GetHsmInfo", + "ImportClearPin", + "ImportHmacKey", + "ImportKey", + "ImportPinBlock", + "ImportPinBlockDukpt", + "ImportPinOffset", + "ImportRsaKey", + "JwtDecode", + "JwtEncode", + "MigrateKey", + "RebuildPinBlock", + "RklGenerateAndEncryptKey", + "RklImportPublicKey", + "RklImportRootPublicKey", + "RsaSign", + "RsaVerify", + "TdsSign", + "TranslateKey", +] + +cases = [TEMPLATE_CASE.replace("{OPERATION}", operation) for operation in ITEMS] +text = TEMPLATE.replace("{SWITCHES}", "\n".join(cases)) + +with open("switch_case.xml", "w", encoding="utf-8") as f: + print(text) + print(text, file=f) diff --git a/job_compassplus/tx_parse_xml/acl__prop_to_title.py b/job_compassplus/tx_parse_xml/acl__prop_to_title.py new file mode 100644 index 000000000..dbba66698 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__prop_to_title.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +FILE_NAME_ACL_LOCALE = ( + FILE_NAME_ACL.parent.parent / "locale" / "en" / ("mlb" + FILE_NAME_ACL.name) +) + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "html.parser") +root_acl_locale = BeautifulSoup(open(FILE_NAME_ACL_LOCALE, "rb"), "html.parser") + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() + +items = [] + +for prop_id in PROP_IDS: + prop_el = root_acl.select_one("#" + prop_id) + name = prop_el["name"] + + title_id = prop_el.presentation["titleid"] + title = root_acl_locale.select_one("#" + title_id).value.text + + items.append((name, title)) + +items.sort() + +for name, title in items: + print(name, title, sep="\t") diff --git a/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py b/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py new file mode 100644 index 000000000..e48cb39af --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +LICENSE_PATH = "com.tranzaxis/Interface/Online/W4/" + +TEMPLATE_THIS_PROP = """ + + + {prop_name} + + +""" +TEMPLATE = """ + + + + if ( + + {TEMPLATE_THIS_PROP} + + == null) { + try { + + + + + + + + + + + + + + + + + + {TEMPLATE_THIS_PROP} + + = true; + + } catch (java.lang.Exception e) { + + + {TEMPLATE_THIS_PROP} + + = false; + } +} + +return + + {TEMPLATE_THIS_PROP} + + ; + + + + +""" + +ITEMS = [...] + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_str = str(root_acl) + +for license_name, license_id, prop_name, prop_id in ITEMS: + print(license_name, license_id, prop_name, prop_id) + + new_getter_src = ( + TEMPLATE.replace("{TEMPLATE_THIS_PROP}", TEMPLATE_THIS_PROP) + .replace("{prop_id}", prop_id) + .replace("{prop_name}", prop_name) + .replace("{license_path}", LICENSE_PATH) + .replace("{license_name}", license_id) + .replace("{acl_id}", FILE_NAME_ACL.stem) + ) + + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + old_prop_el_str = str(prop_el) + new_prop_el_str = re.sub( + ".+?", + new_getter_src, + old_prop_el_str, + flags=re.DOTALL, + ) + root_acl_str = root_acl_str.replace(old_prop_el_str, new_prop_el_str) + +with open("new_" + FILE_NAME_ACL.name, "w", encoding="utf-8") as f: + f.write(root_acl_str) diff --git a/job_compassplus/tx_parse_xml/acl_update_name_props.py b/job_compassplus/tx_parse_xml/acl_update_name_props.py new file mode 100644 index 000000000..5c6d252d2 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl_update_name_props.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) + +ITEMS = [...] + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_str = str(root_acl) + +for license_name, license_id, prop_name, prop_id in ITEMS: + print(license_name, license_id, prop_name, prop_id) + + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + old_prop_el_str = str(prop_el) + + prop_el["Name"] = f"{prop_name}_{license_id}" + new_prop_el_str = str(prop_el) + + root_acl_str = root_acl_str.replace(old_prop_el_str, new_prop_el_str) + +with open("new_" + FILE_NAME_ACL.name, "w", encoding="utf-8") as f: + f.write(root_acl_str) diff --git a/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py b/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py new file mode 100644 index 000000000..0fa9efc26 --- /dev/null +++ b/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from pathlib import Path +from xml.etree.ElementTree import ParseError + + +NS = dict( + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def process(path: str): + if isinstance(path, str): + path = Path(path) + + if not path.exists(): + raise Exception(f"Not exists: {path}") + + assignee_by_number: dict[str, int] = defaultdict(int) + type_by_number: dict[str, int] = defaultdict(int) + behavior_by_number: dict[str, int] = defaultdict(int) + + for layer_dir in path.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir: Path = layer_dir / "ads" + if not layer_ads_dir.is_dir(): + continue + + for class_path in layer_ads_dir.glob("*/src/*.xml"): + try: + model = ET.fromstring(class_path.read_bytes()) + except ParseError as e: + print(f"[#] Invalid XML by path {str(class_path)!r}\nError: {e}\n") + continue + + for task_el in model.findall(".//xsc:Task", namespaces=NS): + assignee: str = task_el.attrib["Assignee"].strip() + if not assignee: + assignee = "" + + assignee_by_number[assignee] += 1 + + task_type: str = task_el.attrib["Type"] + type_by_number[task_type] += 1 + + behavior: str = task_el.attrib["Behavior"] + behavior_by_number[behavior] += 1 + + indent1: str = " " + + print("Total:", sum(assignee_by_number.values())) + print() + + print(f"assignee_by_number ({len(assignee_by_number)}):") + for assignee, number in sorted( + assignee_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{assignee}: {number}") + print() + + print(f"type_by_number ({len(type_by_number)}):") + for task_type, number in sorted( + type_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{task_type}: {number}") + print() + + print(f"behavior_by_number ({len(behavior_by_number)}):") + for behavior, number in sorted( + behavior_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{behavior}: {number}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Count TODO, NOTE, FIXME of ADS classes" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py b/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py new file mode 100644 index 000000000..bdf444a5c --- /dev/null +++ b/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from collections import Counter + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + + +def process(path_layer: str): + if isinstance(path_layer, str): + path_layer = Path(path_layer) + + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + total_files: int = 0 + module_by_size = Counter() + + for path_bin in Path(path_layer).glob("ads/*/bin"): + module: str = path_bin.parent.name + sizes: list[int] = [f.stat().st_size for f in path_bin.glob("*.jar")] + if not sizes: + continue + + total_files += len(sizes) + module_by_size[module] = sum(sizes) + + print(f"Total modules: {len(module_by_size)}") + print(f"Total files: {total_files}") + print("Top10 modules by size:") + for module, size in module_by_size.most_common(10): + print(f" {module}: {sizeof_fmt(size)} ({size} bytes)") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Counting size bin of ADS modules" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path_layer) diff --git a/job_compassplus/tx_parse_xml/export_ids.py b/job_compassplus/tx_parse_xml/export_ids.py new file mode 100644 index 000000000..e83ff4bb7 --- /dev/null +++ b/job_compassplus/tx_parse_xml/export_ids.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +PATTERN_ID: re.Pattern = re.compile(rb'\bId="([a-zA-Z]{3}\w+)"') + + +def process(path_layer: Path, path_extract: Path): + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + items: set[bytes] = set() + for f in path_layer.rglob("*.xml"): + # TODO: Добавить статистику: файлы, количество найденных + ids: list[bytes] = PATTERN_ID.findall(f.read_bytes()) + print(f"{f} ids: {len(ids)}") + items.update(ids) + + with open(path_extract, "wb") as f: + for value in items: + f.write(value + b"\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Exporting IDs to a file" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + type=Path, + ) + parser.add_argument( + "path_extract", + help="Path to extract", + type=Path, + ) + args = parser.parse_args() + + process(args.path_layer, args.path_extract) diff --git a/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py b/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py new file mode 100644 index 000000000..9bc577412 --- /dev/null +++ b/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import fnmatch +import argparse + +from pathlib import Path + + +def process( + root_path: Path, + include_glob: str, + exclude_patterns: list[str], + encoding: str = "utf-8", +) -> None: + file_exclude: Path = root_path / "svn_layers_exclude.bat" + file_restore: Path = root_path / "svn_layers_restore.bat" + + print(f"Scanning directory: {root_path.absolute()}") + + target_dirs: list[Path] = [p for p in root_path.glob(include_glob) if p.is_dir()] + if not target_dirs: + print("No matching directories found.") + return + + # Подготавливаем списки строк для записи + lines_exclude: list[str] = ["@echo off"] + lines_restore: list[str] = ["@echo off"] + + for p in target_dirs: + dir_name: str = p.name + is_excluded: bool = any( + fnmatch.fnmatch(dir_name, pat) for pat in exclude_patterns + ) + + prefix: str = "REM " if is_excluded else "" + lines_exclude.append(f"{prefix}svn update --set-depth exclude {dir_name}") + lines_restore.append(f"{prefix}svn update --set-depth infinity {dir_name}") + + file_exclude.write_text("\n".join(lines_exclude) + "\n", encoding=encoding) + file_restore.write_text("\n".join(lines_restore) + "\n", encoding=encoding) + + print(f"Done! Created '{file_exclude.name}' and '{file_restore.name}'") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generates SVN batch scripts to manage folder depth (exclude/include).", + ) + parser.add_argument( + "path", + metavar="PROJECT_PATH", + type=Path, + help="Path to the project directory", + ) + parser.add_argument( + "--include", + default="com.tranzaxis.*", + help="Glob pattern for directories to process (default: %(default)s)", + ) + parser.add_argument( + "--excludes", + nargs="+", + default=["com.tranzaxis.experimental", "com.tranzaxis.demo"], + help="Globs for directories to comment out in exclude script (default: %(default)s)", + ) + + args = parser.parse_args() + + if not args.path.exists(): + parser.error(f"The path {args.path} does not exist.") + + process(args.path, args.include, args.excludes) diff --git a/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py b/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py new file mode 100644 index 000000000..334458c19 --- /dev/null +++ b/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +ITEMS = [ + "W000", + "W001", + "W002", + "W003", + "W004", + "W005", + "W006", + "W007", + "W008", + "W009", + "W010", + "W011", + "W012", + "W014", + "W015", + "W016", + "W017", + "W018", + "W019", + "W021", + "W022", + "W023", + "W024", + "W025", + "W026", + "W027", + "W028", + "W029", + "W032", + "W033", + "W034", + "W035", + "W036", + "W037", +] + +text = "" + +for name in ITEMS: + text += f"""\ + + + +""" + +print(text) diff --git a/job_compassplus/tx_parse_xml/get_layer_version.py b/job_compassplus/tx_parse_xml/get_layer_version.py new file mode 100644 index 000000000..920a34654 --- /dev/null +++ b/job_compassplus/tx_parse_xml/get_layer_version.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +def get_layer_version(path: Path) -> str: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Path {str(path)!r} must be directory") + + path = path / "layer.xml" + if not path.exists(): + raise Exception(f"Path {str(path)!r} is not exists!") + + m = re.search(r'\bReleaseNumber="(.+?)"', path.read_text("utf-8")) + if not m: + raise Exception(f'Not found "ReleaseNumber" in path {str(path)!r}!') + + return m.group(1) + + +def process(path: Path) -> None: + print(get_layer_version(path)) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Print a layer version" + ) + parser.add_argument( + "path", + metavar="/PATH/TO/LAYER", + type=Path, + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/tx_parse_xml/get_project_versions.py b/job_compassplus/tx_parse_xml/get_project_versions.py new file mode 100644 index 000000000..2612cc5c6 --- /dev/null +++ b/job_compassplus/tx_parse_xml/get_project_versions.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess + +from dataclasses import dataclass, field, fields +from pathlib import Path +from typing import Optional, Any + + +def get_diff_fields(obj1, obj2) -> dict[str, tuple[Any, Any]]: + """ + Compares two dataclass instances and returns a dictionary of differing fields. + The dictionary keys are the field names, and values are a tuple + (value_in_obj1, value_in_obj2). + """ + + if not isinstance(obj1, type(obj2)): + raise TypeError("Both objects must be instances of the same dataclass.") + + diffs: dict[str, tuple[Any, Any]] = dict() + for field_info in fields(obj1): + field_name: str = field_info.name + value1: Any = getattr(obj1, field_name) + value2: Any = getattr(obj2, field_name) + + if value1 != value2: + diffs[field_name] = value1, value2 + + return diffs + + +def parse_xml_attrs(text: str) -> dict[str, str]: + return dict(re.findall(r'(\w+)\s*=\s*"(.+?)"', text)) + + +def parse_line_xml_text(text: str, line_contains: list[str]) -> dict[str, str]: + for line in text.splitlines(): + if all(x in line for x in line_contains): + return parse_xml_attrs(line) + + return dict() + + +def get_remote_file_from_svn(file_name: Path) -> str: + try: + result: bytes = subprocess.check_output( + args=["svn", "info", file_name], + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as e: + # NOTE: svn: warning: W155010: The node 'xxx' was not found. + if b"W155010" in e.stderr: + print(f"[#] Не найден {file_name} в SVN") + return "" + + raise e + + url_svn_file: str | None = None + for line in result.splitlines(): + parts: list[bytes] = line.split(b"URL:", maxsplit=1) + if len(parts) < 2: + continue + + url_svn_file = parts[1].strip().decode("ascii") + break + + if not url_svn_file: + raise Exception( + f"Не удалось получить URL файла {file_name} в репозитории SVN из результата svn info:\n{result!r}" + ) + + return subprocess.check_output( + args=["svn", "cat", url_svn_file], + encoding="utf-8", + ) + + +@dataclass +class BranchInfo: + # TODO: Не все поля были поддержаны + base_dev_uri: str + type: str + base_release: str | None + last_release: str | None + + @classmethod + def parse_from_dict(cls, d: dict[str, str]) -> "BranchInfo": + return cls( + base_dev_uri=d["BaseDevUri"], + type=d["Type"], + base_release=d.get("BaseRelease"), + last_release=d.get("LastRelease"), + ) + + @classmethod + def parse_from_text(cls, text: str) -> Optional["BranchInfo"]: + xml_attrs: dict[str, str] = parse_line_xml_text( + text=text, + line_contains=["Branch", "BaseDevUri", "Type"], + ) + if xml_attrs: + return cls.parse_from_dict(xml_attrs) + + +@dataclass +class LayerInfo: + # TODO: Не все поля были поддержаны + uri: str + name: str + release_number: str | None + base_layer_uris: list[str] = field(default_factory=list) + + @classmethod + def parse_from_dict(cls, d: dict[str, str]) -> "LayerInfo": + return cls( + uri=d["Uri"], + name=d["Name"], + release_number=d.get("ReleaseNumber"), + base_layer_uris=d.get("BaseLayerURIs", "").split(), + ) + + @classmethod + def parse_from_text(cls, text: str) -> Optional["LayerInfo"]: + xml_attrs: dict[str, str] = parse_line_xml_text( + text=text, + line_contains=["Layer", "Uri", "Name"], + ) + if xml_attrs: + return cls.parse_from_dict(xml_attrs) + + +@dataclass +class ProjectInfo: + branch: BranchInfo | None + layers: list[LayerInfo] + + +@dataclass +class TotalProjectInfo: + local: ProjectInfo + remote: ProjectInfo + + +def collect_all_layer_infos( + path_layer: Path, + layer_infos: list[LayerInfo], + remote: bool = False, +): + if not path_layer.exists(): + raise Exception(f"Путь {path_layer} не существует!") + + layer_info: LayerInfo | None = LayerInfo.parse_from_text( + get_remote_file_from_svn(path_layer) + if remote + else path_layer.read_text(encoding="utf-8") + ) + + if not layer_info: + text = f"Не удалось получить информацию из {path_layer}!" + if not remote: + raise Exception(f"{text} (local)") + + print(f"[#] {text} (remote)") + return + + if layer_info in layer_infos: + print(f"[#] Обнаружилось зацикливание с {layer_info}") + return + + layer_infos.append(layer_info) + + if not layer_info.base_layer_uris: + return + + for base_layer_uri in layer_info.base_layer_uris: + path_layer = path_layer.parent.parent / base_layer_uri / "layer.xml" + collect_all_layer_infos(path_layer, layer_infos, remote=remote) + + +def get_project_versions(path: Path) -> TotalProjectInfo: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Путь {path} должен быть директорией") + + path_branch = path / "branch.xml" + if not path_branch.exists(): + raise Exception(f"Не существует: {path_branch}") + + branch_info_local: BranchInfo | None = BranchInfo.parse_from_text( + path_branch.read_text(encoding="utf-8") + ) + if not branch_info_local: + raise Exception( + f"Не удалось получить информацию из {branch_info_local}! (local)" + ) + + branch_info_remote: BranchInfo | None = BranchInfo.parse_from_text( + get_remote_file_from_svn(path_branch) + ) + if not branch_info_remote: + print(f"[#] Не удалось получить информацию из {branch_info_remote}! (remote)") + + path_base_dev_layer = path / branch_info_local.base_dev_uri / "layer.xml" + + layer_infos_local: list[LayerInfo] = [] + collect_all_layer_infos(path_base_dev_layer, layer_infos_local, remote=False) + + layer_infos_remote: list[LayerInfo] = [] + collect_all_layer_infos(path_base_dev_layer, layer_infos_remote, remote=True) + + return TotalProjectInfo( + local=ProjectInfo( + branch=branch_info_local, + layers=layer_infos_local, + ), + remote=ProjectInfo( + branch=branch_info_remote, + layers=layer_infos_remote, + ), + ) + + +def process(path: Path) -> None: + print(f"Информация по версиям из {path}") + + total_project_info: TotalProjectInfo = get_project_versions(path) + + if not total_project_info.local.branch: + print() + print("[#] Local branch: <нет информации>") + elif not total_project_info.remote.branch: + print() + print("[#] Remote branch: <нет информации>") + else: + diff_branches: dict[str, tuple[Any, Any]] = get_diff_fields( + total_project_info.local.branch, total_project_info.remote.branch + ) + if diff_branches: + print() + + print("Обнаружена разница в полях Branch между local и remote:") + for field_name, (value_local, value_remote) in diff_branches.items(): + print(f" {field_name}: {value_local} != {value_remote}") + + @dataclass + class DiffLayersResult: + local: LayerInfo | None = None + remote: LayerInfo | None = None + + uri_by_layers: dict[str, DiffLayersResult] = { + uri: DiffLayersResult() + for uri in set( + l.uri + for l in ( + total_project_info.local.layers + total_project_info.remote.layers + ) + ) + } + + for l in total_project_info.local.layers: + uri_by_layers[l.uri].local = l + for l in total_project_info.remote.layers: + uri_by_layers[l.uri].remote = l + + diff_layers_local: list[LayerInfo] = [] + diff_layers_remote: list[LayerInfo] = [] + for uri, layers in uri_by_layers.items(): + if layers.local and layers.remote: + diff_layers: dict[str, tuple[Any, Any]] = get_diff_fields( + layers.local, layers.remote + ) + if diff_layers: + print() + print(f"Обнаружена разница в полях Layer ({uri}) между local и remote:") + for field_name, (value_local, value_remote) in diff_layers.items(): + print(f" {field_name}: {value_local} != {value_remote}") + + elif layers.local: + diff_layers_remote.append(layers.local) + elif layers.remote: + diff_layers_local.append(layers.remote) + + if diff_layers_local: + print() + print(f"В Local нет слоев из Remote ({len(diff_layers_local)}):") + for l in diff_layers_local: + print(f" {l}") + + if diff_layers_remote: + print() + print(f"В Local нет слоев из Remote ({len(diff_layers_remote)}):") + for l in diff_layers_remote: + print(f" {l}") + + print() + print("Версии:") + + def _print_project_info(project_info: ProjectInfo, is_local: bool) -> None: + ind1: str = " " + print(ind1 + ("Local:" if is_local else "Remote:")) + print(f"{ind1 * 2}Branch: {project_info.branch}") + print(f"{ind1 * 2}Layers:") + for layer in project_info.layers: + print(f"{ind1 * 3}{layer}") + + _print_project_info(total_project_info.local, is_local=True) + print() + _print_project_info(total_project_info.remote, is_local=False) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Print a layer version") + parser.add_argument( + "path", + metavar="/PATH/TO/PROJECT", + type=Path, + help="Path to project (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/tx_parse_xml/module_id_by_name.py b/job_compassplus/tx_parse_xml/module_id_by_name.py new file mode 100644 index 000000000..286ad1be2 --- /dev/null +++ b/job_compassplus/tx_parse_xml/module_id_by_name.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from show_maintainers import parse_xml_partially + + +def get_module_id_by_name(project_dir: str | Path) -> dict[str, str]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + module_id_by_name: dict[str, str] = dict() + for layer_path in project_dir.glob("*/layer.xml"): + for path in layer_path.parent.glob("ads/*/module.xml"): + root_module = parse_xml_partially( + path, line_contains=["Module ", " Id", " Name"] + ) + module_id = root_module.module["id"] + if module_id not in module_id_by_name: + module_id_by_name[module_id] = root_module.module["name"] + + return module_id_by_name + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk" + owner_by_modules = get_module_id_by_name(project_dir) + + for module_id, module_name in owner_by_modules.items(): + print(f"{module_id} = {module_name}") diff --git a/job_compassplus/tx_parse_xml/save_maintainers.py b/job_compassplus/tx_parse_xml/save_maintainers.py new file mode 100644 index 000000000..831180160 --- /dev/null +++ b/job_compassplus/tx_parse_xml/save_maintainers.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from show_maintainers import get_owner_by_modules + + +project_dir = "C:/DEV__TX/trunk_tx" + +with open("maintainers.txt", "w", encoding="utf-8") as f: + owner_by_modules = get_owner_by_modules(project_dir) + + lines = [] + for email in sorted(owner_by_modules): + modules = owner_by_modules[email] + modules.sort() + + lines.append(f"{email} ({len(modules)}):") + for module in modules: + lines.append(f" {module}") + lines.append("") + + text = "\n".join(lines).strip() + f.write(text) diff --git a/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py b/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py new file mode 100644 index 000000000..710166a3e --- /dev/null +++ b/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import xml.etree.ElementTree as ET + +from pathlib import Path + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", +) + + +def get_text(el: ET.Element) -> str: + if el is None: + return "" + return "".join(text for text in el.itertext()) + + +def process(path: str, regexp: str) -> None: + pattern = re.compile(regexp) + + for p in Path(path).glob("*/ads/*/locale/*/mlb*.xml"): + try: + mlb = ET.fromstring(p.read_bytes()) + except Exception: + # Ignore + continue + + items = [] + for string_el in mlb.findall(".//ads:String", namespaces=NS): + string_value_el = string_el.find("./ads:Value", namespaces=NS) + string_value = get_text(string_value_el) + + if pattern.search(string_value): + string_id = string_el.attrib["Id"] + string_lang = string_value_el.attrib["Language"] + + items.append(f"Found with id={string_id} language={string_lang}: {string_value!r}") + + if items: + items.sort() + print(*items, sep="\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Search in locale" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + parser.add_argument( + "regexp", + help="Regular expression", + ) + args = parser.parse_args() + + process(args.path_trunk, args.regexp) diff --git a/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py b/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py new file mode 100644 index 000000000..f57c651b4 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class ADS: + title: str + props: dict[str, list[str]] + editors: dict[str, list[str]] + selectors: dict[str, list[str]] + filters: dict[str, list[str]] + sortings: dict[str, list[str]] + + def __bool__(self) -> bool: + return bool( + self.props + or self.editors + or self.selectors + or self.filters + or self.sortings + ) + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def has_text(el: ET.Element) -> bool: + return any(text.strip() for text in el.itertext()) + + +def get_xml_object_title(el: ET.Element) -> str: + el_id = el.attrib["Id"] + el_name = el.attrib["Name"] + return f"{el_name}({el_id})" + + +def get_databases_specific_sqml_expression(el: ET.Element, tag_name: str) -> list[str]: + items = [] + if not el: + return items + + specific_sqml_expression_el = el.findall(f"./ads:{tag_name}", namespaces=NS) + for sqml_el in specific_sqml_expression_el: + if sqml_el and has_text(sqml_el.find("./xsc:Content", namespaces=NS)): + database = sqml_el.find("./xsc:Database", namespaces=NS).text + items.append(database) + + return items + + +def get_exists_condition_names(condition_el: ET.Element) -> list[str]: + items = [] + if not condition_el: + return items + + condition_where_el = condition_el.find("./ads:ConditionWhere", namespaces=NS) + if condition_where_el and has_text(condition_where_el): + items.append("ConditionWhere") + + condition_from_el = condition_el.find("./ads:ConditionFrom", namespaces=NS) + if condition_from_el and has_text(condition_from_el): + items.append("ConditionFrom") + + for tag_name in ["SpecificConditionWhere", "SpecificConditionFrom"]: + for database in get_databases_specific_sqml_expression(condition_el, tag_name): + items.append(f"{tag_name}={database}") + + return items + + +def get_ads_filters(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for filter_el in ads_el.findall( + "./ads:Presentations/ads:Filters/ads:Filter", namespaces=NS + ): + filter_title = get_xml_object_title(filter_el) + + condition_el = filter_el.find("./ads:Condition", namespaces=NS) + if condition_el and has_text(condition_el): + obj_by_items[filter_title].append("Condition") + + condition_from_el = filter_el.find("./ads:ConditionFrom", namespaces=NS) + if condition_from_el and has_text(condition_from_el): + obj_by_items[filter_title].append("ConditionFrom") + + hint_el = filter_el.find("./ads:Hint", namespaces=NS) + if hint_el and has_text(hint_el): + obj_by_items[filter_title].append("Hint") + + for tag_name in ["SpecificCondition", "SpecificConditionFrom", "SpecificHint"]: + for database in get_databases_specific_sqml_expression(filter_el, tag_name): + obj_by_items[filter_title].append(f"{tag_name}={database}") + + enabled_sorting_el = filter_el.find("./ads:EnabledSorting", namespaces=NS) + if enabled_sorting_el: + enabled_sorting_hint_el = enabled_sorting_el.find( + "./ads:Hint", namespaces=NS + ) + if enabled_sorting_hint_el and has_text(enabled_sorting_hint_el): + obj_by_items[filter_title].append("EnabledSorting/Hint") + + for database in get_databases_specific_sqml_expression( + enabled_sorting_el, "SpecificHint" + ): + obj_by_items[filter_title].append( + f"EnabledSorting/SpecificHint={database}" + ) + + return obj_by_items + + +def get_ads_props(ads_el: ET.Element) -> dict[str, list[str]]: + prop_by_items = defaultdict(list) + + for prop_el in ads_el.findall("./ads:Properties/ads:Property", namespaces=NS): + prop_title = get_xml_object_title(prop_el) + + sqml_expression_el = prop_el.find("./ads:SqmlExpression", namespaces=NS) + if sqml_expression_el and has_text(sqml_expression_el): + prop_by_items[prop_title].append("SqmlExpression") + + for database in get_databases_specific_sqml_expression( + prop_el, "SpecificSqmlExpression" + ): + prop_by_items[prop_title].append(f"SpecificSqmlExpression={database}") + + parent_select_condition_el = prop_el.find( + "./ads:Presentation/ads:ParentSelect/ads:ParentSelectCondition", + namespaces=NS, + ) + for condition_name in get_exists_condition_names(parent_select_condition_el): + prop_by_items[prop_title].append(f"ParentSelect/{condition_name}") + + return prop_by_items + + +def get_ads_editor_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:EditorPresentations/ads:EditorPresentation", + namespaces=NS, + ): + editor_title = get_xml_object_title(presentation_el) + + for child_item_ref_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:ChildRef", namespaces=NS + ): + condition_el = child_item_ref_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_names(condition_el) + if child_sqml_items: + child_ref_title = get_xml_object_title(child_item_ref_el) + obj_by_items[editor_title].append( + f'{child_ref_title}: {", ".join(child_sqml_items)}' + ) + + for child_item_entity_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:Entity", namespaces=NS + ): + condition_el = child_item_entity_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_names(condition_el) + if child_sqml_items: + child_entity_title = get_xml_object_title(child_item_entity_el) + obj_by_items[editor_title].append( + f'{child_entity_title}: {", ".join(child_sqml_items)}' + ) + + return obj_by_items + + +def get_ads_selector_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:SelectorPresentations/ads:SelectorPresentation", + namespaces=NS, + ): + selector_title = get_xml_object_title(presentation_el) + + condition_el = presentation_el.find("./ads:Condition", namespaces=NS) + for condition_name in get_exists_condition_names(condition_el): + obj_by_items[selector_title].append(condition_name) + + addons_el = presentation_el.find("./ads:Addons", namespaces=NS) + if addons_el: + default_hint_el = addons_el.find("./ads:DefaultHint", namespaces=NS) + if default_hint_el and has_text(default_hint_el): + obj_by_items[selector_title].append("Addons/DefaultHint") + + specific_hint_el = addons_el.find("./ads:SpecificHint", namespaces=NS) + for database in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificDefaultHint" + ): + obj_by_items[selector_title].append( + f"Addons/SpecificDefaultHint={database}" + ) + + return obj_by_items + + +def get_ads_sortings(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + sorting_el = ads_el.find( + "./ads:Presentations/ads:Sortings/ads:Sorting", namespaces=NS + ) + if sorting_el: + sorting_title = get_xml_object_title(sorting_el) + + hint_el = sorting_el.find("./ads:Hint", namespaces=NS) + if hint_el and has_text(hint_el): + obj_by_items[sorting_title].append("Hint") + + specific_hint_el = sorting_el.find("./ads:SpecificHint", namespaces=NS) + for database in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificHint" + ): + obj_by_items[sorting_title].append(f"SpecificHint={database}") + + return obj_by_items + + +def get_ads(model_path: Path) -> ADS | None: + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + ads_el = model.find("./ads:AdsClassDefinition", namespaces=NS) + if not ads_el: + return + + return ADS( + title=get_xml_object_title(ads_el), + props=get_ads_props(ads_el), + editors=get_ads_editor_presentations(ads_el), + selectors=get_ads_selector_presentations(ads_el), + filters=get_ads_filters(ads_el), + sortings=get_ads_sortings(ads_el), + ) + + +def get_ads_list(branch_dir: Path | str) -> dict[str, list[ADS]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + if not branch_dir.exists(): + raise Exception(f"Not exists: {branch_dir}") + + layer_module_by_ads_list = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir = layer_dir / "ads" + if layer_ads_dir.is_dir(): + layer = layer_dir.name + for module_path in layer_ads_dir.glob("*/src"): + module = module_path.parent.name + layer_module = f"{layer}/{module}" + + for class_path in module_path.glob("*.xml"): + if ads := get_ads(class_path): + layer_module_by_ads_list[layer_module].append(ads) + + return layer_module_by_ads_list + + +def process(path: str) -> None: + indent1 = " " + indent2 = indent1 * 2 + indent3 = indent1 * 3 + indent4 = indent1 * 4 + for layer_module, ads_list in get_ads_list(path).items(): + print(layer_module) + + for ads in ads_list: + print(indent1 + ads.title) + + prop_by_sqmls = ads.props + if prop_by_sqmls: + print(indent2 + "Props:") + for prop, items in prop_by_sqmls.items(): + print(indent3 + f'{prop}: {", ".join(items)}') + + editor_by_sqmls = ads.editors + if editor_by_sqmls: + print(indent2 + "Editor presentations:") + for editor, items in editor_by_sqmls.items(): + print(indent3 + f"{editor}:") + for sqml in items: + print(indent4 + f"{sqml}") + + selector_by_sqmls = ads.selectors + if selector_by_sqmls: + print(indent2 + "Selector presentations:") + for selector, items in selector_by_sqmls.items(): + print(indent3 + f'{selector}: {", ".join(items)}') + + filter_by_sqmls = ads.filters + if filter_by_sqmls: + print(indent2 + "Filters:") + for filter_name, items in filter_by_sqmls.items(): + print(indent3 + f'{filter_name}: {", ".join(items)}') + + sorting_by_sqmls = ads.sortings + if sorting_by_sqmls: + print(indent2 + "Sortings:") + for sorting, items in sorting_by_sqmls.items(): + print(indent3 + f'{sorting}: {", ".join(items)}') + + print() + + print("\n" + "-" * 100 + "\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Shows ADS entities with SQL definitions" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py b/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py new file mode 100644 index 000000000..11713a16f --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET +from pathlib import Path + + +def get_module_ids_with_maintainter(path_branch: str | Path) -> list[str]: + ns = dict( + p="http://schemas.radixware.org/product.xsd", + ) + + ids: list[str] = [] + + branch = ET.fromstring(path_branch.read_bytes()) + for module_info_el in branch.findall(".//p:ModuleInfo", namespaces=ns): + module_id = module_info_el.find("./p:ModuleId", namespaces=ns).text + has_maintainters = module_info_el.find(".//p:OwnerEmail", namespaces=ns) is not None + if has_maintainters and module_id not in ids: + ids.append(module_id) + + return ids + + +def process(path_layer: str): + if isinstance(path_layer, str): + path_layer = Path(path_layer) + + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + path_branch = path_layer.parent / "branch.xml" + if not path_branch.exists(): + raise Exception(f"Not exists: {path_branch}") + + module_ids: list[str] = get_module_ids_with_maintainter(path_branch) + + deprecated_module_without_maintainters: list[tuple[str, str]] = [] + module_without_maintainters: list[tuple[str, str]] = [] + + for path_module in path_layer.glob("ads/*/module.xml"): + module_name = path_module.parent.name + + module_el = ET.fromstring(path_module.read_bytes()) + module_id = module_el.get("Id") + if module_id in module_ids: + continue + + is_deprecated = module_el.get("IsDeprecated") == "true" + if is_deprecated: + deprecated_module_without_maintainters.append((module_name, module_id)) + else: + module_without_maintainters.append((module_name, module_id)) + + print(f"Modules ({len(module_without_maintainters)}):") + for module_name, module_id in module_without_maintainters: + print(f" {module_name} ({module_id})") + + print() + + print(f"Deprecated modules ({len(deprecated_module_without_maintainters)}):") + for module_name, module_id in deprecated_module_without_maintainters: + print(f" {module_name} ({module_id})") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Show ads modules without maintainer" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path_layer) diff --git a/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py b/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py new file mode 100644 index 000000000..3789a9c9d --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Column: + id: str + name: str + default_value: str + + +def get_table_by_columns(model_path: Path) -> dict[str, list[Column]]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + com="http://schemas.radixware.org/commondef.xsd", + ) + + table_by_columns = defaultdict(list) + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + table_title = f"{table.attrib['Name']}({table.attrib['Id']})" + + for column in table.findall("./dds:Columns/dds:Column", namespaces=ns): + default_value = column.find( + "./dds:DefaultVal[@Type='Expression']/com:Value", namespaces=ns + ) + if default_value is not None: + table_by_columns[table_title].append( + Column( + id=column.attrib["Id"], + name=column.attrib["Name"], + default_value=default_value.text.strip(), + ) + ) + + return table_by_columns + + +def process(branch_dir: Path | str) -> dict[str, dict[str, list[Column]]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_tables = defaultdict(dict) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for table, columns in get_table_by_columns(model_xml).items(): + key = f"{layer}/{module}" + layer_module_by_tables[key][table] = columns + + return layer_module_by_tables + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + for key, table_by_columns in process(path).items(): + print(f"{key} ({len(table_by_columns)})") + for i, (table, columns) in enumerate(table_by_columns.items(), 1): + print(f" {i}. {table}:") + for column in columns: + print(f" {column.name}({column.id}) = {column.default_value!r}") + print() + """ + com.optt/ProtocolSetup (7) + 1. Protocol(tblJBLJOL4TFBAHLBEWZMLSHXYGMM): + extGuid(colW44QAAJJP5ELXOLWL5KKHTLUKI) = 'sys_guid()' + 2. ProtocolField(tblE26CRSMVJZCJVF4ZMR56KUYTAM): + extGuid(colRO63O6OLMRBSNJRIBSSV3HRVOQ) = 'sys_guid()' + 3. ProtocolStateAttribute(tblFP3C5RQFDFFWJAJQMYVXSQ3SEY): + extGuid(col3MK2X5H3ZZBVVJB3AYDMMDCWHQ) = 'sys_guid()' + 4. MessageType(tbl2DX3TB7ZUZE4RG3B7BY6HS4EZI): + extGuid(col73V3MXLEKFD4BASZ2JXAEB4X4A) = 'sys_guid()' + 5. Rule(tblE4HU6SKERVDMHJBSB4D6SCPUVQ): + extGuid(colAN5WEW42GBATLDBNPREXMUY5PU) = 'sys_guid()' + 6. FieldValueGenerator(tblGU3P5FK7PZD4VLOS4XR5236L6Q): + extGuid(colBDVB7VL5ARAVRLSYA2J5YEMBFU) = 'sys_guid()' + 7. ProtocolVersion(tblQLF47J7P4JCY5E6JFJ62NJBTRQ): + extGuid(colJLFFQTXRX5FFJG6AG3AL4MFNDM) = 'sys_guid()' + """ diff --git a/job_compassplus/tx_parse_xml/show_db_custom_triggers.py b/job_compassplus/tx_parse_xml/show_db_custom_triggers.py new file mode 100644 index 000000000..c0e63725e --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_custom_triggers.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Trigger: + table_name: str + name: str + db_name: str + + +def get_triggers(model_path: Path) -> list[Trigger]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + for trigger in table.findall("./dds:Triggers/dds:Trigger", namespaces=ns): + if trigger.attrib.get( + "Type" + ): # При True - триггер был создан автоматически радиксом + continue + + items.append( + Trigger( + table_name=table.attrib["Name"], + name=trigger.attrib["Name"], + db_name=trigger.attrib["DbName"], + ) + ) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[Trigger]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_triggers = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for trigger in get_triggers(model_xml): + key = f"{layer}/{module}" + layer_module_by_triggers[key].append(trigger) + + return layer_module_by_triggers + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + layer_module_by_triggers = process(path) + for key, triggers in layer_module_by_triggers.items(): + print(f"{key} ({len(triggers)})") + for i, trigger in enumerate(triggers, 1): + print(f" {i}. {trigger.table_name}. {trigger.name} ({trigger.db_name})") + print() diff --git a/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py b/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py new file mode 100644 index 000000000..d2fd6eb44 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from pathlib import Path + + +# NOTE: ExcludeTargetDatabases еще присутствует во множестве объектов таблицы: индексы, триггеры, столбцы и т.п. +# Но текущая реализация выводит только таблицы + + +def get_tables(model_path: Path) -> list[str]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + if table.attrib["ExcludeTargetDatabases"]: + title = f"{table.attrib['Name']}({table.attrib['Id']})" + items.append(title) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[str]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_tables = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for table in get_tables(model_xml): + key = f"{layer}/{module}" + layer_module_by_tables[key].append(table) + + return layer_module_by_tables + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + for key, tables in process(path).items(): + print(f"{key} ({len(tables)})") + for i, table in enumerate(tables, 1): + print(f" {i}. {table}") + print() diff --git a/job_compassplus/tx_parse_xml/show_db_views.py b/job_compassplus/tx_parse_xml/show_db_views.py new file mode 100644 index 000000000..46755d2cb --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_views.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class View: + id: str + name: str + db_name: str + + +def get_views(model_path: Path) -> list[View]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for view in model.findall(".//dds:Views/dds:View", namespaces=ns): + items.append( + View( + id=view.attrib["Id"], + name=view.attrib["Name"], + db_name=view.attrib["DbName"], + ) + ) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[View]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_triggers = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for view in get_views(model_xml): + key = f"{layer}/{module}" + layer_module_by_triggers[key].append(view) + + return layer_module_by_triggers + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + layer_module_by_triggers = process(path) + for key, views in layer_module_by_triggers.items(): + print(f"{key} ({len(views)})") + for i, view in enumerate(views, 1): + print(f" {i}. {view.name} ({view.db_name}, {view.id})") + print() diff --git a/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py b/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py new file mode 100644 index 000000000..4cd63df91 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class ADS: + title: str + props: dict[str, list[str]] + editors: dict[str, list[str]] + selectors: dict[str, list[str]] + filters: dict[str, list[str]] + sortings: dict[str, list[str]] + + def __bool__(self) -> bool: + return bool( + self.props + or self.editors + or self.selectors + or self.filters + or self.sortings + ) + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def get_text(el: ET.Element) -> str: + return "".join(text.strip() for text in el.itertext()) + + +def get_xml_object_title(el: ET.Element) -> str: + el_id = el.attrib["Id"] + el_name = el.attrib["Name"] + return f"{el_name}({el_id})" + + +def get_databases_specific_sqml_expression( + el: ET.Element, tag_name: str +) -> list[tuple[str, str]]: + items = [] + if not el: + return items + + specific_sqml_expression_el = el.findall(f"./ads:{tag_name}", namespaces=NS) + for sqml_el in specific_sqml_expression_el: + if sqml_el: + content_el = sqml_el.find("./xsc:Content", namespaces=NS) + sqml_text = get_sqml_text(content_el) + if sqml_text: + database = sqml_el.find("./xsc:Database", namespaces=NS).text + items.append((database, sqml_text)) + + return items + + +def process_sqml_text(text: str) -> str: + return re.sub(r"\s", "", text).lower() + + +def get_sqml_text(el: ET.Element) -> str: + if not el: + return "" + + lines = [] + for item in el.iterfind("./xsc:Item", namespaces=NS): + for child in item: + if child.tag.endswith("Sql"): + if inner_text := get_text(child): + # Пропуск комментариев + if not inner_text.startswith("--"): + lines.append(process_sqml_text(inner_text)) + else: + # Например: [('TableId', 'tbl42K4K2TTGLNRDHRZABQAQH3XQ4'), ('PropId', 'colXJ7GB2ILWZGAVHRCJAZCMCM6E4'), ('Owner', 'CHILD')] + if child.items(): + if owner_type := child.get("Owner"): + attr_str = ( + owner_type + + "." + + ".".join(v for k, v in child.items() if k != "Owner") + ) + else: + attr_str = ",".join(f"{k}={v}" for k, v in child.items()) + lines.append(f"[{attr_str}]") + + return "".join(lines) + + +def get_exists_condition_sqml_text(condition_el: ET.Element) -> list[str]: + items = [] + if not condition_el: + return items + + condition_where_el = condition_el.find("./ads:ConditionWhere", namespaces=NS) + if sqml_text := get_sqml_text(condition_where_el): + items.append(sqml_text) + + condition_from_el = condition_el.find("./ads:ConditionFrom", namespaces=NS) + if sqml_text := get_sqml_text(condition_from_el): + items.append(sqml_text) + + for tag_name in ["SpecificConditionWhere", "SpecificConditionFrom"]: + for database, sqml_text in get_databases_specific_sqml_expression( + condition_el, tag_name + ): + items.append(f"{database}:{sqml_text}") + + return items + + +def get_ads_filters(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for filter_el in ads_el.findall( + "./ads:Presentations/ads:Filters/ads:Filter", namespaces=NS + ): + filter_title = get_xml_object_title(filter_el) + + condition_el = filter_el.find("./ads:Condition", namespaces=NS) + if sqml_text := get_sqml_text(condition_el): + obj_by_items[filter_title].append(sqml_text) + + condition_from_el = filter_el.find("./ads:ConditionFrom", namespaces=NS) + if sqml_text := get_sqml_text(condition_from_el): + obj_by_items[filter_title].append(sqml_text) + + hint_el = filter_el.find("./ads:Hint", namespaces=NS) + if sqml_text := get_sqml_text(hint_el): + obj_by_items[filter_title].append(sqml_text) + + for tag_name in ["SpecificCondition", "SpecificConditionFrom", "SpecificHint"]: + for database, sqml_text in get_databases_specific_sqml_expression( + filter_el, tag_name + ): + obj_by_items[filter_title].append(f"{database}:{sqml_text}") + + enabled_sorting_el = filter_el.find("./ads:EnabledSorting", namespaces=NS) + if enabled_sorting_el: + enabled_sorting_hint_el = enabled_sorting_el.find( + "./ads:Hint", namespaces=NS + ) + if sqml_text := get_sqml_text(enabled_sorting_hint_el): + obj_by_items[filter_title].append(sqml_text) + + for database, sqml_text in get_databases_specific_sqml_expression( + enabled_sorting_el, "SpecificHint" + ): + obj_by_items[filter_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads_props(ads_el: ET.Element) -> dict[str, list[str]]: + prop_by_items = defaultdict(list) + + for prop_el in ads_el.findall("./ads:Properties/ads:Property", namespaces=NS): + prop_title = get_xml_object_title(ads_el) + "/" + get_xml_object_title(prop_el) + + sqml_expression_el = prop_el.find("./ads:SqmlExpression", namespaces=NS) + if sqml_text := get_sqml_text(sqml_expression_el): + prop_by_items[prop_title].append(sqml_text) + + for database, sqml_text in get_databases_specific_sqml_expression( + prop_el, "SpecificSqmlExpression" + ): + prop_by_items[prop_title].append(f"{database}:{sqml_text}") + + parent_select_condition_el = prop_el.find( + "./ads:Presentation/ads:ParentSelect/ads:ParentSelectCondition", + namespaces=NS, + ) + for sqml_text in get_exists_condition_sqml_text(parent_select_condition_el): + prop_by_items[prop_title].append(sqml_text) + + return prop_by_items + + +def get_ads_editor_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:EditorPresentations/ads:EditorPresentation", + namespaces=NS, + ): + editor_title = get_xml_object_title(presentation_el) + + for child_item_ref_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:ChildRef", namespaces=NS + ): + condition_el = child_item_ref_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_sqml_text(condition_el) + if child_sqml_items: + child_ref_title = get_xml_object_title(child_item_ref_el) + + obj_by_items[editor_title].append( + f'{editor_title}/{child_ref_title}: {", ".join(child_sqml_items)}' + ) + + for child_item_entity_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:Entity", namespaces=NS + ): + condition_el = child_item_entity_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_sqml_text(condition_el) + if child_sqml_items: + child_entity_title = get_xml_object_title(child_item_entity_el) + obj_by_items[editor_title].append( + f'{editor_title}/{child_entity_title}: {", ".join(child_sqml_items)}' + ) + + return obj_by_items + + +def get_ads_selector_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:SelectorPresentations/ads:SelectorPresentation", + namespaces=NS, + ): + selector_title = get_xml_object_title(presentation_el) + + condition_el = presentation_el.find("./ads:Condition", namespaces=NS) + for sqml_text in get_exists_condition_sqml_text(condition_el): + obj_by_items[selector_title].append(sqml_text) + + addons_el = presentation_el.find("./ads:Addons", namespaces=NS) + if addons_el: + default_hint_el = addons_el.find("./ads:DefaultHint", namespaces=NS) + if sqml_text := get_sqml_text(default_hint_el): + obj_by_items[selector_title].append(sqml_text) + + specific_hint_el = addons_el.find("./ads:SpecificHint", namespaces=NS) + for database, sqml_text in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificDefaultHint" + ): + obj_by_items[selector_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads_sortings(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + sorting_el = ads_el.find( + "./ads:Presentations/ads:Sortings/ads:Sorting", namespaces=NS + ) + if sorting_el: + sorting_title = get_xml_object_title(sorting_el) + + hint_el = sorting_el.find("./ads:Hint", namespaces=NS) + if sqml_text := get_sqml_text(hint_el): + obj_by_items[sorting_title].append(sqml_text) + + specific_hint_el = sorting_el.find("./ads:SpecificHint", namespaces=NS) + for database, sqml_text in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificHint" + ): + obj_by_items[sorting_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads(model_path: Path) -> ADS | None: + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + ads_el = model.find("./ads:AdsClassDefinition", namespaces=NS) + if not ads_el: + return + + return ADS( + title=get_xml_object_title(ads_el), + props=get_ads_props(ads_el), + editors=get_ads_editor_presentations(ads_el), + selectors=get_ads_selector_presentations(ads_el), + filters=get_ads_filters(ads_el), + sortings=get_ads_sortings(ads_el), + ) + + +def get_ads_list(branch_dir: Path | str) -> dict[str, list[ADS]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + if not branch_dir.exists(): + raise Exception(f"Not exists: {branch_dir}") + + layer_module_by_ads_list = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir = layer_dir / "ads" + if layer_ads_dir.is_dir(): + layer = layer_dir.name + for module_path in layer_ads_dir.glob("*/src"): + module = module_path.parent.name + layer_module = f"{layer}/{module}" + + for class_path in module_path.glob("*.xml"): + if ads := get_ads(class_path): + layer_module_by_ads_list[layer_module].append(ads) + + return layer_module_by_ads_list + + +def process(path: str) -> None: + indent1 = " " + indent2 = indent1 * 2 + + sqml_text_by_ids: dict[str, list[str]] = defaultdict(list) + + for layer_module, ads_list in get_ads_list(path).items(): + for ads in ads_list: + prop_by_sqmls = ads.props + if prop_by_sqmls: + for prop, items in prop_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(prop) + + editor_by_sqmls = ads.editors + if editor_by_sqmls: + for editor, items in editor_by_sqmls.items(): + for sqml in items: + id_str, sqml_text = sqml.split(": ") + sqml_text_by_ids[sqml_text].append(id_str) + + selector_by_sqmls = ads.selectors + if selector_by_sqmls: + for selector, items in selector_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(selector) + + filter_by_sqmls = ads.filters + if filter_by_sqmls: + for filter_name, items in filter_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(filter_name) + + sorting_by_sqmls = ads.sortings + if sorting_by_sqmls: + for sorting, items in sorting_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(sorting) + + for sqml_text, ids in sorted( + sqml_text_by_ids.items(), key=lambda x: len(x[1]), reverse=True + ): + # Если меньше 5, то пропускаем + if len(ids) < 5: + continue + + print(f"Sql: {sqml_text}") + print(f"{indent1}{len(ids)}:") + for id_str in ids: + print(f"{indent2}{id_str}") + print() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Shows ADS entities with duplicates SQL definitions" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py b/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py new file mode 100644 index 000000000..8599ced08 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + +from bs4 import BeautifulSoup + + +def get_duplicates(project_dir: str | Path) -> dict[str, int]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + file_name_branch = project_dir / "branch.xml" + + with open(file_name_branch, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") + + items = [] + for module in root.select("moduleinfo"): + layer_url = module.layerurl.text + module_id = module.moduleid.text + name = f"{layer_url}-{module_id}" + + definition_path = None + if module.definition: + definition_path = module.definition.get("path") + + if definition_path and definition_path != module_id: + name += f': {module.definition["path"]}' + + items.append(name) + + return {name: number for name, number in Counter(items).items() if number > 1} + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk_tx" + duplicates = get_duplicates(project_dir) + + for name, number in sorted(duplicates.items(), reverse=True): + print(f"{name} = {number}") diff --git a/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py b/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py new file mode 100644 index 000000000..53ee5c9bc --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from bs4 import BeautifulSoup, Tag + + +file_name = r"C:\DEV__TX\trunk_tx\com.tranzaxis\uds\Interfacing.Examples\etc\DinersClub\DinersClub.xml" + + +root = BeautifulSoup(open(file_name), "xml") + + +def get_func_title(func_el: Tag) -> str: + description = func_el.Description.text if func_el.Description else "" + attrs_title = ", ".join(f"{k}={v}" for k, v in func_el.attrs.items()) + return f"{attrs_title}. Description: {description}" + + +def get_funcs_from_user_funcs(node_el: Tag) -> list[Tag]: + try: + return [ + func_el + for func_el in node_el.find("UserFuncs", recursive=False).find_all( + "Func", recursive=False + ) + ] + except: + return [] + + +def get_funcs_from_user_props(node_el: Tag) -> list[Tag]: + try: + return [ + func_el + for func_el in node_el.find("UserProps", recursive=False).find_all("Func") + ] + except: + return [] + + +node_by_funcs = defaultdict(list) + +nodes = [] +if pipeline_nodes := root.find("PipelineNodes"): + nodes += pipeline_nodes.find_all("Node") +if other_nodes := root.find("OtherNodes"): + nodes += other_nodes.find_all("Node") + +for node_el in nodes: + node_title = node_el.Title.text if node_el.Title else "" + node_class_id = node_el["ClassId"] + + parent_title = f"{node_el.name} (Title: {node_title}. ClassId: {node_el['ClassId']}, EntityPid: {node_el['EntityPid']})" + + # Старый стиль хранения в UserFuncs/Func + for func_el in get_funcs_from_user_funcs(node_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + # Новый стиль хранения в UserProps/Props/Func + for func_el in get_funcs_from_user_props(node_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + try: + for stage_el in node_el.TransformStages.find_all( + "TransformStage", recursive=False + ): + parent_title = f"{node_el.name}/Stage#{stage_el.Seq.text} (ClassId: {stage_el['ClassId']}, NodeTitle: {node_title}, NodeClassId: {node_el['ClassId']}, NodeEntityPid: {node_el['EntityPid']})" + + for func_el in get_funcs_from_user_funcs(stage_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + for func_el in get_funcs_from_user_props(stage_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + except: + pass + +i = 1 +for node_title, funcs in node_by_funcs.items(): + print(f"{node_title} ({len(funcs)}):") + for func_title in funcs: + print(f" {i}. {func_title}") + i += 1 + + print() + +""" +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Stop List Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 319) (2): + 1. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=110, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 2. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=110, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: IsStopList. ClassId: aclP3F7MNUSNVHJRAMR2454ZA4GCQ, EntityPid: 318) (1): + 3. ClassGUID=aclYRODMAM7AZB55JOWOVGSJ2TL4I, ProfileVersion=0, OwnerPid=318, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclP3F7MNUSNVHJRAMR2454ZA4GCQ, OwnerPropId=pru5WVR5OZMIRC4VC6QHUQZ66EBWM, Profile=boolean check(Radix::ServiceBus.Nodes::Router.Jml router, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Is stop list request + +Node/Stage#1 (ClassId: acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, NodeTitle: System Event, NodeClassId: acl4TJBHQN7PJBZFLBHDI3WHSWDSY, NodeEntityPid: 329) (2): + 4. ClassGUID=acl2G2QDD5EXRBCROWISX5QR5CUCA, ProfileVersion=0, OwnerPid=113, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, OwnerPropId=pruPSTIRHU6HRH2TJTGTE6XD6NZTI, Profile=Xml transform(Transformer transformer, ServiceBus::SbXsd:PipelineMessageRq rq, Xml inRq) throws PipelineException. Description: Request + 5. ClassGUID=aclIB36HWWSIZFGHBNZC4SXAYP2J4, ProfileVersion=0, OwnerPid=113, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, OwnerPropId=pruVGX63H52ZRFMNAAH5JHB6U5LGM, Profile=Xml transform(Transformer transformer, ServiceBus::SbXsd:PipelineMessageRs rs, Xml inRs, Xml inRq, Xml outRq) throws PipelineException. Description: Response + +Node (Title: IsEcho. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 331) (1): + 6. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=331, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is echo request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Reverse Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 330) (2): + 7. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=114, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 8. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=114, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: IsInvalid. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 324) (1): + 9. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=324, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is invalid request + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Auth-Rev Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 322) (2): + 10. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=111, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 11. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=111, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Auth Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 333) (2): + 12. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=116, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 13. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=116, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: Echo Request. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 321) (1): + 14. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=321, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Echo request + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Admin Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 332) (2): + 15. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=115, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 16. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=115, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node (Title: isSystem. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 317) (1): + 17. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=317, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is system request + +Node (Title: TryCatch. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 328) (1): + 18. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=328, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Catch exception + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Stop List Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 316) (2): + 19. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=109, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 20. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=109, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node (Title: Invalid Request. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 315) (1): + 21. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=315, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Invalid request + +Node (Title: IsAdmin. ClassId: aclP3F7MNUSNVHJRAMR2454ZA4GCQ, EntityPid: 326) (1): + 22. ClassGUID=aclYRODMAM7AZB55JOWOVGSJ2TL4I, ProfileVersion=0, OwnerPid=326, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclP3F7MNUSNVHJRAMR2454ZA4GCQ, OwnerPropId=pru5WVR5OZMIRC4VC6QHUQZ66EBWM, Profile=boolean check(Radix::ServiceBus.Nodes::Router.Jml router, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Is admin request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Admin Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 325) (2): + 23. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=112, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 24. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=112, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: Endpoint.NetHub. ClassId: aclQCQNNJMW4FCNZCRN2D5GNWKOKA, EntityPid: 6341) (5): + 25. ClassGUID=aclDUAVIDYVPZD4ZDUJBHXWJV7VTE, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pru6HZ4WT3ULJBYTCTHWO5NFYXI5U, Profile=Str extractUniqueKey(byte[] data) throws PipelineException. Description: + 26. ClassGUID=aclQPC7P4UWZ5BQ5OIJAMWF3SY5X4, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruK7UNBG2GCZDFVAEGA3NFR2U72Q, Profile=int insertStan(byte[] data, int prevStan) throws PipelineException. Description: + 27. ClassGUID=aclKZFUEXWYNJCFPPKI6T2B5ERAZU, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruLXJ7LZAFXRD5ZDHXXBU24324SE, Profile=byte[] prepareEchoTestRq() throws PipelineException. Description: + 28. ClassGUID=aclBKU3COGRUVGLZABNOJSKU5PUM4, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruNHYZI7WV4ZGDPDCXONZQWDWP2Y, Profile=boolean isRequest(byte[] data, Common::Map parsingVars) throws NetHubFormatError, PipelineException. Description: + 29. ClassGUID=aclNJQPXOGPZVAJPCCRI26IFRTZHY, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruQ4URE5BVGVAB7HIHPXE2JN5LBY, Profile=boolean isCorrelated(byte[] rq, Common::Map rqParsingVars, byte[] rs, Common::Map rsParsingVars) throws NetHubFormatError, PipelineException. Description: + +Node (Title: Incoming.Rtp. ClassId: aclSJWDDYSTAJDNFGAKPB5ENWPCVQ, EntityPid: 16983) (1): + 30. ClassGUID=aclJUSHZQABQZFDVC3BHF2R4SDRNY, ProfileVersion=0, OwnerPid=16983, OwnerEntityId=tblKG37PKEBQPNRDB46AALOMT5GDM, OwnerClassId=aclSJWDDYSTAJDNFGAKPB5ENWPCVQ, OwnerPropId=pruUHNT6SGVYFGQNLYXBL6PRVG4RQ, OwnerExtGuid=7B74FE964CFA4446A91C6857A5C71145, Profile=Xml mask(Xml mess) throws PipelineException. Description: + +Node (Title: Outgoing.Rtp. ClassId: aclDL54XGQDBZCETH2Y35V52MIKXA, EntityPid: 16984) (1): + 31. ClassGUID=aclJUSHZQABQZFDVC3BHF2R4SDRNY, ProfileVersion=0, OwnerPid=16984, OwnerEntityId=tblKG37PKEBQPNRDB46AALOMT5GDM, OwnerClassId=aclDL54XGQDBZCETH2Y35V52MIKXA, OwnerPropId=pru4437EDZTLFCQROXATBM4NHG3WQ, OwnerExtGuid=8F4A6616021B43D49CD7714A39A26961, Profile=Xml mask(Xml mess) throws PipelineException. Description: +""" diff --git a/job_compassplus/tx_parse_xml/show_maintainers.py b/job_compassplus/tx_parse_xml/show_maintainers.py new file mode 100644 index 000000000..e45d69121 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_maintainers.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from pathlib import Path + +from bs4 import BeautifulSoup + + +def parse_xml_partially(path: str | Path, line_contains: list) -> BeautifulSoup: + # Считаем только ту часть XML, где имеются необходимые атрибуты + xml_part = [] + for line in open(path, encoding="utf-8"): + xml_part.append(line) + if all(x in line for x in line_contains): + break + + return BeautifulSoup("\n".join(xml_part), "html.parser") + + +def get_owner_by_modules(project_dir: str | Path) -> dict[str, list[str]]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + layer_url_by_name: dict[str, str] = dict() + layer_module_id_by_name: dict[tuple[str, str], str] = dict() + + for layer_path in project_dir.glob("*/layer.xml"): + root_layer = parse_xml_partially( + layer_path, line_contains=["Layer ", " Uri", " Name"] + ) + layer_url = root_layer.layer["uri"] + name = root_layer.layer["name"] + layer_url_by_name[layer_url] = name + + for path in layer_path.parent.glob("ads/*/module.xml"): + root_module = parse_xml_partially( + path, line_contains=["Module ", " Id", " Name"] + ) + module_id = root_module.module["id"] + + layer_module_id_by_name[layer_url, module_id] = root_module.module["name"] + + file_name_branch = project_dir / "branch.xml" + + with open(file_name_branch, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") + + owner_by_modules = defaultdict(list) + + for module in root.select("moduleinfo"): + layer_url = module.layerurl.text + layer_name = layer_url_by_name[layer_url] + + module_id = module.moduleid.text + module_name = layer_module_id_by_name[layer_url, module_id] + + definition_path_id = None + if module.definition: + definition_path_id = module.definition.get("path") + + if definition_path_id and definition_path_id != module_id: + title = f"{layer_name}::{module_name}/{definition_path_id} ({module_id}/{definition_path_id})" + else: + title = f"{layer_name}::{module_name} ({module_id})" + + owner_emails = [el.text for el in module.select("owneremail")] + for email in owner_emails: + if title not in owner_by_modules[email]: + owner_by_modules[email].append(title) + + return owner_by_modules + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk" + owner_by_modules = get_owner_by_modules(project_dir) + + for email in sorted(owner_by_modules): + modules = owner_by_modules[email] + modules.sort() + + print(f"{email} ({len(modules)}):") + for module in modules: + print(f" {module}") + print() diff --git a/job_compassplus/tx_remove_hs_err_pid_log/main.py b/job_compassplus/tx_remove_hs_err_pid_log/main.py new file mode 100644 index 000000000..93083d911 --- /dev/null +++ b/job_compassplus/tx_remove_hs_err_pid_log/main.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import logging +import time +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + + +def get_logger( + name=__file__, + file: str | Path = "log.txt", + formatter: str = "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", + encoding="utf-8", + log_stdout=True, + log_file=True, +): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter(formatter) + + if log_file: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + if log_stdout: + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +DIR = Path(__file__).resolve().parent + +log = get_logger( + file=DIR / "deleted.txt", + formatter="[%(asctime)s] %(message)s", +) + + +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] + + +def run(dirs: list[str | Path]) -> None: + print(f"\nStarted {dt.datetime.today()}") + + for dir_path in dirs: + print(dir_path) + for file_name in Path(dir_path).glob("*/hs_err_pid*.log"): + ctime_timestamp = file_name.stat().st_ctime + ctime = dt.datetime.fromtimestamp(ctime_timestamp) + ctime = ctime.replace(microsecond=0) + + text = f"{file_name} (date creation: {ctime})" + print(text) + + # Удаление, если с даты создания прошло больше 1 часа + if dt.datetime.today() > ctime + dt.timedelta(hours=1): + log.info(text) + file_name.unlink(missing_ok=True) + + print(f"\nFinished {dt.datetime.today()}") + + +if __name__ == "__main__": + if "--one" in sys.argv: + run(DIRS) + sys.exit() + + while True: + run(DIRS) + time.sleep(2 * 60 * 60) diff --git a/job_compassplus/tx_remove_java_pid_hprof/main.py b/job_compassplus/tx_remove_java_pid_hprof/main.py new file mode 100644 index 000000000..3d84d5697 --- /dev/null +++ b/job_compassplus/tx_remove_java_pid_hprof/main.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import logging +import time +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + + +def get_logger( + name=__file__, + file: str | Path = "log.txt", + formatter: str = "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", + encoding="utf-8", + log_stdout=True, + log_file=True, +): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter(formatter) + + if log_file: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + if log_stdout: + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +DIR = Path(__file__).resolve().parent +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] + + +log = get_logger( + file=DIR / "deleted.txt", + formatter="[%(asctime)s] %(message)s", +) + + +def run(dirs: list[str | Path]) -> None: + print(f"\nStarted {dt.datetime.today()}") + + for dir_path in dirs: + print(dir_path) + path = Path(dir_path) + + files = list(path.glob("*/*.hprof")) + files += list(path.glob("*/.config/var/log/heapdump.hprof.old")) + for f in files: + ctime_timestamp = f.stat().st_ctime + ctime = dt.datetime.fromtimestamp(ctime_timestamp) + ctime = ctime.replace(microsecond=0) + + file_size = f.stat().st_size + + text = f"{f} (date creation: {ctime}, size: {sizeof_fmt(file_size)})" + print(text) + + # Удаление, если с даты создания прошло больше 1 часа + if dt.datetime.today() > ctime + dt.timedelta(hours=1): + log.info(text) + f.unlink(missing_ok=True) + + print(f"\nFinished {dt.datetime.today()}") + + +if __name__ == "__main__": + if "--one" in sys.argv: + run(DIRS) + + sys.exit() + + while True: + run(DIRS) + time.sleep(2 * 60 * 60) diff --git a/job_compassplus/visa_base2_tc_counter.py b/job_compassplus/visa_base2_tc_counter.py new file mode 100644 index 000000000..a995cc86d --- /dev/null +++ b/job_compassplus/visa_base2_tc_counter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + + +# TODO: argparser +DIR = Path(r"C:\Users\ipetrash\Desktop\Visa Base 2") + +for path in DIR.rglob("*"): + if not path.is_file() or "errors" in str(path): + continue + + print(path) + + tc_by_counter = Counter() + tc_tcr_by_counter = Counter() + + with open(path, "rb") as f: + for line in f: + is_ctf: bool = len(line.rstrip(b"\n\r")) == 168 + tc_raw: bytes = line[:2] + tcr_raw: bytes = line[3:4] if is_ctf else line[5:6] + # TODO: Support EBCDIC, example b'\xf9\xf0' -> 90 + try: + tc: str = tc_raw.decode("ascii") + except UnicodeDecodeError as e: + print(f" [#] Skip. Error on {tc_raw}: {e}") + break + + try: + tcr: str = tcr_raw.decode("ascii") + except UnicodeDecodeError as e: + print(f" [#] Skip. Error on {tcr_raw}: {e}") + break + + if not tc.strip() or tc in ["90", "91", "92", "00"]: + continue + + tc_by_counter.update([tc]) + tc_tcr_by_counter.update([f"{tc}-{tcr}"]) + + print(" TC: " + ", ".join(f"{k}: {v}" for k, v in tc_by_counter.items())) + print(" TC-TCR: " + ", ".join(f"{k}: {v}" for k, v in tc_tcr_by_counter.items())) diff --git a/job_compassplus/web__show_job_report/main.py b/job_compassplus/web__show_job_report/main.py new file mode 100644 index 000000000..c23ea825b --- /dev/null +++ b/job_compassplus/web__show_job_report/main.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +"""Вывод переработки текущего (или конкретного) пользователя""" + + +import logging +import sys +from pathlib import Path + +from flask import Flask + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent / "job_report")) +from utils import get_report_persons_info, get_person_info + + +app = Flask(__name__) + +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/") +def index() -> str: + report_dict = get_report_persons_info() + + try: + person = report_dict["Текущий пользователь"][0] + except: + person = None + + if person is None: + person = get_person_info(second_name="Петраш", report_dict=report_dict) + + if person: + return f"{person.full_name} {person.deviation_of_time}" + + return "Not found specific user" + + +if __name__ == "__main__": + # Localhost + app.run(port=5001) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/job_compassplus/web_counter/README.md b/job_compassplus/web_counter/README.md new file mode 100644 index 000000000..09a991ce9 --- /dev/null +++ b/job_compassplus/web_counter/README.md @@ -0,0 +1,8 @@ +### Links: + * http://127.0.0.1:7777/docs + * http://127.0.0.1:7777/redoc + +### Run: +``` +python.exe -m uvicorn main:app --reload --port=7777 +``` \ No newline at end of file diff --git a/job_compassplus/web_counter/config.py b/job_compassplus/web_counter/config.py new file mode 100644 index 000000000..2fcb616c2 --- /dev/null +++ b/job_compassplus/web_counter/config.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" diff --git a/job_compassplus/web_counter/db.py b/job_compassplus/web_counter/db.py new file mode 100644 index 000000000..260e1d0af --- /dev/null +++ b/job_compassplus/web_counter/db.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import time +import sys + +from typing import Type, Iterable, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, + BlobField, + IntegerField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DIR, DB_FILE_NAME + +sys.path.append(str(DIR.parent.parent)) +from shorten import shorten + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + if isinstance(field, BlobField) and v is not None: + v = f"<{len(v)} bytes>" + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Counter(BaseModel): + name = CharField(primary_key=True) + value = IntegerField(default=0) + + @classmethod + def get_or_create(cls, name: str) -> "Counter": + obj: Counter | None = cls.get_or_none(name=name) + if not obj: + obj = cls.create(name=name) + return obj + + @classmethod + def increment(cls, name: str) -> int: + # Create or ignore + cls.get_or_create(name) + + cls.update(value=cls.value + 1).where(cls.name == name).execute() + return cls.get_or_create(name).value + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() diff --git a/job_compassplus/web_counter/main.py b/job_compassplus/web_counter/main.py new file mode 100644 index 000000000..d520728c4 --- /dev/null +++ b/job_compassplus/web_counter/main.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI +from db import Counter + + +app = FastAPI() + + +@app.get("/increment/{name}") +def increment(name: str) -> int: + return Counter.increment(name) diff --git a/job_compassplus/web_counter/run_python10.bat b/job_compassplus/web_counter/run_python10.bat new file mode 100644 index 000000000..789784ee1 --- /dev/null +++ b/job_compassplus/web_counter/run_python10.bat @@ -0,0 +1 @@ +C:\Users\ipetrash\AppData\Local\Programs\Python\Python310\python.exe -m uvicorn main:app --reload --port=7777 \ No newline at end of file diff --git a/job_report/__init__.py b/job_report/__init__.py deleted file mode 100644 index 847d83e72..000000000 --- a/job_report/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from job_report.utils import get_report_context, get_report_persons_info, get_person_info diff --git a/job_report/main.py b/job_report/main.py deleted file mode 100644 index 5f1bc835f..000000000 --- a/job_report/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# p12 to pem: -# C:\Users\ipetrash>openssl pkcs12 -in ipetrash.p12 -out ipetrash.pem -nodes -clcerts -# Enter Import Password: -# MAC verified OK -# OR: -# OpenSSL_example\p12_to_pem.py - -PEM_FILE_NAME = 'ipetrash.pem' - - -if __name__ == '__main__': - from job_report.utils import get_report_persons_info, get_person_info - report_dict = get_report_persons_info(PEM_FILE_NAME) - - # Вывести всех сотрудников, отсортировав их по количестве переработанных часов - from itertools import chain - person_list = set(chain(*report_dict.values())) - - # Проверка того, что сортировка работает (в принципе, думаю можно удалить) - assert sorted(person_list, key=lambda x: x.deviation_of_time) == \ - sorted(person_list, key=lambda x: x.deviation_of_time.total) - - sorted_person_list = sorted(person_list, key=lambda x: x.deviation_of_time, reverse=True) - - for i, person in enumerate(sorted_person_list, 1): - print('{:>3}. {} {}'.format(i, person.full_name, person.deviation_of_time)) - - print() - person = get_person_info(PEM_FILE_NAME, second_name='Петраш', first_name='Илья', report_dict=report_dict) - if person: - print('#{}. {} {}'.format(sorted_person_list.index(person) + 1, person.full_name, person.deviation_of_time)) diff --git a/job_report/report_person.py b/job_report/report_person.py deleted file mode 100644 index 59564a576..000000000 --- a/job_report/report_person.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from functools import total_ordering - - -class ReportPerson: - """Класс для описания сотрудника в отчете.""" - - def __init__(self, tags): - # ФИО - self.second_name, self.first_name, self.middle_name = tags[0].split() - - # Невыходов на работу - self.absence_from_work = int(tags[1]) - - # По календарю (смен / ч:мин) - # Для точного значения посещенных дней, может быть указано как "3 = 4- (1 О)", поэтому - # отсекаем правую, от знака равно, сторону, удаляем пробелы и переводим в число - self.need_to_work_days = self.get_work_day(tags[2]) - self.need_to_work_on_time = self.get_work_time(tags[3]) - - # Фактически (смен / ч:мин) - self.worked_days = self.get_work_day(tags[4]) - self.worked_time = self.get_work_time(tags[5]) - - # Отклонение (смен / ч:мин) - self.deviation_of_day = self.get_work_day(tags[6]) - self.deviation_of_time = self.get_work_time(tags[7]) - - @property - def full_name(self): - return self.second_name + ' ' + self.first_name + ' ' + self.middle_name - - @staticmethod - def get_work_day(day_str): - return int(day_str) if '=' not in day_str else int(day_str.split('=')[0].strip()) - - @total_ordering - class Time: - """Простой класс для хранения даты работы.""" - - def __init__(self, time_str): - self._hours, self._minutes = map(int, time_str.split(':')) - - @property - def total(self): - """Всего минут""" - - return self._hours * 60 + self._minutes - - def __repr__(self): - return "{:0>2}:{:0>2}".format(self._hours, self._minutes) - - def __eq__(self, other): - return self.total == other.total - - def __lt__(self, other): - return self.total < other.total - - @staticmethod - def get_work_time(time_str): - return ReportPerson.Time(time_str) - - def __hash__(self): - return hash(self.full_name) - - def __eq__(self, other): - return self.full_name == other.full_name - - def __repr__(self): - return "{}. Невыходов на работу: {}. По календарю ({} смен / {} ч:мин). " \ - "Фактически ({} смен / {} ч:мин) Отклонение ({} смен / {} ч:мин)".format(self.full_name, - self.absence_from_work, - self.need_to_work_days, - self.need_to_work_on_time, - self.worked_days, - self.worked_time, - self.deviation_of_day, - self.deviation_of_time, - ) diff --git a/job_report/utils.py b/job_report/utils.py deleted file mode 100644 index 7d650afa9..000000000 --- a/job_report/utils.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from datetime import datetime -from collections import defaultdict -import os.path -from itertools import chain - -import requests -requests.packages.urllib3.disable_warnings() - -from bs4 import BeautifulSoup - -from job_report.report_person import ReportPerson - - -URL = 'https://confluence.compassplus.ru/reports/index.jsp' -USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0' - -LOGGING_DEBUG = False - - -if LOGGING_DEBUG: - import logging - logging.basicConfig(level=logging.DEBUG) - - -def get_report_context(pem_file_name): - headers = { - 'User-Agent': USER_AGENT - } - - rs = requests.get(URL, headers=headers, cert=pem_file_name, verify=False) - if LOGGING_DEBUG: - print('rs=', rs) - print('rs.request.headers=', rs.request.headers) - print('rs.headers=', rs.headers) - - today = datetime.today() - data = { - 'dep': 'all', - 'rep': 'rep3', - 'period': today.strftime('%Y-%m'), - 'v': int(today.timestamp() * 1000), - 'type': 'normal', - } - - headers = { - 'cookie': rs.headers['set-cookie'], - 'User-Agent': USER_AGENT, - } - - rs = requests.post(URL, data=data, headers=headers, cert=pem_file_name, verify=False) - if LOGGING_DEBUG: - print('rs=', rs) - print('rs.request.headers=', rs.request.headers) - print('rs.headers=', rs.headers) - - return rs.text - - -def get_report_persons_info(pem_file_name): - today = datetime.today().strftime('%d%m%y') - report_file_name = 'report_{}.html'.format(today) - - # Если кэш-файл отчета не существует, загружаем новые данные и сохраняем в кэш-файл - if not os.path.exists(report_file_name): - if LOGGING_DEBUG: - print('{} not exist'.format(report_file_name)) - - context = get_report_context(pem_file_name) - - with open(report_file_name, mode='w', encoding='utf-8') as f: - f.write(context) - else: - if LOGGING_DEBUG: - print('{} exist'.format(report_file_name)) - - with open(report_file_name, encoding='utf-8') as f: - context = f.read() - - html = BeautifulSoup(context, 'lxml') - report = html.select('#report tbody tr') - - current_dep = None - - report_dict = defaultdict(set) - - for row in report: - children = list(row.children) - if len(children) == 1 and children[0].name == 'th': - current_dep = children[0].text.strip() - continue - - if children[0].has_attr('class') and children[0].attrs['class'][0] == 'person': - person_tags = [children[0].text] + [i.text for i in row.nextSibling.select('td')[1:]] - person = ReportPerson(person_tags) - - report_dict[current_dep].add(person) - - return report_dict - - -def get_person_info(pem_file_name, second_name, first_name=None, middle_name=None, report_dict=None): - if report_dict is None: - report_dict = get_report_persons_info(pem_file_name) - - # Вывести всех сотрудников, отсортировав их по количеству переработанных часов - for person in list(chain(*report_dict.values())): - found = person.second_name == second_name - - if first_name is not None: - found = found and person.first_name == first_name - - if middle_name is not None: - found = found and person.middle_name == middle_name - - if found: - return person diff --git a/join_dict_values_if_key_in_other_values.py b/join_dict_values_if_key_in_other_values.py index 751420d5a..157fab459 100644 --- a/join_dict_values_if_key_in_other_values.py +++ b/join_dict_values_if_key_in_other_values.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://ru.stackoverflow.com/questions/854015/ @@ -11,7 +11,7 @@ """ -total = {'a': ['b', 'd'], 'b': ['c', 'd'], 'c': ['d']} +total = {"a": ["b", "d"], "b": ["c", "d"], "c": ["d"]} for k in total: for k1, v1 in total.items(): diff --git a/join_words.py b/join_words.py new file mode 100644 index 000000000..41444b98c --- /dev/null +++ b/join_words.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +def join_words(word_1: str, word_2: str) -> str: + result: str = word_1 + word_2 + for i in range(min(len(word_1), len(word_2))): + if word_1[-i:].upper() == word_2[:i].upper(): + result = word_1.removesuffix(word_1[-i:]) + word_2 + + return result + + +if __name__ == "__main__": + result = join_words("Капитан", "АНИМЕ") + print(result) + assert result == "КапитАНИМЕ" diff --git a/json_examples/counter/main.py b/json_examples/counter/main.py index 6300229d9..16070beef 100644 --- a/json_examples/counter/main.py +++ b/json_examples/counter/main.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -file_name = 'counter.json' - import json -json_data = json.load(open(file_name, encoding='utf-8')) + + +file_name = "counter.json" + +json_data = json.load(open(file_name, encoding="utf-8")) print(json_data) # Изменение объекта -json_data['counter'] += 1 +json_data["counter"] += 1 # Сохранение -json.dump(json_data, open(file_name, mode='w', encoding='utf-8')) +json.dump(json_data, open(file_name, mode="w", encoding="utf-8")) diff --git a/json_examples/dump__serialization__custom_object__default.py b/json_examples/dump__serialization__custom_object__default.py index ff57de312..41e484b89 100644 --- a/json_examples/dump__serialization__custom_object__default.py +++ b/json_examples/dump__serialization__custom_object__default.py @@ -1,11 +1,57 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import json from decimal import Decimal -data = ((69695, 'CASTROL', '156f9d', 'Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover', 'Castrol-156F9D-Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover', Decimal('1.00'), '', Decimal('684.25'), 0, 13155264, '', Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), '7', '', ''), (69695, 'CASTROL', '15667c', 'Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное', 'Castrol-15667C-Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное', Decimal('40.00'), '', Decimal('599.15'), 0, 13155265, '', Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), '7', '', '')) + + +data = ( + ( + 69695, + "CASTROL", + "156f9d", + "Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover", + "Castrol-156F9D-Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover", + Decimal("1.00"), + "", + Decimal("684.25"), + 0, + 13155264, + "", + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + "7", + "", + "", + ), + ( + 69695, + "CASTROL", + "15667c", + "Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное", + "Castrol-15667C-Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное", + Decimal("40.00"), + "", + Decimal("599.15"), + 0, + 13155265, + "", + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + "7", + "", + "", + ), +) print(data) @@ -21,7 +67,6 @@ def my_default(obj): return str(obj) -import json json_data = json.dumps(data, ensure_ascii=False, default=str) print(json_data) diff --git a/json_examples/e.py b/json_examples/e.py index 36086d2ca..5397e5aa3 100644 --- a/json_examples/e.py +++ b/json_examples/e.py @@ -1,31 +1,35 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import json -if __name__ == '__main__': +if __name__ == "__main__": # Compact encoding: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}])) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}])) # Custom separators: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}], separators=(',', ':'))) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}], separators=(",", ":"))) # Pretty print: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}], indent=4)) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}], indent=4)) # Sort key: - print(json.dumps(["z", "b", "d", {'b': 5, 'a': 7, 'c': 2}], sort_keys=True, indent=4)) + print( + json.dumps(["z", "b", "d", {"b": 5, "a": 7, "c": 2}], sort_keys=True, indent=4) + ) # Create object: obj = [ - 3, 2, 1, + 3, + 2, + 1, [2, 1, 1, 4], { - 'a': 1, - 'b': 2, - 'c': 3, - } + "a": 1, + "b": 2, + "c": 3, + }, ] print(obj) @@ -39,5 +43,5 @@ # Test: print(obj2[3][0]) - print(obj2[4]['c']) - print(obj2[4]['b']) + print(obj2[4]["c"]) + print(obj2[4]["b"]) diff --git a/json_examples/load_with_replacing_value.py b/json_examples/load_with_replacing_value.py index 10ce4e5f8..86edf39a0 100644 --- a/json_examples/load_with_replacing_value.py +++ b/json_examples/load_with_replacing_value.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json def dict_clean(items, default): - return { - k: default if v is None else v - for k, v in items - } + return {k: default if v is None else v for k, v in items} text = """ @@ -30,15 +27,17 @@ def dict_clean(items, default): genre_translate = json.loads( - text, encoding='utf-8', - object_pairs_hook=lambda items: dict_clean(items, default=[]) + text, + encoding="utf-8", + object_pairs_hook=lambda items: dict_clean(items, default=[]), ) print(genre_translate) # {'Action-bar': [], 'Action': 'Action', 'Children': [{'Action': []}, {'Action': True}, {'Action': 'false'}, {'Action': {'need': []}}], 'RGB-bar': []} genre_translate = json.loads( - text, encoding='utf-8', - object_pairs_hook=lambda items: dict_clean(items, default="") + text, + encoding="utf-8", + object_pairs_hook=lambda items: dict_clean(items, default=""), ) print(genre_translate) # {'Action-bar': '', 'Action': 'Action', 'Children': [{'Action': ''}, {'Action': True}, {'Action': 'false'}, {'Action': {'need': ''}}], 'RGB-bar': ''} diff --git a/jsonmerge.py b/jsonmerge.py index 3c30fc8e3..9abd250a6 100644 --- a/jsonmerge.py +++ b/jsonmerge.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -40,8 +40,7 @@ # TODO: нужно тестить, возможно, алгоритм где-то дырявый - -def jsonmerge(j1, j2): +def jsonmerge(j1, j2) -> None: for k, v in j2.items(): if k in j1 and isinstance(j1[k], dict) and isinstance(v, dict): jsonmerge(j1[k], v) diff --git a/k_notation_to_number.py b/k_notation_to_number.py index 84985a5c0..1aedeed5e 100644 --- a/k_notation_to_number.py +++ b/k_notation_to_number.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def k_notation_to_number(num_str: str) -> int: - i = num_str.index('k') + i = num_str.index("k") num, size = num_str[:i], len(num_str[i:]) - return int(float(num) * (1000 ** size)) + return int(float(num) * (1000**size)) -if __name__ == '__main__': - print(k_notation_to_number("1k")) # 1000 +if __name__ == "__main__": + print(k_notation_to_number("1k")) # 1000 print(k_notation_to_number("1.5kk")) # 1500000 - print(k_notation_to_number("1kkk")) # 1000000000 + print(k_notation_to_number("1kkk")) # 1000000000 diff --git a/kivy_examples/generate_form_with_cycle.py b/kivy_examples/generate_form_with_cycle.py index e7b20072d..9cab7a17e 100644 --- a/kivy_examples/generate_form_with_cycle.py +++ b/kivy_examples/generate_form_with_cycle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -10,17 +10,17 @@ class MyWidget(GridLayout): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.cols = 1 self.spacing = 10 self.padding = 10 - label_list = ['Собака', 'Сосед', 'Кот', 'Биткоин'] + label_list = ["Собака", "Сосед", "Кот", "Биткоин"] for i, title in enumerate(label_list): label = Label(text=title) - label.id = 'id:' + str(i) + label.id = "id:" + str(i) self.add_widget(label) @@ -30,5 +30,5 @@ def build(self): return MyWidget() -if __name__ == '__main__': +if __name__ == "__main__": MyApp().run() diff --git a/kivy_examples/hello_world.py b/kivy_examples/hello_world.py index 375774a11..0b5d70f2f 100644 --- a/kivy_examples/hello_world.py +++ b/kivy_examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Install kivy: @@ -15,8 +15,8 @@ class MyApp(App): def build(self): - return Label(text='Hello world') + return Label(text="Hello world") -if __name__ == '__main__': +if __name__ == "__main__": MyApp().run() diff --git a/kivy_examples/kivymd__example.py b/kivy_examples/kivymd__example.py new file mode 100644 index 000000000..d46d8b185 --- /dev/null +++ b/kivy_examples/kivymd__example.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from kivy.lang import Builder +from kivymd.app import MDApp +from kivymd.uix.list import OneLineListItem + + +KV = """ +ScrollView: + MDList: + id: container +""" + + +class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self) -> None: + for i in os.listdir(path="."): + self.root.ids.container.add_widget( + OneLineListItem(text=i, on_release=lambda item, i=i: print(i)) + ) + + +if __name__ == "__main__": + Test().run() diff --git a/kivy_examples/kv_file_switch_between_screens/main.py b/kivy_examples/kv_file_switch_between_screens/main.py new file mode 100644 index 000000000..495422a27 --- /dev/null +++ b/kivy_examples/kv_file_switch_between_screens/main.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from kivy.app import App +from kivy.uix.screenmanager import Screen + + +class EegScreen(Screen): + pass + + +class SettingsScreen(Screen): + pass + + +class ExchangeScreen(Screen): + pass + + +class ConnectionsScreen(Screen): + pass + + +class MyApp(App): + pass + + +if __name__ == "__main__": + MyApp().run() diff --git a/kivy_examples/kv_file_switch_between_screens/my.kv b/kivy_examples/kv_file_switch_between_screens/my.kv new file mode 100644 index 000000000..905ab225f --- /dev/null +++ b/kivy_examples/kv_file_switch_between_screens/my.kv @@ -0,0 +1,66 @@ +BoxLayout: + orientation: 'horizontal' + + BoxLayout: + orientation: 'vertical' + + Label: + text: 'PERMANENT' + + Button: + text: 'Connections' + font_size: 20 + on_release: screen_manager.current = 'ConnectionsScreen' + Button: + text: 'Exchange' + font_size: 20 + on_release: screen_manager.current = 'ExchangeScreen' + + Button: + text: 'EEG Signal' + font_size: 20 + on_release: screen_manager.current = 'EegScreen' + + Button: + text: 'Settings' + font_size: 20 + on_release: screen_manager.current = 'SettingsScreen' + + ScreenManager: + id: screen_manager + + EegScreen: + id: eeg_screen + name: 'EegScreen' + + SettingsScreen: + id: settings_screen + name: 'SettingsScreen' + + ExchangeScreen: + id: exchange_screen + name: 'ExchangeScreen' + + ConnectionsScreen: + id: connections_screen + name: 'ConnectionsScreen' + +: + FloatLayout: + Label: + text: 'EegScreen' + +: + FloatLayout: + Label: + text: 'SettingsScreen' + +: + FloatLayout: + Label: + text: 'ExchangeScreen' + +: + FloatLayout: + Label: + text: 'ConnectionsScreen' \ No newline at end of file diff --git a/kivy_examples/login_screen.py b/kivy_examples/login_screen.py index f13d0054c..f745d35ed 100644 --- a/kivy_examples/login_screen.py +++ b/kivy_examples/login_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -13,26 +13,26 @@ class LoginScreen(GridLayout): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.cols = 2 self.spacing = 10 self.padding = 10 - self.add_widget(Label(text='User Name')) + self.add_widget(Label(text="User Name")) self.username = TextInput(multiline=False) - self.username.text = 'admin' + self.username.text = "admin" self.add_widget(self.username) - self.add_widget(Label(text='password')) + self.add_widget(Label(text="password")) self.password = TextInput(password=True, multiline=False) - self.password.text = 'admin' + self.password.text = "admin" self.add_widget(self.password) self.add_widget(Button(text="Ok", on_press=self.check)) - def check(self, button): + def check(self, button) -> None: print("auth called") message = "Success!" if self.username.text == "admin" else "Need admin!" @@ -53,5 +53,5 @@ def build(self): return LoginScreen() -if __name__ == '__main__': +if __name__ == "__main__": MyLoginScreenApp().run() diff --git a/kivy_examples/switch_between_widgets.py b/kivy_examples/switch_between_widgets.py index 678292c07..2009ce5a2 100644 --- a/kivy_examples/switch_between_widgets.py +++ b/kivy_examples/switch_between_widgets.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -11,7 +11,7 @@ class ScreenMain(Screen): - def __init__ (self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) box_layout = BoxLayout(orientation="vertical", spacing=5, padding=[10]) @@ -26,13 +26,13 @@ def __init__ (self, **kwargs): box_layout.add_widget(button_new_pasword) self.add_widget(box_layout) - def _on_press_button_new_pasword(self, *args): - self.manager.transition.direction = 'left' - self.manager.current = 'len_password' + def _on_press_button_new_pasword(self, *args) -> None: + self.manager.transition.direction = "left" + self.manager.current = "len_password" class ScreenLenPassword(Screen): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) box_layout = BoxLayout(orientation="vertical", spacing=5, padding=[10]) @@ -47,16 +47,16 @@ def __init__(self, **kwargs): box_layout.add_widget(button_new_pasword) self.add_widget(box_layout) - def _on_press_button_new_pasword(self, *args): - self.manager.transition.direction = 'right' - self.manager.current = 'main_screen' + def _on_press_button_new_pasword(self, *args) -> None: + self.manager.transition.direction = "right" + self.manager.current = "main_screen" class MainApp(App): def build(self): sm = ScreenManager() - sm.add_widget(ScreenMain(name='main_screen')) - sm.add_widget(ScreenLenPassword(name='len_password')) + sm.add_widget(ScreenMain(name="main_screen")) + sm.add_widget(ScreenLenPassword(name="len_password")) return sm diff --git a/krakozyabry_tx/krakozyabry_tx.py b/krakozyabry_tx/krakozyabry_tx.py index d17fb9f99..0535c6803 100644 --- a/krakozyabry_tx/krakozyabry_tx.py +++ b/krakozyabry_tx/krakozyabry_tx.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример конвертирования кракозябры "РќРµ указана точка" в человеческий вид.""" @@ -11,6 +11,6 @@ from PySide.QtGui import QApplication, QMessageBox -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) - QMessageBox.information(None, None, open('t', encoding='utf-8').read()) + QMessageBox.information(None, None, open("t", encoding="utf-8").read()) diff --git a/languagetool/check.py b/languagetool/check.py index 09f440aba..53effbed8 100644 --- a/languagetool/check.py +++ b/languagetool/check.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,6 +14,9 @@ """ +import requests + + def check_ru(text: str) -> list(): """ Функция делает запрос на https://languagetool.org/ чтобы проверить на правильность указанный текст. @@ -22,20 +25,19 @@ def check_ru(text: str) -> list(): """ - url = 'https://languagetool.org/api/v2/check' + url = "https://languagetool.org/api/v2/check" post_data = { - 'disabledRules': 'WHITESPACE_RULE', - 'allowIncompleteResults': 'true', - 'text': text, - 'language': 'ru', + "disabledRules": "WHITESPACE_RULE", + "allowIncompleteResults": "true", + "text": text, + "language": "ru", } - import requests rs = requests.post(url, data=post_data) if not rs.ok: - raise Exception('Проблема с {}, status_code = {}'.format(url, rs.status_code)) + raise Exception(f"Проблема с {url}, status_code = {rs.status_code}") - return rs.json()['matches'] + return rs.json()["matches"] def is_ok_ru(text: str) -> bool: @@ -47,7 +49,7 @@ def is_ok_ru(text: str) -> bool: return not check_ru(text) -if __name__ == '__main__': +if __name__ == "__main__": text = """\ Интересует другое почему она возникла? Дубаль два, надеюсь все пройдет отлично. @@ -58,20 +60,22 @@ def is_ok_ru(text: str) -> bool: if not matches: print("Ошибок нет") else: - print('Найденные проблемы:') + print("Найденные проблемы:") for match in matches: - error = match['message'] - context = match['context'] - offset = context['offset'] - length = context['length'] + error = match["message"] + context = match["context"] + offset = context["offset"] + length = context["length"] - error_text = context['text'][offset: offset + length] - print('"{}" [{}:{}]: "{}" -> {}'.format(error_text, offset, length, error, - [i['value'] for i in match['replacements']])) + error_text = context["text"][offset : offset + length] + replacements = [i["value"] for i in match["replacements"]] + print( + f'"{error_text}" [{offset}:{length}]: "{error}" -> {replacements}' + ) print() - print('-' * 20) + print("-" * 20) print(is_ok_ru(text)) print(is_ok_ru("Все хорошо!")) diff --git a/leftpad.py b/leftpad.py index 3f15b5e23..d54f944c1 100644 --- a/leftpad.py +++ b/leftpad.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для интереса написать аналог NPM leftpad. @@ -18,7 +18,7 @@ # // => "01" -def leftpad(text, size, ch=' '): +def leftpad(text: str | int, size: int, ch: str | int = " "): text = str(text) ch = str(ch) @@ -27,17 +27,17 @@ def leftpad(text, size, ch=' '): # Версия с использованием общего алгоритм -def leftpad2(text, size, ch=' '): +def leftpad2(text: str | int, size: int, ch: str | int = " "): text = str(text) - ch = str(ch) - text_size = len(text) - result = '' pad_len = size - text_size if pad_len <= 0: return text + ch = str(ch) + + result = "" for i in range(pad_len): result += ch result += text @@ -45,11 +45,11 @@ def leftpad2(text, size, ch=' '): return result -if __name__ == '__main__': - assert leftpad('foo', 5) == " foo" - assert leftpad('foobar', 6) == "foobar" +if __name__ == "__main__": + assert leftpad("foo", 5) == " foo" + assert leftpad("foobar", 6) == "foobar" assert leftpad(1, 2, 0) == "01" - assert leftpad('foo', 5) == leftpad2('foo', 5) - assert leftpad('foobar', 6) == leftpad2('foobar', 6) - assert leftpad(1, 2, 0) == leftpad(1, 2, 0) + assert leftpad("foo", 5) == leftpad2("foo", 5) + assert leftpad("foobar", 6) == leftpad2("foobar", 6) + assert leftpad(1, 2, 0) == leftpad2(1, 2, 0) diff --git a/lex_yacc__examples/sly__examples/added_string_and_print.py b/lex_yacc__examples/sly__examples/added_string_and_print.py new file mode 100644 index 000000000..2b69916a5 --- /dev/null +++ b/lex_yacc__examples/sly__examples/added_string_and_print.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/dabeaz/sly/ +# SOURCE: https://sly.readthedocs.io/en/latest/sly.html + + +# pip install sly +from sly import Lexer, Parser + + +class MyLexer(Lexer): + tokens = { + NAME, + TEXT, + NUMBER, + PRINT, + PLUS, + TIMES, + MINUS, + DIVIDE, + ASSIGN, + LPAREN, + RPAREN, + } + ignore = " \t" + + # Tokens + NUMBER = r"\d+" + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NAME["print"] = PRINT + + @_(r'".+?"', r"'.+?'") + def TEXT(self, t): + return t + + # Special symbols + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + ASSIGN = r"=" + LPAREN = r"\(" + RPAREN = r"\)" + + # Ignored pattern + ignore_newline = r"\n+" + + # Extra action for newlines + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") + + def error(self, t) -> None: + print(f"Illegal character '{t.value[0]}'") + self.index += 1 + + +class MyParser(Parser): + tokens = MyLexer.tokens + + precedence = ( + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), + ) + + def __init__(self) -> None: + self.names = dict() + + @_("NAME ASSIGN expr") + def statement(self, p) -> None: + self.names[p.NAME] = p.expr + + @_("expr") + def statement(self, p) -> int | str: + return p.expr + + @_("PRINT expr") + def statement(self, p) -> None: + print(p.expr) + + @_("expr PLUS expr") + def expr(self, p): + # Javascript style :D + if isinstance(p.expr0, str) or isinstance(p.expr1, str): + return str(p.expr0) + str(p.expr1) + + return p.expr0 + p.expr1 + + @_("expr MINUS expr") + def expr(self, p): + return p.expr0 - p.expr1 + + @_("expr TIMES expr") + def expr(self, p): + return p.expr0 * p.expr1 + + @_("expr DIVIDE expr") + def expr(self, p): + return p.expr0 / p.expr1 + + @_("MINUS expr %prec UMINUS") + def expr(self, p): + return -p.expr + + @_("LPAREN expr RPAREN") + def expr(self, p): + return p.expr + + @_("NUMBER") + def expr(self, p) -> int: + return int(p.NUMBER) + + @_("TEXT") + def expr(self, p) -> str: + return p.TEXT[1:-1] + + @_("NAME") + def expr(self, p): + try: + return self.names[p.NAME] + except LookupError: + print(f"Undefined name {p.NAME!r}") + return 0 + + +if __name__ == "__main__": + lexer = MyLexer() + parser = MyParser() + + text = """ + "12" + '3' + 456 + """.strip() + value = parser.parse(lexer.tokenize(text)) + print(f"{text} = {value!r}") + # 12 + '3' + 456 = '123456' + + print() + + text = """ + "12" + '3' + """.strip() + value = parser.parse(lexer.tokenize(text)) + print(f"{text} = {value!r}") + # "12" + '3' = '123' + + print() + + items = [ + """text = "Hello " + 'World!'""", + "text", + ] + for line in items: + value = parser.parse(lexer.tokenize(line)) + if value is not None: + print(f"{line} = {value!r}") + else: + print(line) + # text = "Hello " + 'World!' + # text = 'Hello World!' + + print() + + for line in [ + 'print text + "!!"', + 'print(text + "!!")', + ]: + parser.parse(lexer.tokenize(line)) + + print() + + while True: + try: + text = input("calc > ") + except EOFError: + break + if text: + value = parser.parse(lexer.tokenize(text)) + if value is not None: + print(value) diff --git a/lex_yacc__examples/sly__examples/added_ternary_operator.py b/lex_yacc__examples/sly__examples/added_ternary_operator.py new file mode 100644 index 000000000..3ab9e16bc --- /dev/null +++ b/lex_yacc__examples/sly__examples/added_ternary_operator.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/dabeaz/sly/ +# SOURCE: https://sly.readthedocs.io/en/latest/sly.html + + +# pip install sly +from sly import Lexer, Parser + + +class MyLexer(Lexer): + tokens = { + NAME, + NUMBER, + IF, + ELSE, + TRUE, + FALSE, + PLUS, + TIMES, + MINUS, + DIVIDE, + LPAREN, + RPAREN, + ASSIGN, + EQ, + LT, + LE, + GT, + GE, + NE, + QUESTION, + COLON, + } + ignore = " \t" + + # Tokens + NUMBER = r"\d+" + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NAME["if"] = IF + NAME["else"] = ELSE + NAME["true"] = TRUE + NAME["false"] = FALSE + + # Special symbols + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + LPAREN = r"\(" + RPAREN = r"\)" + + QUESTION = r"\?" + COLON = r"\:" + + EQ = r"==" + ASSIGN = r"=" + LE = r"<=" + LT = r"<" + GE = r">=" + GT = r">" + NE = r"!=" + + @_(r"true") + def TRUE(self, t): + t.value = True + return t + + @_(r"false") + def FALSE(self, t): + t.value = False + return t + + # Ignored pattern + ignore_newline = r"\n+" + + # Extra action for newlines + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") + + def error(self, t) -> None: + print(f"Illegal character '{t.value[0]}'") + self.index += 1 + + +# TODO: WARNING: 12 shift/reduce conflicts +class MyParser(Parser): + tokens = MyLexer.tokens + + precedence = ( + ("left", IF, ELSE), + ("left", EQ, NE, LT, LE, GT, GE), + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), + ) + + def __init__(self) -> None: + self.names = dict() + + @_("NAME ASSIGN expr") + def statement(self, p) -> None: + self.names[p.NAME] = p.expr + + @_("expr") + def statement(self, p) -> int: + return p.expr + + # Ternary operator python + @_("expr IF expr ELSE expr") + def expr(self, p): + return p.expr0 if p.expr1 else p.expr2 + + # Ternary operator c + @_("expr QUESTION expr COLON expr") + def expr(self, p): + return p.expr1 if p.expr0 else p.expr2 + + @_("expr LT expr") + def expr(self, p) -> bool: + return p.expr0 < p.expr1 + + @_("expr LE expr") + def expr(self, p) -> bool: + return p.expr0 <= p.expr1 + + @_("expr GT expr") + def expr(self, p) -> bool: + return p.expr0 > p.expr1 + + @_("expr GE expr") + def expr(self, p) -> bool: + return p.expr0 >= p.expr1 + + @_("expr EQ expr") + def expr(self, p) -> bool: + return p.expr0 == p.expr1 + + @_("expr NE expr") + def expr(self, p) -> bool: + return p.expr0 != p.expr1 + + @_("expr PLUS expr") + def expr(self, p): + return p.expr0 + p.expr1 + + @_("expr MINUS expr") + def expr(self, p): + return p.expr0 - p.expr1 + + @_("expr TIMES expr") + def expr(self, p): + return p.expr0 * p.expr1 + + @_("expr DIVIDE expr") + def expr(self, p): + return p.expr0 / p.expr1 + + @_("MINUS expr %prec UMINUS") + def expr(self, p): + return -p.expr + + @_("LPAREN expr RPAREN") + def expr(self, p): + return p.expr + + @_("NUMBER") + def expr(self, p) -> int: + return int(p.NUMBER) + + @_("TRUE", "FALSE") + def expr(self, p) -> bool: + return p[0] + + @_("NAME") + def expr(self, p): + try: + return self.names[p.NAME] + except LookupError: + print(f"Undefined name {p.NAME!r}") + return 0 + + +if __name__ == "__main__": + lexer = MyLexer() + parser = MyParser() + + items = [ + "value = 2 > 1", + "value", + "value = 2 + 1 == 6 / 2", + "value", + ] + for line in items: + value = parser.parse(lexer.tokenize(line)) + if value is not None: + print(f"{line} = {value!r}") + else: + print(line) + # value = 2 > 1 + # value = True + # value = 2 + 1 == 6 / 2 + # value = True + + print() + + items = [ + # Ternary operator python + "123 if value else 456", + "1 if true else 0", + "1 if false else 0", + "true if 2 + 2 == 4 else false", + + # Ternary operator c + "2 + 1 == 6 / 2 ? 123 : 456", + "true ? 1 : 0", + "false ? 1 : 0", + "x = true ? 123 : 456", + "x", + ] + for line in items: + value = parser.parse(lexer.tokenize(line)) + if value is not None: + print(f"({line}) -> {value!r}") + else: + print(line) + # (123 if value else 456) -> 123 + # (1 if true else 0) -> 1 + # (1 if false else 0) -> 0 + # (true if 2 + 2 == 4 else false) -> True + # (2 + 1 == 6 / 2 ? 123 : 456) -> 123 + # (true ? 1 : 0) -> 1 + # (false ? 1 : 0) -> 0 + # x = true ? 123 : 456 + # (x) -> 123 + + print() + + while True: + try: + text = input("calc > ") + except EOFError: + break + if text: + value = parser.parse(lexer.tokenize(text)) + if value is not None: + print(value) diff --git a/lex_yacc__examples/sly__examples/calc.py b/lex_yacc__examples/sly__examples/calc.py new file mode 100644 index 000000000..9441833f6 --- /dev/null +++ b/lex_yacc__examples/sly__examples/calc.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/dabeaz/sly/ +# SOURCE: https://sly.readthedocs.io/en/latest/sly.html + + +# pip install sly +from sly import Lexer, Parser + + +class CalcLexer(Lexer): + tokens = {NAME, NUMBER, PLUS, TIMES, MINUS, DIVIDE, ASSIGN, LPAREN, RPAREN} + ignore = " \t" + + # Tokens + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NUMBER = r"\d+" + + # Special symbols + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + ASSIGN = r"=" + LPAREN = r"\(" + RPAREN = r"\)" + + # Ignored pattern + ignore_newline = r"\n+" + + # Extra action for newlines + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") + + def error(self, t) -> None: + print(f"Illegal character '{t.value[0]}'") + self.index += 1 + + +class CalcParser(Parser): + tokens = CalcLexer.tokens + + precedence = ( + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), + ) + + def __init__(self) -> None: + self.names = dict() + + @_("NAME ASSIGN expr") + def statement(self, p) -> None: + self.names[p.NAME] = p.expr + + @_("expr") + def statement(self, p) -> int: + return p.expr + + @_("expr PLUS expr") + def expr(self, p): + return p.expr0 + p.expr1 + + @_("expr MINUS expr") + def expr(self, p): + return p.expr0 - p.expr1 + + @_("expr TIMES expr") + def expr(self, p): + return p.expr0 * p.expr1 + + @_("expr DIVIDE expr") + def expr(self, p): + return p.expr0 / p.expr1 + + @_("MINUS expr %prec UMINUS") + def expr(self, p): + return -p.expr + + @_("LPAREN expr RPAREN") + def expr(self, p): + return p.expr + + @_("NUMBER") + def expr(self, p) -> int: + return int(p.NUMBER) + + @_("NAME") + def expr(self, p): + try: + return self.names[p.NAME] + except LookupError: + print(f"Undefined name {p.NAME!r}") + return 0 + + +if __name__ == "__main__": + lexer = CalcLexer() + parser = CalcParser() + + text = "2 + 2 * 2" + print(list(lexer.tokenize(text))) + # [ + # Token(type='NUMBER', value='2', lineno=1, index=0, end=1), + # Token(type='PLUS', value='+', lineno=1, index=2, end=3), + # Token(type='NUMBER', value='2', lineno=1, index=4, end=5), + # Token(type='TIMES', value='*', lineno=1, index=6, end=7), + # Token(type='NUMBER', value='2', lineno=1, index=8, end=9) + # ] + + value = parser.parse(lexer.tokenize(text)) + print(f"{text!r} = {value}") + # '2 + 2 * 2' = 6 + + print() + + items = [ + "a = 2", + "a = a * 2", + "b = 2", + "a + b + 1", + ] + for line in items: + value = parser.parse(lexer.tokenize(line)) + if value is not None: + print(f"{line!r} = {value}") + else: + print(f"{line!r}") + """ + 'a = 2' + 'a = a * 2' + 'b = 2' + 'a + b + 1' = 7 + """ + + print() + + while True: + try: + text = input("calc > ") + except EOFError: + break + if text: + value = parser.parse(lexer.tokenize(text)) + if value is not None: + print(value) diff --git a/lex_yacc__examples/sly__examples/json_by_goodmami.py b/lex_yacc__examples/sly__examples/json_by_goodmami.py new file mode 100644 index 000000000..157fd8f85 --- /dev/null +++ b/lex_yacc__examples/sly__examples/json_by_goodmami.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/goodmami/python-parsing-benchmarks/blob/611cfc20ca7b61f1f489b0e56f201f6888a5c67b/bench/helpers.py + + +""" +JSON parser implemented with SLY + +Implemented by Michael Wayne Goodman. For license information, see +https://github.com/goodmami/python-parsing-benchmarks/ + +""" + + +import re +from sly import Lexer, Parser + + +_json_unesc_re = re.compile(r'\\(["/\\bfnrt]|u[0-9A-Fa-f])') +_json_unesc_map = { + '"': '"', + "/": "/", + "\\": "\\", + "b": "\b", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", +} + + +def _json_unescape(m): + c = m.group(1) + if c[0] == "u": + return chr(int(c[1:], 16)) + c2 = _json_unesc_map.get(c) + if not c2: + raise ValueError(f"invalid escape sequence: {m.group(0)}") + return c2 + + +def json_unescape(s): + return _json_unesc_re.sub(_json_unescape, s[1:-1]) + + +def compile(): + class JsonLexer(Lexer): + tokens = {STRING, NUMBER, TRUE, FALSE, NULL} + ignore = " \t\n\r" + literals = {"{", "}", "[", "]", ":", ","} + + @_(r"-?(0|[1-9][0-9]*)(\.[0-9]+)?([Ee][+-]?[0-9]+)?") + def NUMBER(self, t): + try: + t.value = int(t.value) + except ValueError: + t.value = float(t.value) + return t + + @_(r'"([ !#-\[\]-\U0010ffff]+|\\(["\/\\bfnrt]|u[0-9A-Fa-f]{4}))*"') + def STRING(self, t): + t.value = json_unescape(t.value) + return t + + @_(r"true") + def TRUE(self, t): + t.value = True + return t + + @_(r"false") + def FALSE(self, t): + t.value = False + return t + + @_(r"null") + def NULL(self, t): + t.value = None + return t + + class JsonParser(Parser): + tokens = JsonLexer.tokens + start = "value" + + @_(r'"{" [ pairs ] "}"') + def value(self, p): + if p.pairs: + return dict(p.pairs) + else: + return {} + + @_(r'pair { "," pair }') + def pairs(self, p): + return [p.pair0] + p.pair1 + + @_(r'STRING ":" value') + def pair(self, p): + return (p.STRING, p.value) + + @_(r'"[" [ items ] "]"') + def value(self, p): + if p.items: + return p.items + else: + return [] + + @_(r'value { "," value }') + def items(self, p): + return [p.value0] + p.value1 + + @_("STRING", "NUMBER", "TRUE", "FALSE", "NULL") + def value(self, p): + return p[0] + + def error(self, p): + raise ValueError(p) + + lexer = JsonLexer() + parser = JsonParser() + + return lambda s: parser.parse(lexer.tokenize(s)) + + +parse = compile() + + +if __name__ == "__main__": + text = '{"abc": [1, 2.5, 3.0, true, null], "value": "123"}' + + print(parse(text)) + # {'abc': [1, 2.5, 3.0, True, None], 'value': '123'} + + import json + print(json.loads(text)) + # {'abc': [1, 2.5, 3.0, True, None], 'value': '123'} diff --git a/lex_yacc__examples/sly__examples/json_by_hadware.py b/lex_yacc__examples/sly__examples/json_by_hadware.py new file mode 100644 index 000000000..8694f3572 --- /dev/null +++ b/lex_yacc__examples/sly__examples/json_by_hadware.py @@ -0,0 +1,89 @@ +from sly import Lexer, Parser +import pprint + + +class JSONLexer(Lexer): + tokens = {"FLOAT", "INTEGER", "STRING"} + + literals = {"{", "}", "[", "]", ",", ":"} + ignore = " \t\n" + + @_(r"\".*?\"") + def STRING(self, t): + t.value = t.value.strip('"') + return t + + @_(r"\d+\.\d*") + def FLOAT(self, t): + t.value = float(t.value) + return t + + @_(r"\d+") + def INTEGER(self, t): + t.value = int(t.value) + return t + + +class JSONParser(Parser): + tokens = JSONLexer.tokens + start = "json" + + @_("object", "array") + def json(self, p): + return p[0] + + @_('"{" members "}"') + def object(self, p): + return {key: value for key, value in p.members} + + @_("pair") + def members(self, p): + return [p.pair] + + @_('pair "," members') + def members(self, p): + return [p.pair] + p.members + + @_('STRING ":" value') + def pair(self, p): + return p.STRING, p.value + + @_('"[" elements "]"') + def array(self, p): + return p.elements + + @_("value") + def elements(self, p): + return [p.value] + + @_('value "," elements') + def elements(self, p): + return [p.value] + p.elements + + @_("STRING", "INTEGER", "FLOAT", "object", "array") + def value(self, p): + return p[0] + + def error(self, p): + raise ValueError("Parsing error at token %s" % str(p)) + + +if __name__ == "__main__": + lexer = JSONLexer() + parser = JSONParser() + json_text = """ + {"menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + {"value": "New", "onclick": "CreateNewDoc()"}, + {"value": "Open", "onclick": "OpenDoc()"}, + {"value": "Close", "onclick": "CloseDoc()"} + ], + "delay" : 1.5 + } + }} + """ + result = parser.parse(lexer.tokenize(json_text)) + pprint.pprint(result) diff --git a/lex_yacc__examples/sly__examples/key=value.py b/lex_yacc__examples/sly__examples/key=value.py new file mode 100644 index 000000000..5f240181b --- /dev/null +++ b/lex_yacc__examples/sly__examples/key=value.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install sly +from sly import Lexer, Parser + + +class MyLexer(Lexer): + tokens = {NAME, ASSIGN, VALUE} + ignore = " \t" + + # Tokens + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + ASSIGN = "=" + VALUE = r".+" + + # Ignored pattern + ignore_newline = r"\n+" + + # Extra action for newlines + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") + + def error(self, t) -> None: + print(f"Illegal character {t.value[0]!r}") + self.index += 1 + + +class MyParser(Parser): + tokens = MyLexer.tokens + + def __init__(self) -> None: + self.names = dict() + + @_("NAME ASSIGN VALUE") + def statement(self, p) -> None: + self.names[p.NAME] = p.VALUE + + +if __name__ == "__main__": + lexer = MyLexer() + + text = "abc=123" + print(*list(lexer.tokenize(text)), sep="\n") + """ + Token(type='NAME', value='abc', lineno=1, index=0, end=3) + Token(type='ASSIGN', value='=', lineno=1, index=3, end=4) + Token(type='VALUE', value='123', lineno=1, index=4, end=7) + """ + print() + + parser = MyParser() + + lines = ["abc=123", "x = 999", "y = 111"] + for line in lines: + tokens = lexer.tokenize(line) + parser.parse(tokens) + + print(parser.names) + # {'abc': '123', 'x': '999', 'y': '111'} diff --git a/list_split_by_pairs.py b/list_split_by_pairs.py index 727b7112a..a7734a1ba 100644 --- a/list_split_by_pairs.py +++ b/list_split_by_pairs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" items = [5, 80, 3, 83, 1, 77, 1, 77, 2, 82, 1, 77, 5, 81, 2, 78, 1, 81, 5, 85, 5, 85, 4, 84, 2, 78, 1, 81, 3, 83] diff --git a/lived time.py b/lived time.py index a812fb90e..c54571569 100644 --- a/lived time.py +++ b/lived time.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import datetime + + my_bd = datetime.datetime(day=18, month=8, year=1992) my_life = datetime.datetime.today() - my_bd -print('lived time: days = {} <=> seconds = {}'.format(my_life.days, int(my_life.total_seconds()))) +print(f"lived time: days = {my_life.days} <=> seconds = {int(my_life.total_seconds())}") diff --git a/load_and_exec_py_from_url.py b/load_and_exec_py_from_url.py index 552977e1b..591734c0e 100644 --- a/load_and_exec_py_from_url.py +++ b/load_and_exec_py_from_url.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.request import urlopen -url = 'https://raw.githubusercontent.com/gil9red/SimplePyScripts/c4ef2f1636f7d75b87807e858ea1eea6116df773/print_triangle.py' + +url = "https://raw.githubusercontent.com/gil9red/SimplePyScripts/c4ef2f1636f7d75b87807e858ea1eea6116df773/print_triangle.py" with urlopen(url) as f: exec(f.read()) diff --git a/logged_human_time_to_seconds.py b/logged_human_time_to_seconds.py deleted file mode 100644 index 1289cbf02..000000000 --- a/logged_human_time_to_seconds.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def logged_human_time_to_seconds(human_time: str) -> int: - """Конвертирование человеко-читаемого времени в секунды. - - >>> logged_human_time_to_seconds("1 minute") - 60 - >>> logged_human_time_to_seconds("1 hour") - 3600 - >>> logged_human_time_to_seconds("6 hours, 30 minutes") - 23400 - >>> logged_human_time_to_seconds("1 day, 30 minutes") - 30600 - """ - - # Jira help: - # You can specify a time unit after a time value 'X', such as Xw, Xd, Xh or Xm, to represent weeks (w), - # days (d), hours (h) and minutes (m), respectively. - # If you do not specify a time unit, minute will be assumed. - # Your current conversion rates are 1w = 5d and 1d = 8h. - - total_seconds = 0 - - for part in human_time.split(', '): - value, metric = part.lower().split() - value = int(value) - - if 'minute' in metric: - total_seconds += value * 60 - - elif 'hour' in metric: - total_seconds += value * 60 * 60 - - elif 'day' in metric: - total_seconds += value * 8 * 60 * 60 - - elif 'week' in metric: - total_seconds += value * 5 * 8 * 60 * 60 - - return total_seconds - - -if __name__ == '__main__': - text = """\ -2 hours -1 hour, 30 minutes -4 hours -7 hours -1 day, 1 hour -6 hours, 30 minutes -30 minutes -1 day -1 hour -4 hours, 30 minutes -40 minutes -7 hours, 30 minutes -1 day, 30 minutes -5 hours -5 hours, 30 minutes -1 minute -1 hour, 20 minutes -6 hours -3 hours, 30 minutes -15 minutes -3 hours -""" - - for line in list(set(text.splitlines())): - print(line, logged_human_time_to_seconds(line)) diff --git a/logger_example.py b/logger_example.py deleted file mode 100644 index 3f34cec4b..000000000 --- a/logger_example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_logger(name, file='log.txt', encoding='utf-8'): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - # Simple file handler - # fh = logging.FileHandler(file, encoding=encoding) - # or: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -log = get_logger(__file__) - - -if __name__ == '__main__': - log.debug('foo') - log.debug('bar') - log.debug(__file__) diff --git a/logging__examples/console_and_rotating_file_handler.py b/logging__examples/console_and_rotating_file_handler.py new file mode 100644 index 000000000..d074e0874 --- /dev/null +++ b/logging__examples/console_and_rotating_file_handler.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from logging.handlers import RotatingFileHandler + + +def get_logger(name, file="log.txt", encoding="utf-8"): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) + + # Simple file handler + # fh = logging.FileHandler(file, encoding=encoding) + # or: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +log = get_logger(__file__) + + +if __name__ == "__main__": + log.debug("foo") + log.debug("bar") + log.debug(__file__) diff --git a/logging__examples/log-config.yaml b/logging__examples/log-config.yaml new file mode 100644 index 000000000..b2e5fccfc --- /dev/null +++ b/logging__examples/log-config.yaml @@ -0,0 +1,33 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(name)-10s %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + main-file: &main-file + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + + main2-file: + <<: *main-file + filename: "main2.log" + +loggers: + main: + handlers: ["console", "main-file"] + level: "DEBUG" + + main2: + handlers: ["console", "main2-file"] + level: "INFO" diff --git a/logging__examples/logger_from_dict.py b/logging__examples/logger_from_dict.py new file mode 100644 index 000000000..bf1b9c108 --- /dev/null +++ b/logging__examples/logger_from_dict.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from logging import getLogger +from pathlib import Path +from typing import Any + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +LOGGING: dict[str, Any] = { + "version": 1, + "formatters": { + "default": { + "format": "[%(asctime)s] %(name)-10s %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s", + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "default", + }, + "main-file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "main.log", + "maxBytes": 10_000_000, + "backupCount": 5, + "encoding": "utf-8", + "formatter": "default", + }, + "main2-file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "main2.log", + "maxBytes": 10_000_000, + "backupCount": 5, + "encoding": "utf-8", + "formatter": "default", + }, + }, + "loggers": { + "main": { + "level": "DEBUG", + "handlers": [ + "console", + "main-file", + ] + }, + "main2": { + "level": "INFO", + "handlers": [ + "console", + "main2-file", + ] + }, + }, +} +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + + +logging.config.dictConfig(LOGGING) + +log = getLogger("main") +log.debug("DEBUG") +log.info("INFO") + +print() + +log = getLogger("main2") +log.debug("DEBUG") +log.info("INFO") diff --git a/logging__examples/logger_from_yaml.py b/logging__examples/logger_from_yaml.py new file mode 100644 index 000000000..5e9d1b926 --- /dev/null +++ b/logging__examples/logger_from_yaml.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from logging import getLogger +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + + +logging.config.dictConfig(LOGGING) + +log = getLogger("main") +log.debug("DEBUG") +log.info("INFO") + +print() + +log = getLogger("main2") +log.debug("DEBUG") +log.info("INFO") diff --git a/login_vk.com.py b/login_vk.com.py index f837fb718..cb667f76b 100644 --- a/login_vk.com.py +++ b/login_vk.com.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -18,37 +18,39 @@ QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) # Телефон или емейл -LOGIN = '' -PASSWORD = '' +LOGIN = "" +PASSWORD = "" -URL = 'https://vk.com' +URL = "https://vk.com" -if __name__ == '__main__': - app = QApplication(sys.argv) - view = QWebView() - view.show() +app = QApplication(sys.argv) - view.load(URL) +view = QWebView() +view.show() - loop = QEventLoop() - view.loadFinished.connect(loop.quit) - loop.exec_() +view.load(URL) - doc = view.page().mainFrame().documentElement() +loop = QEventLoop() +view.loadFinished.connect(loop.quit) +loop.exec_() - email = doc.findFirst('#quick_email') - password = doc.findFirst('#quick_pass') - login_button = doc.findFirst('#quick_login_button') +doc = view.page().mainFrame().documentElement() - if email.isNull() or password.isNull() or login_button.isNull(): - raise Exception('Ошибка при авторизации: не найдены поля емейла или пароля, или кнопка "Войти".') +email = doc.findFirst("#quick_email") +password = doc.findFirst("#quick_pass") +login_button = doc.findFirst("#quick_login_button") - # Заполняем поля емейла/телефона и пароля - email.setAttribute('value', LOGIN) - password.setAttribute('value', PASSWORD) +if email.isNull() or password.isNull() or login_button.isNull(): + raise Exception( + 'Ошибка при авторизации: не найдены поля емейла или пароля, или кнопка "Войти".' + ) - # Кликаем на кнопку "Войти" - login_button.evaluateJavaScript('this.click()') +# Заполняем поля емейла/телефона и пароля +email.setAttribute("value", LOGIN) +password.setAttribute("value", PASSWORD) - sys.exit(app.exec_()) +# Кликаем на кнопку "Войти" +login_button.evaluateJavaScript("this.click()") + +sys.exit(app.exec_()) diff --git a/magtu_ru/mgtu_get_students.py b/magtu_ru/mgtu_get_students.py index b2cd556ae..428327d7a 100644 --- a/magtu_ru/mgtu_get_students.py +++ b/magtu_ru/mgtu_get_students.py @@ -1,27 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department') + + +rs = requests.get( + "http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department" +) for dep in rs.json(): id_dep = list(dep.keys())[0] name = dep[id_dep] print(id_dep, name) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod=' + id_dep) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod={id_dep}" + ) for kaf in rs.json(): id_kaf = list(kaf.keys())[0] name = kaf[id_kaf] - print(' ' + id_kaf, name) + print(" " + id_kaf, name) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod=' + id_kaf) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod={id_kaf}" + ) for i, stud in enumerate(rs.json(), 1): name = stud[0] - print(' {}. {}'.format(i, name)) + print(f" {i}. {name}") print() diff --git a/magtu_ru/mgtu_get_students_gui.py b/magtu_ru/mgtu_get_students_gui.py index 4c9383c69..ee290cb13 100644 --- a/magtu_ru/mgtu_get_students_gui.py +++ b/magtu_ru/mgtu_get_students_gui.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests - try: from PyQt5.QtGui import * from PyQt5.QtWidgets import * @@ -19,7 +18,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw_dep = QListWidget() @@ -37,9 +36,11 @@ def __init__(self): self.setLayout(layout) - def fill(self): + def fill(self) -> None: self.lw_dep.clear() - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department') + rs = requests.get( + "http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department" + ) rs.raise_for_status() for dep in rs.json(): @@ -50,11 +51,13 @@ def fill(self): item.setData(Qt.UserRole, id_dep) self.lw_dep.addItem(item) - def fill_kaf(self, item_dep): + def fill_kaf(self, item_dep) -> None: self.lw_kaf.clear() id_dep = item_dep.data(Qt.UserRole) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod=' + id_dep) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod={id_dep}" + ) rs.raise_for_status() for kaf in rs.json(): @@ -65,11 +68,13 @@ def fill_kaf(self, item_dep): item.setData(Qt.UserRole, id_kaf) self.lw_kaf.addItem(item) - def fill_stu(self, item_kaf): + def fill_stu(self, item_kaf) -> None: self.lw_stu.clear() id_kaf = item_kaf.data(Qt.UserRole) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod=' + id_kaf) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod={id_kaf}" + ) rs.raise_for_status() for stud in rs.json(): diff --git a/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py b/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py index b104361d0..9440ad6e8 100644 --- a/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py +++ b/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py @@ -1,36 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -url = 'http://www.magtu.ru/student/bakalavriat-spetsialitet-magistratura/raspisanie-konsultatsij-prepodavatelej.html' +from urllib.parse import urljoin import requests -rs = requests.get(url) - from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'html.parser') -from urllib.parse import urljoin -for tag in root.select('[itemprop=articleBody] > *'): +url = "http://www.magtu.ru/student/bakalavriat-spetsialitet-magistratura/raspisanie-konsultatsij-prepodavatelej.html" +rs = requests.get(url) + +root = BeautifulSoup(rs.content, "html.parser") +for tag in root.select("[itemprop=articleBody] > *"): name = tag.name - if name == 'h3': + if name == "h3": print(tag.text.upper()) continue - elif name == 'p': + elif name == "p": print(tag.text) continue - elif name == 'ul': - for li in tag.select('li'): + elif name == "ul": + for li in tag.select("li"): if li.a: - print(' "{}": {}'.format(li.a.text, urljoin(rs.url, li.a['href']))) + print(f' "{li.a.text}": {urljoin(rs.url, li.a["href"])}') else: - print(' "{}"'.format(li.text)) + print(f' "{li.text}"') print() continue diff --git a/magtu_ru/selection_of_specialty.py b/magtu_ru/selection_of_specialty.py index fa4b69020..87273d5d1 100644 --- a/magtu_ru/selection_of_specialty.py +++ b/magtu_ru/selection_of_specialty.py @@ -1,54 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт получает список специальностей в формате HTML, разбирает таблицу и оформляет ее в JSON""" -if __name__ == '__main__': - url = 'http://magtu.ru/modules/mod_abiturient_helper/tmpl/get_spec.php' - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0', - 'X-Requested-With': 'XMLHttpRequest', - } +import json +import sys + +import requests +from bs4 import BeautifulSoup - # Русский язык и Математика - post_data = { - 'data': '01,02' - } - import requests - rs = requests.post(url, headers=headers, data=post_data) - if not rs.ok or not rs.text: - print('Post запрос не вернул данные таблицы. Возможно, не хватает каких-то данных.') - quit() +def element_to_text_list(el) -> str: + return ", ".join([child.strip() for child in el.strings]) - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.text, 'lxml') +url = "http://magtu.ru/modules/mod_abiturient_helper/tmpl/get_spec.php" +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0", + "X-Requested-With": "XMLHttpRequest", +} - def element_to_text_list(el): - return ', '.join([child.strip() for child in el.strings]) +# Русский язык и Математика +post_data = {"data": "01,02"} - table_rows = list() +rs = requests.post(url, headers=headers, data=post_data) +if not rs.ok or not rs.text: + print("Post запрос не вернул данные таблицы. Возможно, не хватает каких-то данных.") + sys.exit() - for tr in root.select('table tr')[1:]: - td_list = tr.select('td') +root = BeautifulSoup(rs.text, "lxml") +table_rows = [] - row_data = { - "number": element_to_text_list(td_list[0]), - "code": element_to_text_list(td_list[1]), - "speciality": element_to_text_list(td_list[2]), - "level_of_education": element_to_text_list(td_list[3]), - "budget_or_contract": element_to_text_list(td_list[4]), - "mode_of_study": element_to_text_list(td_list[5]), - "institute_faculty": element_to_text_list(td_list[6]), - "list_of_examinations": element_to_text_list(td_list[7]), - } - table_rows.append(row_data) +for tr in root.select("table tr")[1:]: + td_list = tr.select("td") + + row_data = { + "number": element_to_text_list(td_list[0]), + "code": element_to_text_list(td_list[1]), + "speciality": element_to_text_list(td_list[2]), + "level_of_education": element_to_text_list(td_list[3]), + "budget_or_contract": element_to_text_list(td_list[4]), + "mode_of_study": element_to_text_list(td_list[5]), + "institute_faculty": element_to_text_list(td_list[6]), + "list_of_examinations": element_to_text_list(td_list[7]), + } + table_rows.append(row_data) - import json - json_text = json.dumps(table_rows, indent=4, ensure_ascii=False, sort_keys=True) - print(json_text) +json_text = json.dumps(table_rows, indent=4, ensure_ascii=False, sort_keys=True) +print(json_text) diff --git a/manual_auth_to_github.py b/manual_auth_to_github.py index a88180693..dae2744e0 100644 --- a/manual_auth_to_github.py +++ b/manual_auth_to_github.py @@ -1,42 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для авторизации на github вручную""" -LOGIN = '' -PASSWORD = '' +import requests +from lxml import etree + + +LOGIN = "" +PASSWORD = "" -import requests session = requests.Session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0' +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0" -rs = session.get('https://github.com') +rs = session.get("https://github.com") print(rs) -rs = session.get('https://github.com/login') +rs = session.get("https://github.com/login") print(rs) -from lxml import etree root = etree.HTML(rs.content) input_name_by_value = dict() -for input_tag in root.xpath('//input'): +for input_tag in root.xpath("//input"): try: - input_name_by_value[input_tag.attrib['name']] = input_tag.attrib['value'] + input_name_by_value[input_tag.attrib["name"]] = input_tag.attrib["value"] except KeyError: pass -input_name_by_value['login'] = LOGIN -input_name_by_value['password'] = PASSWORD +input_name_by_value["login"] = LOGIN +input_name_by_value["password"] = PASSWORD -rs = session.post('https://github.com/session', data=input_name_by_value) +rs = session.post("https://github.com/session", data=input_name_by_value) print(rs) -rs = session.get('https://github.com/gil9red/search_in_users_github_gists') +rs = session.get("https://github.com/gil9red/search_in_users_github_gists") print(rs) print(rs.text) diff --git a/markdown__examples/common.py b/markdown__examples/common.py new file mode 100644 index 000000000..ef293bab7 --- /dev/null +++ b/markdown__examples/common.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +EXAMPLE_MARKDOWN = """\ +https://ru.wikipedia.org/wiki/Markdown + +-------- + +*выделение* (например, курсив) + +**сильное выделение** (например, полужирное начертание) + +-------- + +Пример кода внутри строки (inline) `Hello world!` + + + + + + + + + + + +-------- + +необходимо сделать ~~одну~~ другую вещь + +-------- + +* элемент маркированного списка +- ещё один элемент ненумерованного списка ++ буллеты элементов могут быть разными + +1. Элемент нумерованного списка +2. Элемент №2 того же списка +9. Элемент №3 списка — элементы нумеруются по порядку, цифра в начале строки не имеет значения + +-------- + +# Заголовок первого уровня +... +### Заголовок третьего уровня +... +###### Заголовок шестого уровня +... + +-------- + +> Данный текст будет заключен в HTML-теги
    + +-------- + +[Текст ссылки](http://example.com/ "Необязательный заголовок ссылки") + +Где-то среди текста встречается [текст ссылки][example]. + +Также ссылка повторяется [пример адреса][example]. + +Ссылка на [второй][foo] также [Bar][] ресурсы. + +[example]: http://example.com/ "Необязательный заголовок ссылки" +[foo]: http://example.net/ 'Необязательный заголовок ссылки' +[bar]: http://example.edu/ (Необязательный заголовок ссылки) + +-------- + +![Alt-текст](http://example.com/ "Заголовок изображения") +""" diff --git a/markdown__examples/hello_world.py b/markdown__examples/hello_world.py new file mode 100644 index 000000000..42fe7a160 --- /dev/null +++ b/markdown__examples/hello_world.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install markdown +import markdown + + +text_markdown = """ +*Hello* **[World](https://ru.wikipedia.org/wiki/Hello,_world!)**! +""".strip() +text_html = markdown.markdown(text_markdown) +print(text_html) +#

    Hello World!

    diff --git a/markdown__examples/pyqt5_gui.py b/markdown__examples/pyqt5_gui.py new file mode 100644 index 000000000..237e163bc --- /dev/null +++ b/markdown__examples/pyqt5_gui.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyqt5 +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QTextEdit, + QLabel, + QSplitter, + QVBoxLayout, + QTabWidget, +) + +# pip install markdown +import markdown + +from common import EXAMPLE_MARKDOWN + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(Path(__file__).stem) + + self.edit_markdown = QTextEdit() + self.edit_markdown.textChanged.connect(self._on_input_text_markdown) + + self.edit_result_qt = QTextEdit() + self.edit_result_qt.setReadOnly(True) + + self.edit_result_markdown = QTextEdit() + self.edit_result_markdown.setReadOnly(True) + + left_side_layout = QVBoxLayout() + left_side_layout.addWidget(QLabel("Markdown:")) + left_side_layout.addWidget(self.edit_markdown) + + left_side = QWidget() + left_side.setLayout(left_side_layout) + + tab_result = QTabWidget() + tab_result.addTab(self.edit_result_qt, "Result (Qt)") + tab_result.addTab(self.edit_result_markdown, "Result (markdown)") + + splitter = QSplitter() + splitter.addWidget(left_side) + splitter.addWidget(tab_result) + + main_layout = QVBoxLayout() + main_layout.addWidget(splitter) + + self.setLayout(main_layout) + + def _on_input_text_markdown(self) -> None: + self.edit_result_qt.setMarkdown(self.edit_markdown.toPlainText()) + + html = markdown.markdown(self.edit_markdown.toPlainText()) + self.edit_result_markdown.setHtml(html) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + mw.edit_markdown.setPlainText(EXAMPLE_MARKDOWN) + + app.exec() diff --git a/markdown__examples/pyqt6_gui.py b/markdown__examples/pyqt6_gui.py new file mode 100644 index 000000000..cb029aef6 --- /dev/null +++ b/markdown__examples/pyqt6_gui.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyqt6 +from PyQt6.QtWidgets import ( + QApplication, + QWidget, + QTextEdit, + QLabel, + QSplitter, + QVBoxLayout, + QTabWidget, +) + +# pip install markdown +import markdown + +from common import EXAMPLE_MARKDOWN + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(Path(__file__).stem) + + self.edit_markdown = QTextEdit() + self.edit_markdown.textChanged.connect(self._on_input_text_markdown) + + self.edit_result_qt = QTextEdit() + self.edit_result_qt.setReadOnly(True) + + self.edit_result_markdown = QTextEdit() + self.edit_result_markdown.setReadOnly(True) + + left_side_layout = QVBoxLayout() + left_side_layout.addWidget(QLabel("Markdown:")) + left_side_layout.addWidget(self.edit_markdown) + + left_side = QWidget() + left_side.setLayout(left_side_layout) + + tab_result = QTabWidget() + tab_result.addTab(self.edit_result_qt, "Result (Qt)") + tab_result.addTab(self.edit_result_markdown, "Result (markdown)") + + splitter = QSplitter() + splitter.addWidget(left_side) + splitter.addWidget(tab_result) + + main_layout = QVBoxLayout() + main_layout.addWidget(splitter) + + self.setLayout(main_layout) + + def _on_input_text_markdown(self) -> None: + self.edit_result_qt.setMarkdown(self.edit_markdown.toPlainText()) + + html = markdown.markdown(self.edit_markdown.toPlainText()) + self.edit_result_markdown.setHtml(html) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + mw.edit_markdown.setPlainText(EXAMPLE_MARKDOWN) + + app.exec() diff --git a/matplotlib__examples/animate.py b/matplotlib__examples/animate.py index 73430f1aa..6bfe34666 100644 --- a/matplotlib__examples/animate.py +++ b/matplotlib__examples/animate.py @@ -1,31 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import psutil -from datetime import datetime as dt from collections import deque +from datetime import datetime as dt + +import matplotlib.animation as animation import matplotlib.pyplot as plt from matplotlib.dates import date2num -import matplotlib.animation as animation + +import psutil + N = 600 x = deque([date2num(dt.now())], maxlen=N) y = deque([0], maxlen=N) fig, ax = plt.subplots(figsize=(8, 3)) -line, = ax.plot_date(x, y, marker="") +(line,) = ax.plot_date(x, y, marker="") -ax.spines['left'].set_visible(False) -ax.spines['bottom'].set_visible(False) -ax.spines['top'].set_visible(False) -ax.spines['right'].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["bottom"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["right"].set_visible(False) def get_data(): - return psutil.cpu_percent(.15) + return psutil.cpu_percent(0.15) def animate(i): @@ -34,8 +37,8 @@ def animate(i): ax.relim(visible_only=True) ax.autoscale_view(True) line.set_data(x, y) - ax.fill_between(x, -0.5, y, color='lightgrey') - return line, + ax.fill_between(x, -0.5, y, color="lightgrey") + return (line,) ani = animation.FuncAnimation(fig, animate, interval=300) diff --git a/matplotlib__examples/examples/exm1.py b/matplotlib__examples/examples/exm1.py index 59655c5ea..fb02671fd 100644 --- a/matplotlib__examples/examples/exm1.py +++ b/matplotlib__examples/examples/exm1.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Следующий пример строит график функции f(x) = x / sin(x): @@ -11,7 +11,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Будем рисовать график этой функции def func(x): """ @@ -19,7 +19,7 @@ def func(x): """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -29,13 +29,13 @@ def func(x): dx = 0.01 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm10.py b/matplotlib__examples/examples/exm10.py index b3b3df635..c3650f05e 100644 --- a/matplotlib__examples/examples/exm10.py +++ b/matplotlib__examples/examples/exm10.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Все классы для работы с трехмерными графиками находятся в пакете mpl_toolkits.mplot3d, @@ -14,8 +14,8 @@ import pylab from mpl_toolkits.mplot3d import Axes3D -if __name__ == '__main__': +if __name__ == "__main__": fig = pylab.figure() Axes3D(fig) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm11.py b/matplotlib__examples/examples/exm11.py index 8ed3ec829..e57d4b340 100644 --- a/matplotlib__examples/examples/exm11.py +++ b/matplotlib__examples/examples/exm11.py @@ -1,23 +1,23 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" +import numpy import pylab from mpl_toolkits.mplot3d import Axes3D -import numpy # Эта функция возвращает три двумерные матрицы: x, y, z. # Координаты x и y лежат в интервале от -10 до 10 с шагом 0.1. -def makeData (): - x = numpy.arange (-10, 10, 0.1) - y = numpy.arange (-10, 10, 0.1) +def makeData(): + x = numpy.arange(-10, 10, 0.1) + y = numpy.arange(-10, 10, 0.1) xgrid, ygrid = numpy.meshgrid(x, y) - zgrid = numpy.sin (xgrid) * numpy.sin (ygrid) / (xgrid * ygrid) + zgrid = numpy.sin(xgrid) * numpy.sin(ygrid) / (xgrid * ygrid) return xgrid, ygrid, zgrid -if __name__ == '__main__': +if __name__ == "__main__": x, y, z = makeData() # Чтобы отобразить наши данные, достаточно вызвать метод plot_surface() @@ -31,4 +31,4 @@ def makeData (): # axes.plot_surface(x, y, z, rstride=5, cstride=5) # Установка шага сетки (при 5, очень мелкая сетка) # axes.plot_surface(x, y, z, rstride=20, cstride=20) # Установка шага сетки (при 20, крупная сетка) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm12.py b/matplotlib__examples/examples/exm12.py index c93d45a38..b16136963 100644 --- a/matplotlib__examples/examples/exm12.py +++ b/matplotlib__examples/examples/exm12.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Цветовые карты используются, если нужно указать в какие цвета должны окрашиваться @@ -11,23 +11,25 @@ # чтобы создать градиент перехода от синего цвета к красному через белый. +import numpy import pylab + from mpl_toolkits.mplot3d import Axes3D from matplotlib__examples.colors import LinearSegmentedColormap -import numpy + # Эта функция возвращает три двумерные матрицы: x, y, z. # Координаты x и y лежат в интервале от -10 до 10 с шагом 0.1. -def makeData (): - x = numpy.arange (-10, 10, 0.1) - y = numpy.arange (-10, 10, 0.1) +def makeData(): + x = numpy.arange(-10, 10, 0.1) + y = numpy.arange(-10, 10, 0.1) xgrid, ygrid = numpy.meshgrid(x, y) - zgrid = numpy.sin (xgrid) * numpy.sin (ygrid) / (xgrid * ygrid) + zgrid = numpy.sin(xgrid) * numpy.sin(ygrid) / (xgrid * ygrid) return xgrid, ygrid, zgrid -if __name__ == '__main__': +if __name__ == "__main__": x, y, z = makeData() # Чтобы отобразить наши данные, достаточно вызвать метод plot_surface() @@ -54,7 +56,7 @@ def makeData (): # color_map = cm.cmap_d["jet"] # axes.plot_surface(x, y, z, rstride=4, cstride=4, cmap=color_map) - color_map = LinearSegmentedColormap.from_list("red_blue", ['b', 'w', 'r'], 256) + color_map = LinearSegmentedColormap.from_list("red_blue", ["b", "w", "r"], 256) axes.plot_surface(x, y, z, rstride=3, cstride=3, cmap=color_map) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm13.py b/matplotlib__examples/examples/exm13.py index 2d410e818..50768bc89 100644 --- a/matplotlib__examples/examples/exm13.py +++ b/matplotlib__examples/examples/exm13.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -9,6 +9,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции def func(x): """ @@ -19,7 +20,7 @@ def func(x): return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -28,10 +29,10 @@ def func(x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график с использованием стиля pylab.plot(xlist, ylist, "x") @@ -50,4 +51,4 @@ def func(x): # pylab.plot(xlist, ylist, "-*k") # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm14.py b/matplotlib__examples/examples/exm14.py index 25fe00d23..ad26cf7a4 100644 --- a/matplotlib__examples/examples/exm14.py +++ b/matplotlib__examples/examples/exm14.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -9,6 +9,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции def func(x): """ @@ -19,7 +20,7 @@ def func(x): return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -28,10 +29,10 @@ def func(x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # # !!! Нарисуем одномерный график с использованием стиля # pylab.plot(xlist, ylist, "x") @@ -58,8 +59,9 @@ def func(x): # pylab.plot (xlist, ylist, linestyle = "-", marker = "*", color = "k") # Результат будет выглядеть точно также. А, например, следующий код - pylab.plot(xlist, ylist, linestyle="-", marker="o", color="k", - markerfacecolor="#ff22aa") + pylab.plot( + xlist, ylist, linestyle="-", marker="o", color="k", markerfacecolor="#ff22aa" + ) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm15.py b/matplotlib__examples/examples/exm15.py index 3479aff2b..7f45e86fc 100644 --- a/matplotlib__examples/examples/exm15.py +++ b/matplotlib__examples/examples/exm15.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как отобразить легенду @@ -14,17 +14,18 @@ # Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -33,12 +34,11 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] - + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] ## Первый способ # В качестве параметра функции legend() нужно передать список или кортеж, @@ -49,16 +49,15 @@ def func (x): # виде легенды. # Нарисуем два одномерных графика - pylab.plot (xlist, ylist1, "b-") - pylab.plot (xlist, ylist2, "g--") + pylab.plot(xlist, ylist1, "b-") + pylab.plot(xlist, ylist2, "g--") # !!! Добавим легенду. # !!! Первому графику будет соответствовать надпись "f(x)", # !!! А второму - "f(0.2 * x)" - pylab.legend ( ("f(x)", "f(0.2 * x)") ) + pylab.legend(("f(x)", "f(0.2 * x)")) ## Первый способ - ## Второй способ # Того же самого результата мы можем добиться, если при рисовании графиков # будем использовать дополнительный параметр label, а затем вызовем функцию @@ -73,7 +72,6 @@ def func (x): # метка. ## Второй способ - ## Заголовок легенды # В легенду можно добавить заголовок, для этого в функцию legend() надо # передать дополнительный строковый параметр title со строкой заголовка. @@ -81,6 +79,5 @@ def func (x): # pylab.legend (title = "Sinc") ## Заголовок легенды - # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm16.py b/matplotlib__examples/examples/exm16.py index 884b653d6..896a804b7 100644 --- a/matplotlib__examples/examples/exm16.py +++ b/matplotlib__examples/examples/exm16.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как отображать формулы в нотации TeX @@ -20,17 +20,18 @@ # Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -39,18 +40,18 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] # !!! Графики имеют метки с формулами в формате TeX - pylab.plot (xlist, ylist1, "b-", label = "$f(x)$") - pylab.plot (xlist, ylist2, "g--", label = "$f(x \cdot 0.2)$") + pylab.plot(xlist, ylist1, "b-", label="$f(x)$") + pylab.plot(xlist, ylist2, "g--", label="$f(x \cdot 0.2)$") # !!! Добавим легенду с заголовком в виде формулы - pylab.legend (title = r"$f(x) = \frac{sin(x)}{x}$") + pylab.legend(title=r"$f(x) = \frac{sin(x)}{x}$") # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm17.py b/matplotlib__examples/examples/exm17.py index a31cf5c3e..9d943598b 100644 --- a/matplotlib__examples/examples/exm17.py +++ b/matplotlib__examples/examples/exm17.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как нарисовать несколько графиков в одном окне @@ -26,16 +26,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -44,40 +44,40 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Две строки, три столбца. # !!! Текущая ячейка - 1 - pylab.subplot (2, 3, 1) - pylab.plot (xlist, ylist) - pylab.title ("1") + pylab.subplot(2, 3, 1) + pylab.plot(xlist, ylist) + pylab.title("1") # !!! Две строки, три столбца. # !!! Текущая ячейка - 2 - pylab.subplot (2, 3, 2) - pylab.plot (xlist, ylist) - pylab.title ("2") + pylab.subplot(2, 3, 2) + pylab.plot(xlist, ylist) + pylab.title("2") # !!! Две строки, три столбца. # !!! Текущая ячейка - 4 - pylab.subplot (2, 3, 4) - pylab.plot (xlist, ylist) - pylab.title ("4") + pylab.subplot(2, 3, 4) + pylab.plot(xlist, ylist) + pylab.title("4") # !!! Две строки, три столбца. # !!! Текущая ячейка - 5 - pylab.subplot (2, 3, 5) - pylab.plot (xlist, ylist) - pylab.title ("5") + pylab.subplot(2, 3, 5) + pylab.plot(xlist, ylist) + pylab.title("5") # !!! Одна строка, три столбца. # !!! Текущая ячейка - 3 - pylab.subplot (1, 3, 3) - pylab.plot (xlist, ylist) - pylab.title ("3") + pylab.subplot(1, 3, 3) + pylab.plot(xlist, ylist) + pylab.title("3") # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm18.py b/matplotlib__examples/examples/exm18.py index a98cd407e..7951b3c31 100644 --- a/matplotlib__examples/examples/exm18.py +++ b/matplotlib__examples/examples/exm18.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Более гибкий способ расположения графиков с @@ -22,33 +22,34 @@ # Импортируем один из пакетов Matplotlib import pylab -if __name__ == '__main__': + +if __name__ == "__main__": # Таблица графиков будет иметь три строки и три столбца (3,3) # Вывод будет осуществляться в ячейку с координатами (0, 0), # то есть 0-ая строка и 0-ой столбец # Оси для графика будут занимать две ячейки по горизонтали (colspan = 2) # и две ячейки по вертикали (rowspan = 2) - pylab.subplot2grid ((3,3), (0, 0), colspan = 2, rowspan = 2) - pylab.title ("Graph1") + pylab.subplot2grid((3, 3), (0, 0), colspan=2, rowspan=2) + pylab.title("Graph1") # Вывод будет осуществляться в ячейку с координатами (0, 2), # то есть 0-ая строка и 2-ой столбец (нумерация начинается с нуля) # Оси для графика будут занимать две ячейки по вертикали (rowspan = 2) - pylab.subplot2grid ((3,3), (0, 2), rowspan = 2) - pylab.title ("Graph2") + pylab.subplot2grid((3, 3), (0, 2), rowspan=2) + pylab.title("Graph2") # Вывод будет осуществляться в ячейку с координатами (2, 0), # то есть 2-ая строка и 0-ой столбец # Оси для графика будут занимать одну ячейку по вертикали и горизонтали # Аналог этого вызова - pylab.subplot (3, 3, 7) - pylab.subplot2grid ((3,3), (2, 0)) - pylab.title ("Graph3") + pylab.subplot2grid((3, 3), (2, 0)) + pylab.title("Graph3") # Вывод будет осуществляться в ячейку с координатами (2, 1), # то есть 2-ая строка и 1-ый столбец # Оси для графика будут занимать две ячейки по горизонтали (colspan = 2) - pylab.subplot2grid ((3,3), (2, 1), colspan = 2) - pylab.title ("Graph4") + pylab.subplot2grid((3, 3), (2, 1), colspan=2) + pylab.title("Graph4") - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm19.py b/matplotlib__examples/examples/exm19.py index 934118994..a235e2ab9 100644 --- a/matplotlib__examples/examples/exm19.py +++ b/matplotlib__examples/examples/exm19.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Использование класса GridSpec для расположения @@ -20,18 +20,19 @@ import math -import pylab import matplotlib__examples +import pylab + -def plotGraph (): +def plotGraph() -> None: # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -41,76 +42,76 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = matplotlib__examples.mlab.frange (xmin, xmax, dx) + xlist = matplotlib__examples.mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) -if __name__ == '__main__': +if __name__ == "__main__": # создаем таблицу (сетку) размером 4 x 4 ячеек grid = matplotlib__examples.gridspec.GridSpec(4, 4) # Одномерное индексирование. # 0-ая ячейка, начиная с левого верхнего угла # График занимает одну ячейку - pylab.subplot (grid[0]) - pylab.title ("Graph 1") + pylab.subplot(grid[0]) + pylab.title("Graph 1") plotGraph() # Одномерное индексирование. # 1-ая ячейка, начиная с левого верхнего угла (0-ая строка, 1-ый столбец) # График занимает одну ячейку - pylab.subplot (grid[1]) - pylab.title ("Graph 2") + pylab.subplot(grid[1]) + pylab.title("Graph 2") plotGraph() # Двумерное индексирование. # 1-ая строка, 0-ой столбец # График занимает одну ячейку - pylab.subplot (grid [1, 0]) - pylab.title ("Graph 3") + pylab.subplot(grid[1, 0]) + pylab.title("Graph 3") plotGraph() # Двумерное индексирование. # 0-ая строка. График занимает столбцы от 2 и до конца строки (2:) # График занимает одну строку и два столбца (2-й и 3-й) - pylab.subplot (grid [0, 2:]) - pylab.title ("Graph 4") + pylab.subplot(grid[0, 2:]) + pylab.title("Graph 4") plotGraph() # Двумерное индексирование. # График занимает строки, начиная с 1 и до 2 включительно (1: 3) # График занимает столбцы от 1 и до предпоследней ячейки включительно (1: -1) # График занимает две строки и два столбца - pylab.subplot (grid [1: 3, 1: -1]) - pylab.title ("Graph 5") + pylab.subplot(grid[1:3, 1:-1]) + pylab.title("Graph 5") plotGraph() # Двумерное индексирование. # График занимает строки, начиная со 2 и до конца всех строк (столбца) (2: ) # График занимает один столбец по горизонтали # График занимает две строки и один столбец - pylab.subplot (grid [2:, 0]) - pylab.title ("Graph 6") + pylab.subplot(grid[2:, 0]) + pylab.title("Graph 6") plotGraph() # Двумерное индексирование. # График занимает строки, начиная с 1 и до последней строки включительно (1:) # График занимает один последний столбец (-1) # График занимает две строки и один столбец - pylab.subplot (grid [1:, -1]) - pylab.title ("Graph 7") + pylab.subplot(grid[1:, -1]) + pylab.title("Graph 7") plotGraph() # Двумерное индексирование. # График занимает одну последнюю строку (-1) # График занимает два столбца: 1 и 2 (1: 3) # График занимает одну строку и два столбца - pylab.subplot (grid [-1, 1: 3]) - pylab.title ("Graph 8") + pylab.subplot(grid[-1, 1:3]) + pylab.title("Graph 8") plotGraph() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm2.py b/matplotlib__examples/examples/exm2.py index d390fdd40..dd41235f7 100644 --- a/matplotlib__examples/examples/exm2.py +++ b/matplotlib__examples/examples/exm2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Если вызывать функцию plot() несколько раз подряд, то на график @@ -12,15 +12,15 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -30,15 +30,15 @@ def func (x): dx = 0.01 # !!! Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] # !!! Нарисуем одномерные графики - pylab.plot (xlist, ylist1) - pylab.plot (xlist, ylist2) + pylab.plot(xlist, ylist1) + pylab.plot(xlist, ylist2) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm20.py b/matplotlib__examples/examples/exm20.py index 789d67f36..3905c58e2 100644 --- a/matplotlib__examples/examples/exm20.py +++ b/matplotlib__examples/examples/exm20.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как изменять размеры ячеек таблицы при @@ -14,18 +14,19 @@ import math import pylab + import matplotlib__examples -def plotGraph (): +def plotGraph() -> None: # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -35,15 +36,15 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = matplotlib__examples.mlab.frange (xmin, xmax, dx) + xlist = matplotlib__examples.mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) -if __name__ == '__main__': +if __name__ == "__main__": # Создаем таблицу (сетку) с тремя строками и двумя столбцами rows = 3 cols = 2 @@ -54,13 +55,12 @@ def func (x): # height_ratios - список соотношений высот ячеек # В данном случае высота второй строки будет в 2 раза больше первого, # а высота третьей строки будет в 3 раза больше первой (в 1.5 раза больше второй) - grid = matplotlib__examples.gridspec.GridSpec(rows, - cols, - width_ratios=[1, 1.5], - height_ratios=[1, 2, 3]) + grid = matplotlib__examples.gridspec.GridSpec( + rows, cols, width_ratios=[1, 1.5], height_ratios=[1, 2, 3] + ) - for n in range (rows * cols): - pylab.subplot (grid[n]) + for n in range(rows * cols): + pylab.subplot(grid[n]) plotGraph() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm21.py b/matplotlib__examples/examples/exm21.py index 864596c1f..a8cef33cc 100644 --- a/matplotlib__examples/examples/exm21.py +++ b/matplotlib__examples/examples/exm21.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как рисовать графики в разных окнах @@ -25,16 +25,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -43,26 +43,26 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] - ylist3 = [func (x * 2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] + ylist3 = [func(x * 2) for x in xlist] # !!! Нарисуем график в первом окне - pylab.figure (1) - pylab.plot (xlist, ylist1, label = "f(x)") + pylab.figure(1) + pylab.plot(xlist, ylist1, label="f(x)") # !!! Нарисуем график во втором окне pylab.figure(2) - pylab.plot (xlist, ylist2, label = "f(x * 0.2)") + pylab.plot(xlist, ylist2, label="f(x * 0.2)") pylab.legend() # !!! Нарисуем еще один график в первом окне - pylab.figure (1) - pylab.plot (xlist, ylist3, label = "f(x * 2)") + pylab.figure(1) + pylab.plot(xlist, ylist3, label="f(x * 2)") pylab.legend() # Покажем окна с нарисованными графиками - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm22.py b/matplotlib__examples/examples/exm22.py index 3963a27aa..6c2af83ff 100644 --- a/matplotlib__examples/examples/exm22.py +++ b/matplotlib__examples/examples/exm22.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как делать анимированные графики @@ -24,7 +24,7 @@ from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -33,25 +33,25 @@ dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # !!! Включаем интерактивный режим pylab.ion() # У нас будет 50 кадров - for n in range (50): + for n in range(50): # Данные для очередного кадра - ylist = [math.sin (x + n / 2.0) for x in xlist] + ylist = [math.sin(x + n / 2.0) for x in xlist] # !!! Очистим график pylab.clf() # Выведем новые данные - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! Нарисуем их # !!! Обратите внимание, что здесь используется функция draw(), а не show() pylab.draw() # !!! Закроем окно, иначе при завершении программы получим ошибку - pylab.close() \ No newline at end of file + pylab.close() diff --git a/matplotlib__examples/examples/exm23_1.py b/matplotlib__examples/examples/exm23_1.py index 4f4844aaf..9f452103f 100644 --- a/matplotlib__examples/examples/exm23_1.py +++ b/matplotlib__examples/examples/exm23_1.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как изменять формат меток на осях @@ -19,13 +19,13 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_2.py b/matplotlib__examples/examples/exm23_2.py index f44344052..c4118d280 100644 --- a/matplotlib__examples/examples/exm23_2.py +++ b/matplotlib__examples/examples/exm23_2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Теперь рассмотрим, как можно менять форматирование с помощью метода @@ -21,19 +21,19 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер formatter = matplotlib__examples.ticker.NullFormatter() # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_3.py b/matplotlib__examples/examples/exm23_3.py index d4f21431c..4b6da443b 100644 --- a/matplotlib__examples/examples/exm23_3.py +++ b/matplotlib__examples/examples/exm23_3.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FormatStrFormatter @@ -27,20 +27,20 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер - formatter = matplotlib__examples.ticker.FormatStrFormatter ("%.3f") + formatter = matplotlib__examples.ticker.FormatStrFormatter("%.3f") # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_4.py b/matplotlib__examples/examples/exm23_4.py index a0f200db6..034afcaa3 100644 --- a/matplotlib__examples/examples/exm23_4.py +++ b/matplotlib__examples/examples/exm23_4.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FuncFormatter @@ -28,31 +28,31 @@ import matplotlib__examples.ticker -def funcForFormatter (x, pos): +def funcForFormatter(x, pos) -> str: if x < 0: - return "минус {x}".format (x=abs(x)) + return f"минус {abs(x)}" if x > 0: - return "плюс {x}".format (x=x) + return f"плюс {x}" return "0" if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер formatter = matplotlib__examples.ticker.FuncFormatter(funcForFormatter) # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_5.py b/matplotlib__examples/examples/exm23_5.py index ec5e58528..b056caea7 100644 --- a/matplotlib__examples/examples/exm23_5.py +++ b/matplotlib__examples/examples/exm23_5.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## ScalarFormatter @@ -14,17 +14,16 @@ import numpy import pylab -import matplotlib__examples.ticker if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) * 1e5 + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) * 1e5 figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_6.py b/matplotlib__examples/examples/exm23_6.py index 6e70a96c3..82a30583e 100644 --- a/matplotlib__examples/examples/exm23_6.py +++ b/matplotlib__examples/examples/exm23_6.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FixedFormatter @@ -22,20 +22,22 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер - formatter = matplotlib__examples.ticker.FixedFormatter([u"Раз", u"Два", u"Три", u"Четыре", u"Пять"]) + formatter = matplotlib__examples.ticker.FixedFormatter( + ["Раз", "Два", "Три", "Четыре", "Пять"] + ) # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm24.py b/matplotlib__examples/examples/exm24.py index af033b352..91b453301 100644 --- a/matplotlib__examples/examples/exm24.py +++ b/matplotlib__examples/examples/exm24.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как изменять интервал осей @@ -20,16 +20,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -50.0 xmax = 50.0 @@ -38,16 +38,16 @@ def func (x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график с использованием стиля - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! На графике будет показан только участок от -10 до 30 по оси X - pylab.xlim (-10, 30) + pylab.xlim(-10, 30) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm3.py b/matplotlib__examples/examples/exm3.py index 02636ce3a..e0c25af85 100644 --- a/matplotlib__examples/examples/exm3.py +++ b/matplotlib__examples/examples/exm3.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Простой пример рисования графика import pylab -if __name__ == '__main__': - pylab.plot(range(1, 20), [i * i for i in range(1, 20)], 'ro') +if __name__ == "__main__": + pylab.plot(range(1, 20), [i * i for i in range(1, 20)], "ro") # pylab.savefig('example.png') - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm4.py b/matplotlib__examples/examples/exm4.py index ee5e37a51..54ecf0a23 100644 --- a/matplotlib__examples/examples/exm4.py +++ b/matplotlib__examples/examples/exm4.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для рисования кругового графика используется функция pie() из модуля pylab. @@ -8,11 +8,11 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] # Нарисовать круговой график - pylab.pie (data) + pylab.pie(data) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm5.py b/matplotlib__examples/examples/exm5.py index fbff62293..401d4771d 100644 --- a/matplotlib__examples/examples/exm5.py +++ b/matplotlib__examples/examples/exm5.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # По умолчанию создается окно, у которого ширина и высота различны, из-за чего "пирог" @@ -11,17 +11,17 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.0, 0.0, 1.0, 1.0]) # И снова нарисуем график - pylab.pie (data) + pylab.pie(data) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm6.py b/matplotlib__examples/examples/exm6.py index 5eadccc24..1ba8f4ca6 100644 --- a/matplotlib__examples/examples/exm6.py +++ b/matplotlib__examples/examples/exm6.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для того, чтобы добавить текстовые метки к секторам, достаточно передать их список @@ -6,7 +6,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -14,12 +14,12 @@ labels = ["data1", "data2", "data3", "data4", "data5"] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.05, 0.05, 0.9, 0.9]) # И снова нарисуем график - pylab.pie (data, labels=labels) + pylab.pie(data, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm7.py b/matplotlib__examples/examples/exm7.py index cc4aeb8b9..fe12001ad 100644 --- a/matplotlib__examples/examples/exm7.py +++ b/matplotlib__examples/examples/exm7.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Цвета секторов задаются с помощью параметра colors функции pie(). import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -16,12 +16,12 @@ colors = ["g", "r", "#FF00BB", "0.5", "y"] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.05, 0.05, 0.9, 0.9]) # И снова нарисуем график - pylab.pie (data, colors=colors, labels=labels) + pylab.pie(data, colors=colors, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm8.py b/matplotlib__examples/examples/exm8.py index 28672dd31..0ca842f93 100644 --- a/matplotlib__examples/examples/exm8.py +++ b/matplotlib__examples/examples/exm8.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Часто при отображении круговых графиков сектора немного "выдвигают" из центра. @@ -7,7 +7,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -21,12 +21,12 @@ explode = [0.02, 0.05, 0.08, 0.08, 0.05] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.1, 0.1, 0.8, 0.8]) # И снова нарисуем график - pylab.pie (data, explode=explode, colors=colors, labels=labels) + pylab.pie(data, explode=explode, colors=colors, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm9.py b/matplotlib__examples/examples/exm9.py index 10f0e373c..b682dfa88 100644 --- a/matplotlib__examples/examples/exm9.py +++ b/matplotlib__examples/examples/exm9.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Из эстетических соображений к графику можно добавить тень. Для этого в функцию pie() @@ -7,7 +7,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -20,12 +20,12 @@ explode = [0.02, 0.05, 0.08, 0.08, 0.05] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.1, 0.1, 0.8, 0.8]) # И снова нарисуем график - pylab.pie (data, explode=explode, colors=colors, labels=labels, shadow=True) + pylab.pie(data, explode=explode, colors=colors, labels=labels, shadow=True) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/price_by_date__plot.py b/matplotlib__examples/price_by_date__plot.py index b5fb68c51..7bd7ad0ab 100644 --- a/matplotlib__examples/price_by_date__plot.py +++ b/matplotlib__examples/price_by_date__plot.py @@ -1,34 +1,70 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -if __name__ == '__main__': - price_by_date = {156802: '2017-04-30', 147719: '2017-04-02', 120172: '2017-01-27', 123787: '2017-02-01', - 127890: '2017-02-11', 131699: '2017-02-16', 146452: '2017-03-27', 148507: '2017-04-09', - 144858: '2017-03-18', 144209: '2017-03-16', 126114: '2017-02-06', 102947: '2017-01-16', - 154404: '2017-04-22', 141733: '2017-03-11', 123249: '2017-01-29', 148008: '2017-04-07', - 112297: '2017-01-19', 120371: '2017-01-28', 119923: '2017-01-25', 127541: '2017-02-10', - 127163: '2017-02-07', 142911: '2017-03-14', 151106: '2017-04-10', 147020: '2017-03-29', - 123598: '2017-01-30', 136784: '2017-03-05', 134737: '2017-02-21', 135507: '2017-02-19', - 135636: '2017-02-20', 136535: '2017-02-27', 123866: '2017-02-04', 143710: '2017-03-15', - 142562: '2017-03-13', 138833: '2017-03-10', 153705: '2017-04-20', 137834: '2017-03-06', - 116076: '2017-01-22', 90386: '2017-01-15', 128239: '2017-02-14', 145136: '2017-03-20', - 146033: '2017-03-22', 124115: '2017-02-05', 135158: '2017-02-17', 145784: '2017-03-21', - 145535: '2017-03-19'} - - # Данные для построения графика - x = list(sorted(price_by_date.keys())) - - from datetime import datetime - get_date = lambda date_str: datetime.strptime(date_str, '%Y-%m-%d') - y = list(get_date(price_by_date[key]) for key in sorted(price_by_date.keys())) - - import pylab - pylab.plot(x, y) - - pylab.xlabel('Price') - pylab.ylabel('Date') - pylab.grid() - pylab.show() +__author__ = "ipetrash" + + +from datetime import datetime +import pylab + + +price_by_date = { + 156802: "2017-04-30", + 147719: "2017-04-02", + 120172: "2017-01-27", + 123787: "2017-02-01", + 127890: "2017-02-11", + 131699: "2017-02-16", + 146452: "2017-03-27", + 148507: "2017-04-09", + 144858: "2017-03-18", + 144209: "2017-03-16", + 126114: "2017-02-06", + 102947: "2017-01-16", + 154404: "2017-04-22", + 141733: "2017-03-11", + 123249: "2017-01-29", + 148008: "2017-04-07", + 112297: "2017-01-19", + 120371: "2017-01-28", + 119923: "2017-01-25", + 127541: "2017-02-10", + 127163: "2017-02-07", + 142911: "2017-03-14", + 151106: "2017-04-10", + 147020: "2017-03-29", + 123598: "2017-01-30", + 136784: "2017-03-05", + 134737: "2017-02-21", + 135507: "2017-02-19", + 135636: "2017-02-20", + 136535: "2017-02-27", + 123866: "2017-02-04", + 143710: "2017-03-15", + 142562: "2017-03-13", + 138833: "2017-03-10", + 153705: "2017-04-20", + 137834: "2017-03-06", + 116076: "2017-01-22", + 90386: "2017-01-15", + 128239: "2017-02-14", + 145136: "2017-03-20", + 146033: "2017-03-22", + 124115: "2017-02-05", + 135158: "2017-02-17", + 145784: "2017-03-21", + 145535: "2017-03-19", +} + +# Данные для построения графика +x = list(sorted(price_by_date.keys())) + +get_date = lambda date_str: datetime.strptime(date_str, "%Y-%m-%d") +y = list(get_date(price_by_date[key]) for key in sorted(price_by_date.keys())) + +pylab.plot(x, y) + +pylab.xlabel("Price") +pylab.ylabel("Date") +pylab.grid() +pylab.show() diff --git a/max_seqs_zeros__itertools.groupby.py b/max_seqs_zeros__itertools.groupby.py index 25022be7f..9ffd7dcf7 100644 --- a/max_seqs_zeros__itertools.groupby.py +++ b/max_seqs_zeros__itertools.groupby.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import itertools @@ -11,13 +11,13 @@ def end_zeros(num: int) -> int: size = 0 for x, items in itertools.groupby(str(num)): - if x == '0': + if x == "0": size = max(size, len(list(items))) return size -if __name__ == '__main__': +if __name__ == "__main__": assert end_zeros(10979000000) == 6 assert end_zeros(1000000) == 6 assert end_zeros(10009) == 3 diff --git a/mechanize__examples/follow_link.py b/mechanize__examples/follow_link.py new file mode 100644 index 000000000..0b9c12732 --- /dev/null +++ b/mechanize__examples/follow_link.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install mechanize +import mechanize + + +br = mechanize.Browser() +br.open("https://docs.python.org/3/") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") +""" +Title: 3.11.1 Documentation +URL: https://docs.python.org/3/ +""" + +print() + +br.follow_link(text_regex=r"Tutorial") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") +""" +Title: The Python Tutorial — Python 3.11.1 documentation +URL: https://docs.python.org/3/tutorial/index.html +""" diff --git a/mechanize__examples/submit_form.py b/mechanize__examples/submit_form.py new file mode 100644 index 000000000..32156515f --- /dev/null +++ b/mechanize__examples/submit_form.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json + +# pip install mechanize +import mechanize + + +br = mechanize.Browser() +br.open("https://httpbin.org/forms/post") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") + +rs = br.response() + +print("Headers:") +print(rs.info()) +print() + +print("Response body:") +print(rs.read()) + +print("\n" + "-" * 100 + "\n") + +br.select_form(nr=0) # First form +br["custname"] = "Customer" +br["custtel"] = "+79990001122" +br["custemail"] = "my@example.com" +br["size"] = ["medium"] # radio +br["topping"] = ["bacon", "cheese"] # checkbox +br["delivery"] = "11:15" +br["comments"] = "My comment!" + +rs2 = br.submit() + +print("Headers:") +print(rs2.info()) +print() + +rs2_body = rs2.read() +print("Response body:") +print(rs2_body) + +rs2_json = json.loads(rs2_body) +print(rs2_json["form"]) +# {'comments': 'My comment!', 'custemail': 'my@example.com', 'custname': 'Customer', 'custtel': '+79990001122', +# 'delivery': '11:15', 'size': 'medium', 'topping': ['bacon', 'cheese']} diff --git a/mediawiki__wikipedia.py b/mediawiki__wikipedia.py index 7cef42c82..2f3d25329 100644 --- a/mediawiki__wikipedia.py +++ b/mediawiki__wikipedia.py @@ -1,38 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from urllib.parse import unquote + # pip install pymediawiki # https://github.com/barrust/mediawiki # http://pymediawiki.readthedocs.io/en/latest/index.html +from mediawiki import MediaWiki -def wiki_search(query: str, lang='ru', unquote_percent_encoded=False) -> str: +def wiki_search(query: str, lang="ru", unquote_percent_encoded=False) -> str: # Default using wikipedia - from mediawiki import MediaWiki wikipedia = MediaWiki(lang=lang) result = wikipedia.opensearch(query, results=1) if not result: - return '' + return "" _, text, url = result[0] if unquote_percent_encoded: - from urllib.parse import unquote url = unquote(url) - return '{} ({})'.format(text, url) + return f"{text} ({url})" -if __name__ == '__main__': - query = 'GitHub' +if __name__ == "__main__": + query = "GitHub" print(wiki_search(query)) - query = 'python' + query = "python" print(wiki_search(query)) - query = 'магнитогорск' + query = "магнитогорск" print(wiki_search(query)) print(wiki_search(query, unquote_percent_encoded=True)) diff --git a/merge_url_params.py b/merge_url_params.py new file mode 100644 index 000000000..d3cac3876 --- /dev/null +++ b/merge_url_params.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from urllib.parse import ParseResult, urlparse, urlencode, parse_qsl + + +def merge_url_params(url: str, params: dict) -> str: + result: ParseResult = urlparse(url) + + current_params = dict(parse_qsl(result.query)) + merged_params = {**current_params, **params} + + new_query = urlencode(merged_params, doseq=True) + return result._replace(query=new_query).geturl() + + +if __name__ == "__main__": + url = "https://examples.com/jobs/search?keywords=engineer" + new_params = {"location": "United States", "keywords": True, "items": [1, 2, 3]} + + assert ( + merge_url_params(url, new_params) + == "https://examples.com/jobs/search?keywords=True&location=United+States&items=1&items=2&items=3" + ) + + url = "https://examples.com/jobs/search" + assert ( + merge_url_params(url, new_params) + == "https://examples.com/jobs/search?location=United+States&keywords=True&items=1&items=2&items=3" + ) diff --git a/metabolism.py b/metabolism.py new file mode 100644 index 000000000..2dc8969e5 --- /dev/null +++ b/metabolism.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Формулы для вычисления обмена веществ +""" + + +import enum + + +@enum.unique +class FactorEnum(enum.Enum): + # Сидячий образ жизни. Минимум нагрузки + V_12 = 1.2 + + # Небольшая физическая нагрузка/занятия спортом 1-3 в неделю + V_1375 = 1.375 + + # Достаточно большая физическая нагрузка/занятия спортом 3-5 в неделю + V_155 = 1.55 + + # Большая физическая нагрузка/занятия спортом 6-7 раз в неделю + V_1725 = 1.725 + + # Очень большая ежедневная физическая нагрузка/занятия спортом 2 раза в день + V_19 = 1.9 + + +def get_1918_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Харриса-Бенедикта для мужчин. + """ + value = (13.7416 * weight_kg) + (5.0033 * height_cm) - (6.7500 * age) + 66.4730 + return int(value * factor.value) + + +def get_1918_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Харриса-Бенедикта для женщин. + """ + value = (9.5634 * weight_kg) + (1.8496 * height_cm) - (4.6756 * age) + 665.0955 + return int(value * factor.value) + + +def get_1984_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Пересмотренная Формула-Уравнение Харриса-Бенедикта для мужчин. + """ + value = (13.397 * weight_kg) + (4.799 * height_cm) - (5.677 * age) + 88.362 + return int(value * factor.value) + + +def get_1984_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Пересмотренная Формула-Уравнение Харриса-Бенедикта для женщин. + """ + value = (9.247 * weight_kg) + (3.098 * height_cm) - (4.330 * age) + 447.593 + return int(value * factor.value) + + +def get_2005_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Миффлина-Санкт-Джеора для мужчин. + """ + value = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) + 5 + return int(value * factor.value) + + +def get_2005_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Миффлина-Санкт-Джеора для женщин. + """ + value = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) - 161 + return int(value * factor.value) + + +if __name__ == "__main__": + weight_kg = 100 + height_cm = 190 + age = 30 + + print("male (1918):", get_1918_for_male(weight_kg, height_cm, age)) + print("female (1918):", get_1918_for_female(weight_kg, height_cm, age)) + # male (1918): 2626 + # female (1918): 2199 + + print() + + print("male 1984:", get_1984_for_male(weight_kg, height_cm, age)) + print("female 1984:", get_1984_for_female(weight_kg, height_cm, age)) + # male 1984: 2603 + # female 1984: 2197 + + print() + + print("male 2005:", get_2005_for_male(weight_kg, height_cm, age)) + print("female 2005:", get_2005_for_female(weight_kg, height_cm, age)) + # male 2005: 2451 + # female 2005: 2251 diff --git a/minecraft__seed.py b/minecraft__seed.py deleted file mode 100644 index c37fe962e..000000000 --- a/minecraft__seed.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://minecraft-ru.gamepedia.com/Зерно -# Последовательность в поле ввода преобразуется с помощью Java-функции String.hashCode(). -# Например, строка «abc» конвертируется в числовое значение 97×31² + 98×31 + 99 = 96354. - -def get_value_seed_v1(seed: str) -> int: - return sum(map(lambda x: x[1] * (31 ** x[0]), enumerate(map(ord, reversed(seed))))) - - -def get_value_seed_v2(seed: str) -> int: - value = 0 - - for i in range(len(seed)): - j = (len(seed) - 1) - i - - value += ord(seed[i]) * (31 ** j) - - return value - - -def get_value_seed_v3(seed: str) -> int: - return sum(ord(seed[i]) * (31 ** ((len(seed) - 1) - i)) for i in range(len(seed))) - - -if __name__ == '__main__': - # print(97 * 31 ** 2 + 98 * 31 + 99) - - print(get_value_seed_v1('abc')) - print(get_value_seed_v2('abc')) - print(get_value_seed_v3('abc')) - - assert get_value_seed_v1('abc') == 96354 - assert get_value_seed_v2('abc') == 96354 - assert get_value_seed_v3('abc') == 96354 diff --git a/mock__examples/_test_datetime.py b/mock__examples/_test_datetime.py new file mode 100644 index 000000000..7d7a765ad --- /dev/null +++ b/mock__examples/_test_datetime.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + + +def get() -> tuple[datetime, datetime]: + return datetime.now(), datetime.utcnow() diff --git a/mock__examples/datetime_now_utcnow.py b/mock__examples/datetime_now_utcnow.py new file mode 100644 index 000000000..416091557 --- /dev/null +++ b/mock__examples/datetime_now_utcnow.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from unittest.mock import patch, Mock +import datetime + +import _test_datetime + + +def test() -> None: + print("local:", datetime.datetime.now(), datetime.datetime.utcnow()) + print("import:", *_test_datetime.get()) + print() + + +datetime_mock = Mock(wraps=datetime.datetime) +datetime_mock.now.return_value = datetime.datetime(1999, 1, 1) +datetime_mock.utcnow.return_value = datetime.datetime(2000, 1, 1) + +test() + +with ( + patch("datetime.datetime", new=datetime_mock), + patch("_test_datetime.datetime", new=datetime_mock) +): + test() + # 1999-01-01 00:00:00 2000-01-01 00:00:00 + + assert datetime.datetime.now() == datetime.datetime(1999, 1, 1) + assert datetime.datetime.utcnow() == datetime.datetime(2000, 1, 1) + + now_2, utcnow_2 = _test_datetime.get() + assert datetime.datetime.now() == now_2 + assert datetime.datetime.utcnow() == utcnow_2 + +test() diff --git a/module click.py b/module click.py deleted file mode 100644 index 2c5ab50e1..000000000 --- a/module click.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import click - -# @click.group() -# def greet (): pass -# -# @greet.command () -# def hello ( ** kwargs ): pass -# -# @greet.command () -# def goodbye ( ** kwargs ): pass -# -# if __name__ == '__main__': -# greet () - -# @click.command() -# @click.argument('filename', required=False) -# def touch(filename): -# click.echo('filename=' + str(filename)) -# touch() - -# @click.command() -# @click.argument('src', nargs=-1) -# @click.argument('dst', nargs=1) -# def copy(src, dst): -# for fn in src: -# click.echo('move %s to folder %s' % (fn, dst)) -# -# if __name__ == '__main__': -# copy() - - -# @click.command() -# @click.argument('input', type=click.File('rb')) -# @click.argument('output', type=click.File('wb')) -# def inout(input, output): -# while True: -# chunk = input.read(1024) -# if not chunk: -# break -# output.write(chunk) -# -# inout() - -# -# @click.command() -# @click.argument('f', type=click.Path(exists=True)) -# def touch(f): -# click.echo(click.format_filename(f)) -# -# touch() - -# import os -# for k, v in os.environ.items(): -# print(k + ': ') -# for i, val in enumerate(v.split(';'), 1): -# print(' {}. {}'.format(i, val)) -# quit() - -# @click.command() -# @click.argument('src', envvar='SRC', type=click.File('r')) -# def echo(src): -# click.echo(src.read()) - -# @click.command() -# @click.argument('files', nargs=1, type=int) -# def touch(files): -# print(type(files)) -# click.echo(files) - - -# @click.command() -# @click.option('--value', type=[str, int]) -# def touch(value): -# print(value) -# click.echo('%s=%s' % value) - - -# @click.command() -# @click.option('--message', '-m', multiple=True) -# def touch(message): -# print(type(message), message) -# click.echo('\n'.join(message)) - - -# @click.command() -# @click.option('-m', '--message') -# def touch(message): -# print(type(message), message) -# click.echo('\n'.join(message)) - -# @click.command() -# @click.option('-v', '--verbose', count=True) -# def touch(verbose): -# click.echo('Verbosity: %s' % verbose) - -# @click.command() -# @click.option('-shoot/-no-shoot') -# @click.option('-flag', is_flag=True) -# @click.option('/debug;/no-debug') -# def touch(shoot, flag, debug): -# click.echo('Verbosity: %s %s %s' % (shoot, flag, debug)) - - -# import sys -# -# @click.command() -# @click.option('--upper', 'case', flag_value='upper', default=True) -# @click.option('--lower', 'case', flag_value='lower') -# def touch(case): -# print(case) -# click.echo(getattr(sys.platform, case)()) -# -# touch() - - -# @click.command() -# @click.option('--hash-type', '-hash', type=click.Choice(['md5', 'sha1'])) -# def digest(hash_type): -# click.echo(hash_type) -# -# digest() - -# @click.command() -# @click.option('--name', prompt=True) -# @click.option('--name2', prompt='Your name please') -# def hello(name, name2): -# click.echo('Hello %s-%s!' % (name, name2)) -# -# hello() - - -# @click.command() -# @click.option('--login', prompt='Your login please') -# @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) -# # @click.password_option() -# def encrypt(password, login): -# import codecs -# click.echo('Encrypting password to %s %s' % (codecs.encode(password, 'ROT13'), login)) -# -# encrypt() - - -# import os -# -# -# @click.command() -# # @click.option('--username', prompt=True, default=lambda: os.environ.get('JAVA_HOME', '')) -# @click.option('--username', prompt=True, default=lambda: 1) -# @click.option('--username', prompt=True, default=lambda: [1, 2, 3]) -# def hello(username): -# print("Hello,", username) -# -# hello() - - -# def print_version(ctx, param, value): -# if not value or ctx.resilient_parsing: -# return -# click.echo('Version 1.0') -# ctx.exit() -# -# @click.command() -# @click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True) -# def hello(): -# click.echo('Hello World!') -# -# hello() - - -# def abort_if_false(ctx, param, value): -# if not value: -# ctx.abort() -# -# @click.command() -# @click.option('--yes', is_flag=True, callback=abort_if_false, -# expose_value=False, -# prompt='Are you sure you want to drop the db?') -# def dropdb(): -# click.echo('Dropped all tables!') - - -# @click.command() -# @click.confirmation_option(help='Are you sure you want to drop the db?') -# def dropdb(): -# click.echo('Dropped all tables!') -# -# dropdb() - diff --git a/moving_LNKs_from_non_existent_files.py b/moving_LNKs_from_non_existent_files.py new file mode 100644 index 000000000..4876bcee8 --- /dev/null +++ b/moving_LNKs_from_non_existent_files.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shutil +from pathlib import Path + +# pip install winshell +import winshell + + +DIR = Path(r"~\Desktop\Пройти").expanduser() +DIR_NOT_EXISTENT = DIR / "Несуществуют" + +files = [] +for file_name in DIR.glob("*.lnk"): + shortcut = winshell.shortcut(str(file_name)) + path = Path(shortcut.path) + if not path.exists(): + files.append(file_name) + +print(f"Найдено {len(files)}") + +if files: + DIR_NOT_EXISTENT.mkdir(parents=True, exist_ok=True) + + for f in files: + print(f"Выполнено перемещение {f.name} в папку {DIR_NOT_EXISTENT}") + + new_file = DIR_NOT_EXISTENT / f.name + shutil.move(f, new_file) diff --git a/multiprocessing__examples/code_execution_with_time_limit.py b/multiprocessing__examples/code_execution_with_time_limit.py deleted file mode 100644 index 0788aa60e..000000000 --- a/multiprocessing__examples/code_execution_with_time_limit.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import multiprocessing - - -def run(): - import time - - i = 1 - - # Бесконечный цикл - while True: - print(i) - i += 1 - - time.sleep(1) - - -if __name__ == '__main__': - p = multiprocessing.Process(target=run) - p.start() - - # Wait - p.join(5) - - # Если процесс живой,то убиваем его - if p.is_alive(): - print("Kill it.") - - # Terminate - p.terminate() diff --git a/multiprocessing__examples/create_process_get_result__communication__Pipe.py b/multiprocessing__examples/create_process_get_result__communication__Pipe.py deleted file mode 100644 index 35e196cc4..000000000 --- a/multiprocessing__examples/create_process_get_result__communication__Pipe.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process, Pipe - - -def f(con, name): - con.send('Hello, ' + name) - con.close() - - -if __name__ == '__main__': - parent_conn, child_conn = Pipe() - p = Process(target=f, args=(child_conn, 'bob')) - p.start() - print(parent_conn.recv()) # Hello, bob - p.join() diff --git a/multiprocessing__examples/create_process_get_result__communication__Queue.py b/multiprocessing__examples/create_process_get_result__communication__Queue.py deleted file mode 100644 index aa56310ab..000000000 --- a/multiprocessing__examples/create_process_get_result__communication__Queue.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process, Queue - - -def f(q, name): - q.put('Hello, ' + name) - - -if __name__ == '__main__': - q = Queue() - p = Process(target=f, args=(q, 'bob')) - p.start() - print(q.get()) # Hello, bob - p.join() diff --git a/multiprocessing__examples/hello_world__Process.py b/multiprocessing__examples/hello_world__Process.py deleted file mode 100644 index 0efeac66d..000000000 --- a/multiprocessing__examples/hello_world__Process.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process - - -def f(name): - print('Hello,', name) - - -if __name__ == '__main__': - p = Process(target=f, args=('bob',)) - p.start() - p.join() diff --git a/multiprocessing__examples/hello_world__with_daemon.py b/multiprocessing__examples/hello_world__with_daemon.py deleted file mode 100644 index bbe07a628..000000000 --- a/multiprocessing__examples/hello_world__with_daemon.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process, current_process -import time - - -def run(): - print(current_process()) - - for i in 'Hello World!': - print(i) - time.sleep(0.2) - - -if __name__ == '__main__': - p = Process(target=run, daemon=True) - p.start() - - # Main process NOT WAIT thread diff --git a/multiprocessing__examples/hello_world__without_daemon.py b/multiprocessing__examples/hello_world__without_daemon.py deleted file mode 100644 index 69e7bc4e0..000000000 --- a/multiprocessing__examples/hello_world__without_daemon.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process, current_process -import time - - -def run(): - print(current_process()) - - for i in 'Hello World!': - print(i) - time.sleep(0.2) - - -if __name__ == '__main__': - p = Process(target=run, daemon=False) - p.start() - - # Main process WAIT child process diff --git a/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py b/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py deleted file mode 100644 index 850c80d7c..000000000 --- a/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process -from multiprocessing__QApplication__in_other_process import go as go_qt -from multiprocessing__Tkinter__in_other_process import go as go_tk - - -def create_qt(): - p = Process(target=go_qt, args=('Qt',)) - p.start() - - -def create_tk(): - p = Process(target=go_tk, args=('Tk',)) - p.start() - - -if __name__ == '__main__': - from PyQt5.Qt import QApplication, QPushButton, QWidget, QVBoxLayout - app = QApplication([]) - - button_qt = QPushButton('Create Qt') - button_qt.clicked.connect(create_qt) - - button_tk = QPushButton('Create Tk') - button_tk.clicked.connect(create_tk) - - layout = QVBoxLayout() - layout.addWidget(button_qt) - layout.addWidget(button_tk) - - mw = QWidget() - mw.setLayout(layout) - mw.show() - - app.exec() diff --git a/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py b/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py deleted file mode 100644 index 7c49a0231..000000000 --- a/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def go(name): - from PyQt5.Qt import QApplication, Qt, QLabel - app = QApplication([]) - - mw = QLabel() - mw.setAlignment(Qt.AlignCenter) - mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) - mw.show() - - app.exec() - - -if __name__ == '__main__': - from multiprocessing import Process - p = Process(target=go, args=('bob',)) - p.start() - - # Необязательно в данном случае -- главный поток все-равно закроется после дочернего - # p.join() diff --git a/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py b/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py deleted file mode 100644 index 7a17434fa..000000000 --- a/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -if __name__ == '__main__': - from multiprocessing import Pool - from multiprocessing__QApplication__in_other_process import go - - with Pool() as p: - p.map(go, ['Alice', 'Bob', 'World']) diff --git a/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py b/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py deleted file mode 100644 index 2289017f3..000000000 --- a/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def go(name): - import tkinter as tk - app = tk.Tk() - app.minsize(150, 50) - - mw = tk.Label(app, text='Hello, ' + name) - mw.pack(fill='both', expand=True) - - app.mainloop() - - -if __name__ == '__main__': - from multiprocessing import Process - p = Process(target=go, args=('bob',)) - p.start() - - # Необязательно в данном случае -- главный поток все-равно закроется после дочернего - # p.join() diff --git a/multiprocessing__examples/multiprocessing_with_webservers__flask.py b/multiprocessing__examples/multiprocessing_with_webservers__flask.py deleted file mode 100644 index 153112347..000000000 --- a/multiprocessing__examples/multiprocessing_with_webservers__flask.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def go(port): - from flask import Flask - app = Flask(__name__) - - import logging - logging.basicConfig(level=logging.DEBUG) - - @app.route("/") - def index(): - return "Hello World! (port={})".format(port) - - app.run(port=port) - - -def go_parser(urls): - import time - import requests - - while True: - for url in urls: - try: - rs = requests.get(url) - print('Parser: {}. "{}"'.format(rs, rs.text)) - - except: - pass - - time.sleep(2) - - -if __name__ == '__main__': - # NOTE: recommended to add daemon=False or call join() for each process - - from multiprocessing import Process - - p1 = Process(target=go, args=(5001,)) - p1.start() - - p2 = Process(target=go, args=(5002,)) - p2.start() - - # OR: - # for port in [5001, 5002]: - # p = Process(target=go, args=(port,)) - # p.start() - - urls = ['http://127.0.0.1:5001/', 'http://127.0.0.1:5002/'] - p3 = Process(target=go_parser, args=(urls,)) - p3.start() - - # NOTE: optional - # p1.join() - # p2.join() - # p3.join() diff --git a/multiprocessing__examples/print_process_info.py b/multiprocessing__examples/print_process_info.py deleted file mode 100644 index 9e91062f0..000000000 --- a/multiprocessing__examples/print_process_info.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing import Process -import os - - -def info(title): - print(title) - print('module name:', __name__) - print('parent process:', os.getppid()) - print('process id:', os.getpid()) - - -def f(name): - info('function f') - print('Hello,', name) - - -if __name__ == '__main__': - info('main line') - print() - - p = Process(target=f, args=('bob',)) - p.start() - p.join() diff --git a/multithreading__threading__examples/code_execution_with_time_limit.py b/multithreading__threading__examples/code_execution_with_time_limit.py deleted file mode 100644 index 850ed7f2e..000000000 --- a/multithreading__threading__examples/code_execution_with_time_limit.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading - - -def run(): - import time - - i = 1 - - # Бесконечный цикл - while True: - print(i) - i += 1 - - time.sleep(1) - - -if __name__ == '__main__': - thread = threading.Thread(target=run, daemon=True) - thread.start() - - # Wait - thread.join(5) - - print('Quit!') diff --git a/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py b/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py deleted file mode 100644 index 63929c1db..000000000 --- a/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def go(url): - from urllib.request import urlopen - rs = urlopen(url) - print(url, rs) - - return rs - - -urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', -] - -from multiprocessing.dummy import Pool as ThreadPool -pool = ThreadPool() -results = pool.map(go, urls) -print(results) diff --git a/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py b/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py deleted file mode 100644 index 96d6642cd..000000000 --- a/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing.dummy import Pool as ThreadPool -import requests - - -urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', -] - -pool = ThreadPool() -result = pool.map(requests.get, urls) -print(result) diff --git a/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py b/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py deleted file mode 100644 index ba9327893..000000000 --- a/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing.dummy import Pool as ThreadPool -from urllib.request import urlopen - - -urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', -] - -pool = ThreadPool() -result = pool.map(urlopen, urls) -print(result) diff --git a/multithreading__threading__examples/flask__threaded/flask_test_script.py b/multithreading__threading__examples/flask__threaded/flask_test_script.py deleted file mode 100644 index fb3c257ec..000000000 --- a/multithreading__threading__examples/flask__threaded/flask_test_script.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests - -# With threaded -for _ in range(5): - rs = requests.get('http://127.0.0.1:6000/') - print(rs.text) - -print() - -# Without threaded -for _ in range(5): - rs = requests.get('http://127.0.0.1:6001/') - print(rs.text) - diff --git a/multithreading__threading__examples/flask__threaded/threaded__with.py b/multithreading__threading__examples/flask__threaded/threaded__with.py deleted file mode 100644 index a6c7f17ef..000000000 --- a/multithreading__threading__examples/flask__threaded/threaded__with.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from flask import Flask -app = Flask(__name__) - - -@app.route("/") -def index(): - import threading - return "Current thread: {}".format(threading.current_thread()) - - -if __name__ == '__main__': - app.run( - port=6000, - - # Включение поддержки множества подключений - threaded=True, - ) diff --git a/multithreading__threading__examples/flask__threaded/threaded__without.py b/multithreading__threading__examples/flask__threaded/threaded__without.py deleted file mode 100644 index db1096552..000000000 --- a/multithreading__threading__examples/flask__threaded/threaded__without.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from flask import Flask -app = Flask(__name__) - - -@app.route("/") -def index(): - import threading - return "Current thread: {}".format(threading.current_thread()) - - -if __name__ == '__main__': - app.run(port=6001) diff --git a/multithreading__threading__examples/hello_world__with_daemon.py b/multithreading__threading__examples/hello_world__with_daemon.py deleted file mode 100644 index f820e00bd..000000000 --- a/multithreading__threading__examples/hello_world__with_daemon.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading -import time - - -def run(): - print(threading.current_thread()) - for i in 'Hello World!': - print(i) - time.sleep(0.2) - - -if __name__ == '__main__': - thread = threading.Thread(target=run, daemon=True) - thread.start() - - # Main process NOT WAIT thread diff --git a/multithreading__threading__examples/hello_world__without_daemon.py b/multithreading__threading__examples/hello_world__without_daemon.py deleted file mode 100644 index 213a22e49..000000000 --- a/multithreading__threading__examples/hello_world__without_daemon.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading -import time - - -def run(): - print(threading.current_thread()) - for i in 'Hello World!': - print(i) - time.sleep(0.2) - - -if __name__ == '__main__': - thread = threading.Thread(target=run, daemon=False) - thread.start() - - # Main process WAIT thread diff --git a/multithreading__threading__examples/mini_example__with_threading.py b/multithreading__threading__examples/mini_example__with_threading.py deleted file mode 100644 index f098b1c74..000000000 --- a/multithreading__threading__examples/mini_example__with_threading.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading - - -def run(name='main', sleep_seconds=None): - if sleep_seconds: - import time - time.sleep(sleep_seconds) - - print('start: "{}": {}'.format(name, threading.current_thread())) - - -if __name__ == '__main__': - run() - - thread = threading.Thread(target=run, args=('thread #1', 3)) - thread.start() - - thread = threading.Thread(target=run, args=('thread #2', 1)) - thread.start() - - thread = threading.Thread(target=run, args=('thread #3', 0)) - thread.start() diff --git a/multithreading__threading__examples/race_condition.py b/multithreading__threading__examples/race_condition.py deleted file mode 100644 index 7538bdccc..000000000 --- a/multithreading__threading__examples/race_condition.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing.dummy import Pool as ThreadPool -import threading - - -number = 0 -DATA = { - 'number': 0 -} -lock = threading.Lock() - - -def inc(*args): - global number - - DATA['number'] += 1 - number += 1 - - -def inc_lock(*args): - global number - - with lock: - number += 1 - DATA['number'] += 1 - - -max_number = 1_000_000 - -# Only one thread -- main -number = DATA['number'] = 0 -for _ in range(max_number): - inc() -print(number, DATA['number']) -# 1000000 1000000 - -# Many threads -number = DATA['number'] = 0 -pool = ThreadPool() -pool.map(inc, range(max_number)) -print(number, DATA['number']) -# 819903 459802 - -# Many threads -number = DATA['number'] = 0 -pool = ThreadPool() -pool.map(inc_lock, range(max_number)) -print(number, DATA['number']) -# 1000000 1000000 diff --git a/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py b/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py deleted file mode 100644 index 70f20e689..000000000 --- a/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from multiprocessing.dummy import Pool as ThreadPool -import time - -from bs4 import BeautifulSoup -import requests - - -def go(url): - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - return root.select_one('.user-details > a').text.strip() - - -urls = [ - 'https://ru.stackoverflow.com/questions/962400', 'https://ru.stackoverflow.com/questions/962311', - 'https://ru.stackoverflow.com/questions/962128', 'https://ru.stackoverflow.com/questions/962396', - 'https://ru.stackoverflow.com/questions/962349' -] - -t = time.time() -result = [go(url) for url in urls] -print(result) -print('Elapsed {:.3f} secs'.format(time.time() - t)) -# ['Streletz', 'Kromster', 'Stepan Kasyanenko', 'Kromster', 'JamesJGoodwin'] -# Elapsed 6.030 secs - -print() - -t = time.time() -pool = ThreadPool() -result = pool.map(go, urls) -print(result) -print('Elapsed {:.3f} secs'.format(time.time() - t)) -# ['Streletz', 'Kromster', 'Stepan Kasyanenko', 'Kromster', 'JamesJGoodwin'] -# Elapsed 3.203 secs diff --git a/multithreading__threading__examples/threading_with_webservers__flask.py b/multithreading__threading__examples/threading_with_webservers__flask.py deleted file mode 100644 index 5fc19cd3f..000000000 --- a/multithreading__threading__examples/threading_with_webservers__flask.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading - - -def run(port=80): - from flask import Flask - app = Flask(__name__) - - import logging - logging.basicConfig(level=logging.DEBUG) - - @app.route("/") - def index(): - html = "Hello World! (port={})".format(port) - print(html) - - return html - - app.run(port=port) - - -if __name__ == '__main__': - # NOTE: recommended to add daemon=False or call join() for each thread - - thread = threading.Thread(target=run, args=(5000, )) - thread.start() - - thread = threading.Thread(target=run, args=(5001, )) - thread.start() - - thread = threading.Thread(target=run, args=(5002,)) - thread.start() - - # OR: - # for port in [5000, 5001, 5002]: - # thread = threading.Thread(target=run, args=(port,)) - # thread.start() diff --git a/multithreading__threading__examples/timeout_run_function.py b/multithreading__threading__examples/timeout_run_function.py deleted file mode 100644 index 2b3d575be..000000000 --- a/multithreading__threading__examples/timeout_run_function.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time -from threading import Thread, current_thread - - -class User: - def __init__(self, name): - self.name = name - - def post_msg(self): - time.sleep(3) - - # Написать пользователю - print('Hi, {}! current_thread: {}'.format(self.name, current_thread())) - - -user_1 = User('Vasya') -user_1.post_msg() - -user_2 = User('Petya') -Thread(target=user_2.post_msg).start() - - -def foo(name): - time.sleep(5) - - # Написать пользователю - print('Hi, {}! current_thread: {}'.format(name, current_thread())) - - -Thread(target=lambda: foo('Thread')).start() diff --git a/my_torrent_list_from_iknowwhatyoudownload.py b/my_torrent_list_from_iknowwhatyoudownload.py index 96e7a7ede..88caad2f4 100644 --- a/my_torrent_list_from_iknowwhatyoudownload.py +++ b/my_torrent_list_from_iknowwhatyoudownload.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -11,31 +11,34 @@ """ +import time + import requests from bs4 import BeautifulSoup -import time def get_my_torrents(append_torrent_size=False): - rs = requests.get('http://iknowwhatyoudownload.com/ru/peer/', headers={'User-Agent': '-'}) - root = BeautifulSoup(rs.content, 'lxml') + rs = requests.get( + "http://iknowwhatyoudownload.com/ru/peer/", headers={"User-Agent": "-"} + ) + root = BeautifulSoup(rs.content, "lxml") # Если нужно вместе с названием передавать и размер торрента if not append_torrent_size: - return [item.text.strip() for item in root.select('.torrent_files > a')] + return [item.text.strip() for item in root.select(".torrent_files > a")] items = list() - for row in root.select('table > tbody > tr'): - name = row.select_one('.name-column').text.strip() - size = row.select_one('.size-column').text.strip() + for row in root.select("table > tbody > tr"): + name = row.select_one(".name-column").text.strip() + size = row.select_one(".size-column").text.strip() items.append((name, size)) return items -if __name__ == '__main__': +if __name__ == "__main__": while True: try: items = get_my_torrents() @@ -45,4 +48,4 @@ def get_my_torrents(append_torrent_size=False): time.sleep(60 * 60 * 12) except Exception as e: - print('Error:', e) + print("Error:", e) diff --git a/namedtuple__typing__example/main.py b/namedtuple__typing__example/main.py index 1d4965687..daec6fe50 100644 --- a/namedtuple__typing__example/main.py +++ b/namedtuple__typing__example/main.py @@ -1,28 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from collections import namedtuple +from typing import NamedTuple -Point = namedtuple('Point', ['x', 'y']) +Point = namedtuple("Point", ["x", "y"]) # # TypeError: __new__() missing 2 required positional arguments: 'x' and 'y' # p = Point() # print(p) p = Point(1, 2) -print(p) # Point(x=1, y=2) +print(p) # Point(x=1, y=2) print(p._asdict()) # OrderedDict([('x', 1), ('y', 2)]) print() # SOURCE: https://docs.python.org/3/library/typing.html#typing.NamedTuple -from typing import NamedTuple - - class Point(NamedTuple): x: int = 0 y: int = 0 @@ -32,5 +30,5 @@ class Point(NamedTuple): print(p) # Point(x=0, y=0) p = Point(1, 2) -print(p) # Point(x=1, y=2) +print(p) # Point(x=1, y=2) print(p._asdict()) # OrderedDict([('x', 1), ('y', 2)]) diff --git a/namedtuple_example/namedtuple.py b/namedtuple_example/namedtuple.py index 72ab00839..e41df4050 100644 --- a/namedtuple_example/namedtuple.py +++ b/namedtuple_example/namedtuple.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# TODO: https://docs.python.org/3/library/collections.html#collections.namedtuple_example -# Basic example from collections import namedtuple -Point = namedtuple('Point', ['x', 'y']) + + +Point = namedtuple("Point", ["x", "y"]) p = Point(11, y=22) # instantiate with positional or keyword arguments -print(p[0] + p[1]) # indexable like the plain tuple (11, 22) -x, y = p # unpack like a regular tuple +print(p[0] + p[1]) # indexable like the plain tuple (11, 22) +x, y = p # unpack like a regular tuple print(x, y) -print(p.x + p.y) # fields also accessible by name -print(p) # readable __repr__ with a name=value style +print(p.x + p.y) # fields also accessible by name +print(p) # readable __repr__ with a name=value style print() -XYZPoint = namedtuple('XYZPoint', ['x', 'y', 'z']) +XYZPoint = namedtuple("XYZPoint", ["x", "y", "z"]) xyz = XYZPoint(2, 2, z=5) print(xyz) print("z:", xyz.z) diff --git a/natasha__examples/hello_world.py b/natasha__examples/hello_world.py new file mode 100644 index 000000000..337be9ca2 --- /dev/null +++ b/natasha__examples/hello_world.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/natasha/natasha + + +# pip install natasha +from natasha import Segmenter, NewsEmbedding, NewsMorphTagger, Doc + + +segmenter = Segmenter() + +emb = NewsEmbedding() +morph_tagger = NewsMorphTagger(emb) + + +text = "Появление ООН было обусловлено целым рядом объективных факторов" + +doc = Doc(text) +doc.segment(segmenter) +doc.tag_morph(morph_tagger) + +for token in doc.tokens: + print(f"[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n") + +""" +[0:9] NOUN 'Появление' +DocToken(stop=9, text='Появление', pos='NOUN', feats=) + +[10:13] PROPN 'ООН' +DocToken(start=10, stop=13, text='ООН', pos='PROPN', feats=) + +[14:18] AUX 'было' +DocToken(start=14, stop=18, text='было', pos='AUX', feats=) + +[19:30] VERB 'обусловлено' +DocToken(start=19, stop=30, text='обусловлено', pos='VERB', feats=) + +[31:36] ADJ 'целым' +DocToken(start=31, stop=36, text='целым', pos='ADJ', feats=) + +[37:42] NOUN 'рядом' +DocToken(start=37, stop=42, text='рядом', pos='NOUN', feats=) + +[43:54] ADJ 'объективных' +DocToken(start=43, stop=54, text='объективных', pos='ADJ', feats=) + +[55:63] NOUN 'факторов' +DocToken(start=55, stop=63, text='факторов', pos='NOUN', feats=) +""" + +print("\n" + "-" * 10 + "\n") + +text = "Hello World!" + +doc = Doc(text) +doc.segment(segmenter) +doc.tag_morph(morph_tagger) + +for token in doc.tokens: + print(f"[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n") + +""" +[0:5] X 'Hello' +DocToken(stop=5, text='Hello', pos='X', feats=) + +[6:11] X 'World' +DocToken(start=6, stop=11, text='World', pos='X', feats=) + +[11:12] PUNCT '!' +DocToken(start=11, stop=12, text='!', pos='PUNCT') +""" diff --git a/neutralize_emoji.py b/neutralize_emoji.py new file mode 100644 index 000000000..e77a533b1 --- /dev/null +++ b/neutralize_emoji.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/crinny/gatee/blob/11f78228fbb42dc4e06d180d90974849d4d4e45f/bot/utils/emoji.py#L7 + + +def neutralize_emoji(character: str) -> str: + """ + Remove skin tone and gender modifiers from the emoji. + """ + return ( + character.replace("🏻", "") + .replace("🏼", "") + .replace("🏽", "") + .replace("🏾", "") + .replace("🏿", "") + .replace("♂️", "") + .replace("♀️", "") + ) + + +if __name__ == "__main__": + text = ", ".join(["👨", "👨🏻", "👨🏼", "👨🏽", "👨🏾", "👨🏿"]) + print(neutralize_emoji(text)) + # 👨, 👨, 👨, 👨, 👨, 👨 + + text = ", ".join(["👩", "👩🏻", "👩🏼", "👩🏽", "👩🏾", "👩🏿"]) + print(neutralize_emoji(text)) + # 👩, 👩, 👩, 👩, 👩, 👩 diff --git a/nltk__examples/_add_support_russian.py b/nltk__examples/_add_support_russian.py index 448e1cb06..cd36ad0ad 100644 --- a/nltk__examples/_add_support_russian.py +++ b/nltk__examples/_add_support_russian.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk import nltk -nltk.download('punkt') +nltk.download("punkt") diff --git a/nltk__examples/_add_support_russian_stopwords.py b/nltk__examples/_add_support_russian_stopwords.py index 9ac18f5b0..319b3192b 100644 --- a/nltk__examples/_add_support_russian_stopwords.py +++ b/nltk__examples/_add_support_russian_stopwords.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk import nltk -nltk.download('stopwords') +nltk.download("stopwords") diff --git a/nltk__examples/nltk_sent_tokenize__with_new_lines.py b/nltk__examples/nltk_sent_tokenize__with_new_lines.py index c06d8b7fc..c28747e24 100644 --- a/nltk__examples/nltk_sent_tokenize__with_new_lines.py +++ b/nltk__examples/nltk_sent_tokenize__with_new_lines.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -19,16 +19,16 @@ sentences = [] for line in text.splitlines(): - sentences += nltk.sent_tokenize(line, language='russian') + sentences += nltk.sent_tokenize(line, language="russian") print(*map(repr, sentences)) # 'Согласен.' 'В первое ПОЕ' 'Да вся вторая часть' 'А читеры — подлецы.' -print('-' * 100) +print("-" * 100) -text = text.replace('\n', '. ') -text = re.sub('[.]{2,}', '.', text) +text = text.replace("\n", ". ") +text = re.sub("[.]{2,}", ".", text) -sentences = nltk.sent_tokenize(text, language='russian') +sentences = nltk.sent_tokenize(text, language="russian") print(*map(repr, sentences)) # 'Согласен.' 'В первое ПОЕ.' 'Да вся вторая часть.' 'А читеры — подлецы.' diff --git a/nltk__examples/split_by_sentences.py b/nltk__examples/split_by_sentences.py index 8a5bd40b7..361b5bae7 100644 --- a/nltk__examples/split_by_sentences.py +++ b/nltk__examples/split_by_sentences.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk diff --git a/nltk__examples/split_by_sentences_and_words.py b/nltk__examples/split_by_sentences_and_words.py index 32895af0a..cb7fe956f 100644 --- a/nltk__examples/split_by_sentences_and_words.py +++ b/nltk__examples/split_by_sentences_and_words.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk @@ -20,17 +20,17 @@ А читеры — подлецы. """ -sentences = nltk.sent_tokenize(text, language='russian') +sentences = nltk.sent_tokenize(text, language="russian") for sent in sentences: print(repr(sent)) words = nltk.word_tokenize(sent) - print(f'({len(words)}): {words}') + print(f"({len(words)}): {words}") words_no_punct = [word for word in words if not is_punctuation(word)] - print(f'({len(words_no_punct)}): {words_no_punct}') + print(f"({len(words_no_punct)}): {words_no_punct}") - words_no_punct = [word for word in words if 'PNCT' not in morph.parse(word)[0].tag] - print(f'({len(words_no_punct)}): {words_no_punct}') + words_no_punct = [word for word in words if "PNCT" not in morph.parse(word)[0].tag] + print(f"({len(words_no_punct)}): {words_no_punct}") print() diff --git a/nltk__examples/stopwords_russian.py b/nltk__examples/stopwords_russian.py index e8cedaf97..47513bbb8 100644 --- a/nltk__examples/stopwords_russian.py +++ b/nltk__examples/stopwords_russian.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk from nltk.corpus import stopwords -print(stopwords.words('russian')) +print(stopwords.words("russian")) # ['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со', 'как', 'а', 'то', 'все', 'она', # 'так', 'его', 'но', 'да', 'ты', 'к', 'у', 'же', 'вы', 'за', 'бы', 'по', 'только', 'ее', # 'мне', 'было', 'вот', 'от', 'меня', 'еще', 'нет', 'о', 'из', 'ему', 'теперь', 'когда', diff --git a/now_UTC_datetime.py b/now_UTC_datetime.py index eaf3fc64c..7017a5840 100644 --- a/now_UTC_datetime.py +++ b/now_UTC_datetime.py @@ -1,9 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -utc_datetime = DT.datetime.utcnow() -print(utc_datetime.strftime("%d/%m/%Y %H:%M:%S")) +from datetime import datetime, timezone + +utc_datetime_old = datetime.utcnow().replace(microsecond=0) +print(utc_datetime_old) + +utc_datetime = datetime.now(timezone.utc).replace(microsecond=0) +print(utc_datetime) + +assert utc_datetime_old != utc_datetime +assert utc_datetime_old == utc_datetime.replace(tzinfo=None) diff --git a/now_UTC_datetime__previous_365_days.py b/now_UTC_datetime__previous_365_days.py index a4fa9840c..f08b36aff 100644 --- a/now_UTC_datetime__previous_365_days.py +++ b/now_UTC_datetime__previous_365_days.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -utc_datetime = DT.datetime.utcnow() +from datetime import datetime, timedelta, timezone + + +utc_datetime = datetime.now(timezone.utc) for i in range(365 + 1): - date = utc_datetime - DT.timedelta(days=i) + date = utc_datetime - timedelta(days=i) print(date.strftime("%d/%m/%Y %H:%M:%S")) diff --git a/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py b/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py index 06334aa61..f25732790 100644 --- a/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py +++ b/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py b/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py index a51fede76..8a34f582c 100644 --- a/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py +++ b/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl @@ -12,7 +12,7 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8b69f96d0bd87c10f3d0779ff46b02558c4cca84/excel__openpyxl__xlwt/xlsx__set_row_column_size.py -def set_row_column_size(ws): +def set_row_column_size(ws) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -34,7 +34,7 @@ def get_pixel_array(img, rgb_hex=False): r, g, b = img.getpixel((x, y)) if rgb_hex: - value = '{:02X}{:02X}{:02X}'.format(r, g, b) + value = f"{r:02X}{g:02X}{b:02X}" row.append(value) else: row.append((r, g, b)) @@ -42,8 +42,8 @@ def get_pixel_array(img, rgb_hex=False): return pixels -def draw_image(ws, img): - img = img.convert('RGB') +def draw_image(ws, img) -> None: + img = img.convert("RGB") # Resize img.thumbnail((250, 250)) @@ -56,9 +56,9 @@ def draw_image(ws, img): cell.fill = PatternFill(fgColor=pixels[i][j], fill_type="solid") -if __name__ == '__main__': +if __name__ == "__main__": wb = openpyxl.Workbook() - ws = wb.get_active_sheet() + ws = wb.active # Масштаб 10% ws.sheet_view.zoomScale = 10 @@ -70,4 +70,4 @@ def draw_image(ws, img): ws.title = file_name draw_image(ws, img) - wb.save('excel.xlsx') + wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py b/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py index 06afd6776..51c990878 100644 --- a/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py +++ b/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py @@ -1,34 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob import os +import sys import openpyxl # pip install Pillow from PIL import Image -import sys -sys.path.append('..') - # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/2c4391214de936260926a47440f2bad2d6fb90da/excel__openpyxl__xlwt/draw_image_in_sheet +sys.path.append("..") from draw_image_in_sheet.main import set_row_column_size, get_pixel_array, draw_image -if __name__ == '__main__': +if __name__ == "__main__": wb = openpyxl.Workbook() # Remove default sheet for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) # Append images - for file_name in glob.glob('images/*.jpg') + glob.glob('images/*.png'): + for file_name in glob.glob("images/*.jpg") + glob.glob("images/*.png"): title = os.path.basename(file_name) ws = wb.create_sheet(title) @@ -40,4 +39,4 @@ img = Image.open(file_name) draw_image(ws, img) - wb.save('excel.xlsx') \ No newline at end of file + wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/find_cells_by_color/main.py b/office__excel__openpyxl__xlwt/find_cells_by_color/main.py index 2eb75e28e..93789d6c5 100644 --- a/office__excel__openpyxl__xlwt/find_cells_by_color/main.py +++ b/office__excel__openpyxl__xlwt/find_cells_by_color/main.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/845335/201445 -def find_cells_by_color(ws, color='00000000'): - ret = {} +from openpyxl.worksheet.worksheet import Worksheet + + +def find_cells_by_color(ws: Worksheet, color: str = "00000000") -> dict: + ret = dict() for row in ws.iter_rows(): for cell in row: if cell.fill.fgColor.value == color: @@ -17,19 +20,20 @@ def find_cells_by_color(ws, color='00000000'): return ret -if __name__ == '__main__': +if __name__ == "__main__": from openpyxl import load_workbook - wb = load_workbook('excel.xlsx') - ws = wb.get_active_sheet() - print('background colors for ALL cells:\n') + wb = load_workbook("excel.xlsx") + ws = wb.active + + print("background colors for ALL cells:\n") for row in ws.iter_rows(): for cell in row: - print(f'[{cell.coordinate}]: {cell.fill.fgColor.value}', end=' ') + print(f"[{cell.coordinate}]: {cell.fill.fgColor.value}", end=" ") print() print() - cells = find_cells_by_color(ws, color='FFFFFF00') - print(f'given color has been found in the following cells: {cells}') + cells = find_cells_by_color(ws, color="FFFFFF00") + print(f"given color has been found in the following cells: {cells}") diff --git a/office__excel__openpyxl__xlwt/get_sheets/main.py b/office__excel__openpyxl__xlwt/get_sheets/main.py index 50a1f737b..a863e8001 100644 --- a/office__excel__openpyxl__xlwt/get_sheets/main.py +++ b/office__excel__openpyxl__xlwt/get_sheets/main.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -wb = openpyxl.load_workbook('excel.xlsx') -print(wb.get_sheet_names()) # ['Sheet', 'auto', 'Students', 'Students1'] +wb = openpyxl.load_workbook("excel.xlsx") +print(wb.sheetnames) # ['Sheet', 'auto', 'Students', 'Students1'] -for name in wb.get_sheet_names(): - ws = wb.get_sheet_by_name(name) +for name in wb.sheetnames: + ws = wb[name] print(name, ws) diff --git a/office__excel__openpyxl__xlwt/hidden_columns.py b/office__excel__openpyxl__xlwt/hidden_columns.py index d9fb0b38d..43312110b 100644 --- a/office__excel__openpyxl__xlwt/hidden_columns.py +++ b/office__excel__openpyxl__xlwt/hidden_columns.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://openpyxl.readthedocs.io/en/stable/usage.html#fold-columns-outline @@ -12,7 +12,7 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() -ws.column_dimensions.group('B', 'D', hidden=True) +ws = wb.active +ws.column_dimensions.group("B", "D", hidden=True) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/insert_image/main.py b/office__excel__openpyxl__xlwt/insert_image/main.py index 0dd37fe83..bb47534f4 100644 --- a/office__excel__openpyxl__xlwt/insert_image/main.py +++ b/office__excel__openpyxl__xlwt/insert_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://openpyxl.readthedocs.io/en/stable/usage.html#inserting-an-image @@ -12,14 +12,14 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active -ws['A1'] = 'You should see three logos below' +ws["A1"] = "You should see three logos below" # Create an image -img = Image('logo.png') +img = Image("logo.png") # Add to worksheet and anchor next to cells -ws.add_image(img, 'A2') +ws.add_image(img, "A2") -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py b/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py index fb98bc61d..bd8639d84 100644 --- a/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py +++ b/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] -FILE_NAME = 'excel.xlsx' +FILE_NAME = "excel.xlsx" try: wb = openpyxl.load_workbook(FILE_NAME) @@ -24,11 +24,11 @@ # Удаление листа, создаваемого по умолчанию, при создании документа for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) # Создание нового листа, названия новых листов будут инкрементироваться: Students, Students1, Students2, и т.п. -ws = wb.create_sheet('Students') +ws = wb.create_sheet("Students") for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -37,4 +37,4 @@ for j, value in enumerate(row, 1): ws.cell(row=i, column=j).value = value -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/print_cells/main.py b/office__excel__openpyxl__xlwt/print_cells/main.py new file mode 100644 index 000000000..09b1e7a67 --- /dev/null +++ b/office__excel__openpyxl__xlwt/print_cells/main.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl + + +path = "../get_sheets/excel.xlsx" + +wb = openpyxl.load_workbook(path) +sheet = wb.active + +for row in range(1, sheet.max_row + 1): + for col in range(1, sheet.max_column + 1): + cell = sheet.cell(row=row, column=col) + print(cell.value, end=" | ") + print() +""" +Language | Text | +python | ---------- | +java | jjjjjjjjjj | +c# | ***** | +c++ | 00000000000000000000000000000000000000000000000000 | +""" diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx new file mode 100644 index 000000000..36fb9146d Binary files /dev/null and b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx differ diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py new file mode 100644 index 000000000..3222048c8 --- /dev/null +++ b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +from pathlib import Path + +# pip install pyexcel-xlsx +from pyexcel_xlsx import read_data + + +DIR = Path(__file__).resolve().parent +file_name = str(DIR / "output.xlsx") + + +data = read_data(file_name) +print(json.dumps(data, ensure_ascii=False, indent=4)) +""" +{ + "Sheet 1": [ + [ + "Col1", + "Col2", + "Col3" + ], + [ + 1, + 2, + 3 + ], + [ + 4, + 5, + 6 + ] + ], + "Sheet 2": [ + [ + "row 1", + "row 2", + "row 3" + ] + ], + "Страница 3": [ + [ + "Поле:", + "Привет" + ], + [ + "Поле:", + "Мир!" + ] + ] +} +""" diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py new file mode 100644 index 000000000..ea4e7b1b0 --- /dev/null +++ b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyexcel-xlsx +from pyexcel_xlsx import save_data + + +DIR = Path(__file__).resolve().parent +file_name = str(DIR / "output.xlsx") + + +data = { + "Sheet 1": [ + ["Col1", "Col2", "Col3"], + [1, 2, 3], + [4, 5, 6] + ], + "Sheet 2": [["row 1"]], +} +data.update({ + "Sheet 2": [ + ["row 1", "row 2", "row 3"] + ] +}) +data.update({ + "Страница 3": [ + ["Поле:", "Привет"], + ["Поле:", "Мир!"] + ] +}) + +save_data(file_name, data) diff --git a/office__excel__openpyxl__xlwt/random_fill_color.py b/office__excel__openpyxl__xlwt/random_fill_color.py index ee7dba5f8..892a93926 100644 --- a/office__excel__openpyxl__xlwt/random_fill_color.py +++ b/office__excel__openpyxl__xlwt/random_fill_color.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random import openpyxl from openpyxl.styles import PatternFill +from openpyxl.worksheet.worksheet import Worksheet # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8b69f96d0bd87c10f3d0779ff46b02558c4cca84/excel__openpyxl__xlwt/xlsx__set_row_column_size.py -def set_row_column_size(ws): +def set_row_column_size(ws: Worksheet) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -20,11 +21,11 @@ def set_row_column_size(ws): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1c2274804f52004b07601ece535ec74b3f82aa8d/get_random_hex_color.py -def get_random_hex_color(): - return ''.join(random.choices('0123456789ABCDEF', k=6)) +def get_random_hex_color() -> str: + return "".join(random.choices("0123456789ABCDEF", k=6)) -def set_fill_color(ws): +def set_fill_color(ws: Worksheet) -> None: size = 250 for i in range(1, size + 1): @@ -34,7 +35,7 @@ def set_fill_color(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active # Масштаб 10% ws.sheet_view.zoomScale = 10 @@ -42,4 +43,4 @@ def set_fill_color(ws): set_row_column_size(ws) set_fill_color(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/set_cell_color.py b/office__excel__openpyxl__xlwt/set_cell_color.py index cbdb67450..4dd7a2080 100644 --- a/office__excel__openpyxl__xlwt/set_cell_color.py +++ b/office__excel__openpyxl__xlwt/set_cell_color.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl @@ -9,10 +9,10 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active -ws.cell(row=1, column=1).fill = PatternFill(fgColor='FF0000', fill_type="solid") -ws.cell(row=2, column=1).fill = PatternFill(fgColor='00FF00', fill_type="solid") -ws['A3'].fill = PatternFill(fgColor='0000FF', fill_type="solid") +ws.cell(row=1, column=1).fill = PatternFill(fgColor="FF0000", fill_type="solid") +ws.cell(row=2, column=1).fill = PatternFill(fgColor="00FF00", fill_type="solid") +ws["A3"].fill = PatternFill(fgColor="0000FF", fill_type="solid") -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py b/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py index 26a66be22..360aed216 100644 --- a/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py +++ b/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install xlwt import xlwt -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] wb = xlwt.Workbook() -ws = wb.add_sheet('Students') +ws = wb.add_sheet("Students") for i, column in enumerate(columns): ws.write(0, i, column) @@ -26,4 +26,4 @@ for j, data in enumerate(row): ws.write(i, j, data) -wb.save('excel.xls') +wb.save("excel.xls") diff --git a/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py b/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py index c1e4c5f9e..651585216 100644 --- a/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py +++ b/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py @@ -1,33 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl +from openpyxl.worksheet.worksheet import Worksheet -def set_column_size(worksheet): - dims = dict() +def set_column_size(ws: Worksheet) -> None: + dims: dict[str, int] = dict() - for row in worksheet.rows: + for row in ws.rows: for cell in row: if not cell.value: continue - dims[cell.column] = max(dims.get(cell.column, 0), len(str(cell.value))) + column = cell.column_letter + dims[column] = max(dims.get(column, 0), len(str(cell.value))) for col, value in dims.items(): - worksheet.column_dimensions[col].width = value * 1.15 # Append 15% + ws.column_dimensions[col].width = value * 1.15 # Append 15% -def fill_sheet(ws): - columns = ['Language', 'Text'] +def fill_sheet(ws: Worksheet) -> None: + columns = ["Language", "Text"] rows = [ - ['python', '-' * 10], - ['java', 'j' * 10], - ['c#', '*' * 5], - ['c++', '0' * 50], + ["python", "-" * 10], + ["java", "j" * 10], + ["c#", "*" * 5], + ["c++", "0" * 50], ] for i, value in enumerate(columns, 1): @@ -40,14 +42,14 @@ def fill_sheet(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active # Sheet 1 fill_sheet(ws) # Sheet 2 -ws = wb.create_sheet('auto') +ws = wb.create_sheet("set_column_size") fill_sheet(ws) set_column_size(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py b/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py index 9c0dfa4c3..980b13b9c 100644 --- a/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py +++ b/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py @@ -1,22 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Language', 'Text'] -rows = [ - ['python', 1], - ['java', 2], - ['c#', 3], - ['Total:', 0] -] +columns = ["Language", "Text"] +rows = [["python", 1], ["java", 2], ["c#", 3], ["Total:", 0]] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -27,6 +22,6 @@ cell.value = value # Total: -ws.cell(row=5, column=2).value = '=SUM(B2:B4)' +ws.cell(row=5, column=2).value = "=SUM(B2:B4)" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py index 3d8039aa1..8efdf89a2 100644 --- a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.parse import quote import openpyxl -columns = ['Language', 'Text', 'Hyperlink'] +columns = ["Language", "Text", "Hyperlink"] rows = [ - ['python', 'excel'], - ['java', 'excel'], - ['c#', 'excel'], + ["python", "excel"], + ["java", "excel"], + ["c#", "excel"], ] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -27,8 +27,9 @@ ws.cell(row=i, column=1).value = lang ws.cell(row=i, column=2).value = text - ws.cell(row=i, column=3).hyperlink = "https://stackoverflow.com/search?q=" + quote(lang + ' ' + text) + url = "https://stackoverflow.com/search?q=" + quote(lang + " " + text) + ws.cell(row=i, column=3).hyperlink = url ws.cell(row=i, column=3).value = "StackOverflow" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py new file mode 100644 index 000000000..43d24fd1f --- /dev/null +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl +from openpyxl.worksheet.hyperlink import Hyperlink + + +wb = openpyxl.Workbook() + +sheet1 = wb.active + +sheet2 = wb.create_sheet(title="Лист 2") + +cell = sheet1.cell(row=1, column=1) +cell.hyperlink = f"#'{sheet2.title}'!K20" +cell.value = "Go" +cell.style = "Hyperlink" + +cell = sheet1.cell(row=2, column=1) +cell.hyperlink = Hyperlink( + location=f"'{sheet2.title}'!K20", + ref="", +) +cell.value = "Go 2" +cell.style = "Hyperlink" + +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py new file mode 100644 index 000000000..630b1a382 --- /dev/null +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl + + +# NOTE: from xlsx__hyperlink_cell_to_sheet.py +filename = "excel.xlsx" + +wb = openpyxl.load_workbook(filename=filename) + +sheet1 = wb["Sheet"] +sheet2 = wb["Лист 2"] + +cell = sheet1.cell(row=5, column=1) +cell.hyperlink = f"#'{sheet2.title}'!B20" +cell.value = "Go 3" +cell.style = "Hyperlink" + +wb.save(filename) diff --git a/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py b/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py index 3460d7117..e1b83ff07 100644 --- a/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py +++ b/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Language', 'Text', 'Total'] +columns = ["Language", "Text", "Total"] rows = [ - ['python', 1], - ['java', 2], - ['c#', 3], + ["python", 1], + ["java", 2], + ["c#", 3], ] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -25,9 +25,9 @@ cell = ws.cell(row=i, column=j) cell.value = value -ws.merge_cells('C2:C4') +ws.merge_cells("C2:C4") # Total: -ws['C2'].value = '=SUM(B2:B4)' +ws["C2"].value = "=SUM(B2:B4)" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py b/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py index 5d39b5031..afba7b9a9 100644 --- a/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py +++ b/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install openpyxl import openpyxl -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] @@ -20,10 +20,10 @@ # Remove default sheet for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) -ws = wb.create_sheet('Students') +ws = wb.create_sheet("Students") for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -32,4 +32,4 @@ for j, value in enumerate(row, 1): ws.cell(row=i, column=j).value = value -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py b/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py index 90c0a8c98..7324565de 100644 --- a/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py +++ b/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl +from openpyxl.worksheet.worksheet import Worksheet -def set_row_column_size(ws): +def set_row_column_size(ws: Worksheet) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -16,10 +17,9 @@ def set_row_column_size(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() # Sheet 2 ws = wb.create_sheet() set_row_column_size(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py b/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py index cbc34638a..2de7489aa 100644 --- a/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py +++ b/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py @@ -1,30 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl + wb = openpyxl.Workbook() -ws = wb.get_active_sheet() -ws.title = '10' +ws = wb.active +ws.title = "10" ws.sheet_view.zoomScale = 10 -ws = wb.create_sheet('20') +ws = wb.create_sheet("20") ws.sheet_view.zoomScale = 20 -ws = wb.create_sheet('50') +ws = wb.create_sheet("50") ws.sheet_view.zoomScale = 50 -ws = wb.create_sheet('100') +ws = wb.create_sheet("100") # ws.sheet_view.zoomScale = 100 -ws = wb.create_sheet('200') +ws = wb.create_sheet("200") ws.sheet_view.zoomScale = 200 -ws = wb.create_sheet('500') +ws = wb.create_sheet("500") ws.sheet_view.zoomScale = 500 -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__word__doc_docx/counter_of_font.py b/office__word__doc_docx/counter_of_font.py new file mode 100644 index 000000000..8d61973c3 --- /dev/null +++ b/office__word__doc_docx/counter_of_font.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + +# pip install python-docx==1.1.2 +import docx +from docx.text.run import Run +from docx.text.paragraph import Paragraph + + +def get_font_name_and_size( + doc: docx.Document, + p: Paragraph, + r: Run, +) -> tuple[str | None, int | None]: + def _get_normal_size(size: int | None) -> int | None: + return size // 12700 if size else None + + try: + font_name = r.font.name + except: + font_name = None + + if not font_name: + try: + font_name = r.style.font.name + except: + pass + + if not font_name: + try: + font_name = p.style.font.name + except: + pass + + if not font_name: + try: + font_name = doc.styles["Normal"].font.name + except: + pass + + try: + font_size = r.font.size + except: + font_size = None + + if not font_size: + try: + font_size = r.style.font.size + except: + pass + + if not font_size: + try: + font_size = p.style.font.size + except: + pass + + if not font_size: + try: + font_size = doc.styles["Normal"].font.size + except: + pass + + return font_name, _get_normal_size(font_size) + + +def process(path: Path) -> None: + for file_name in path.glob("*.docx"): + print(file_name) + + try: + doc_file = docx.Document(file_name) + + name_and_size = [] + names = [] + sizes = [] + + for p in doc_file.paragraphs: + for r in p.runs: + name, size = get_font_name_and_size(doc_file, p, r) + name_and_size.append((name, size)) + names.append(name) + sizes.append(size) + + for (name, size), number in Counter(name_and_size).most_common(): + print((name, size), number) + + except Exception as e: + print(e) + + print() + + +if __name__ == "__main__": + path = Path(__file__).parent / "fill_template" + process(path) diff --git a/office__word__doc_docx/create_table__function.py b/office__word__doc_docx/create_table__function.py new file mode 100644 index 000000000..7520e7e0d --- /dev/null +++ b/office__word__doc_docx/create_table__function.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from docx import Document + + +def create_table(document, headers, rows, style="Table Grid"): + cols_number = len(headers) + + table = document.add_table(rows=1, cols=cols_number) + table.style = style + + hdr_cells = table.rows[0].cells + for i in range(cols_number): + hdr_cells[i].text = headers[i] + + for row in rows: + row_cells = table.add_row().cells + for i in range(cols_number): + row_cells[i].text = str(row[i]) + + return table + + +document = Document() + +headers = ("№ п/п", "Наименование параметра", "Единицы измерения", "Значение") +records_table1 = ( + (0, "Nan", "Nan", 0), + (1, "Первая величина", "-/-", 0), + (2, "Вторая величина", "-/-", "Базальт"), + (3, "Третья величина", "м^2/ч", 0), +) +table1 = create_table(document, headers, records_table1) + +document.add_paragraph() + +rows = [[x, x, x * x] for x in range(1, 10)] +table2 = create_table(document, ("x", "y", "x * y"), rows) + +document.save("testing.docx") diff --git a/office__word__doc_docx/fill_template/save_style.docx b/office__word__doc_docx/fill_template/save_style.docx new file mode 100644 index 000000000..1770ad8e0 Binary files /dev/null and b/office__word__doc_docx/fill_template/save_style.docx differ diff --git a/office__word__doc_docx/fill_template/save_style.py b/office__word__doc_docx/fill_template/save_style.py new file mode 100644 index 000000000..d11db0b6d --- /dev/null +++ b/office__word__doc_docx/fill_template/save_style.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt + +# pip install python-docx +import docx + + +# SOURCE: https://stackoverflow.com/a/55733040/5909792 +def docx_replace(doc, data) -> None: + paragraphs = list(doc.paragraphs) + for t in doc.tables: + for row in t.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + paragraphs.append(paragraph) + for p in paragraphs: + for key, val in data.items(): + # I'm using placeholders in the form ${PlaceholderName} + key_name = "${{{}}}".format(key) + if key_name in p.text: + inline = p.runs + # Replace strings and retain the same style. + # The text to be replaced can be split over several runs so + # search through, identify which runs need to have text replaced + # then replace the text in those identified + started = False + key_index = 0 + # found_runs is a list of (inline index, index of match, length of match) + found_runs = list() + found_all = False + replace_done = False + for i in range(len(inline)): + # case 1: found in single run so short circuit the replace + if key_name in inline[i].text and not started: + found_runs.append( + (i, inline[i].text.find(key_name), len(key_name)) + ) + text = inline[i].text.replace(key_name, str(val)) + inline[i].text = text + replace_done = True + found_all = True + break + + if key_name[key_index] not in inline[i].text and not started: + # keep looking ... + continue + + # case 2: search for partial text, find first run + if ( + key_name[key_index] in inline[i].text + and inline[i].text[-1] in key_name + and not started + ): + # check sequence + start_index = inline[i].text.find(key_name[key_index]) + check_length = len(inline[i].text) + for text_index in range(start_index, check_length): + if inline[i].text[text_index] != key_name[key_index]: + # no match so must be false positive + break + if key_index == 0: + started = True + chars_found = check_length - start_index + key_index += chars_found + found_runs.append((i, start_index, chars_found)) + if key_index != len(key_name): + continue + else: + # found all chars in key_name + found_all = True + break + + # case 2: search for partial text, find subsequent run + if ( + key_name[key_index] in inline[i].text + and started + and not found_all + ): + # check sequence + chars_found = 0 + check_length = len(inline[i].text) + for text_index in range(0, check_length): + if inline[i].text[text_index] == key_name[key_index]: + key_index += 1 + chars_found += 1 + else: + break + # no match so must be end + found_runs.append((i, 0, chars_found)) + if key_index == len(key_name): + found_all = True + break + + if found_all and not replace_done: + for i, item in enumerate(found_runs): + index, start, length = [t for t in item] + if i == 0: + text = inline[index].text.replace( + inline[index].text[start : start + length], str(val) + ) + inline[index].text = text + else: + text = inline[index].text.replace( + inline[index].text[start : start + length], "" + ) + inline[index].text = text + # print(p.text) + + +if __name__ == "__main__": + from_filename = "template.docx" + to_filename = "save_style.docx" + + REPLACING = { + "title": "My pretty title!", + "date_time": dt.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), + } + + doc = docx.Document(from_filename) + docx_replace(doc, REPLACING) + + doc.save(to_filename) diff --git a/office__word__doc_docx/fill_template/simple.docx b/office__word__doc_docx/fill_template/simple.docx new file mode 100644 index 000000000..376d64733 Binary files /dev/null and b/office__word__doc_docx/fill_template/simple.docx differ diff --git a/office__word__doc_docx/fill_template/simple.py b/office__word__doc_docx/fill_template/simple.py new file mode 100644 index 000000000..86fd52b63 --- /dev/null +++ b/office__word__doc_docx/fill_template/simple.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt + +# pip install python-docx +import docx + + +from_filename = "template.docx" +to_filename = "simple.docx" + + +REPLACING = { + "${title}": "My pretty title!", + "${date_time}": dt.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), +} + +doc = docx.Document(from_filename) +for p in doc.paragraphs: + for k, v in REPLACING.items(): + if k in p.text: + new_text = p.text.replace(k, v) + p.text = new_text + +doc.save(to_filename) diff --git a/office__word__doc_docx/fill_template/template.docx b/office__word__doc_docx/fill_template/template.docx new file mode 100644 index 000000000..d3f17b034 Binary files /dev/null and b/office__word__doc_docx/fill_template/template.docx differ diff --git a/office__word__doc_docx/hello_world/main.py b/office__word__doc_docx/hello_world/main.py index e9b51843e..866e84d79 100644 --- a/office__word__doc_docx/hello_world/main.py +++ b/office__word__doc_docx/hello_world/main.py @@ -1,47 +1,84 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/python-openxml/python-docx # SOURCE: https://python-docx.readthedocs.io/en/latest/ +import os + # pip install python-docx import docx from docx.shared import Inches, Cm, Mm document = docx.Document() -document.add_heading('Document Title', level=0) +document.add_heading("Document Title", level=0) -p = document.add_paragraph('A plain paragraph having some ') -p.add_run('bold').bold = True -p.add_run(' and some ') -p.add_run('italic.').italic = True +p = document.add_paragraph("A plain paragraph having some ") +p.add_run("bold").bold = True +p.add_run(" and some ") +p.add_run("italic.").italic = True -document.add_heading('Heading, level 1', level=1) -document.add_paragraph('Intense quote', style='Intense Quote') -document.add_paragraph('first item in unordered list', style='List Bullet') -document.add_paragraph('first item in ordered list', style='List Number') +document.add_heading("Heading, level 1", level=1) +document.add_paragraph("Intense quote", style="Intense Quote") +document.add_paragraph("first item in unordered list", style="List Bullet") +document.add_paragraph("first item in ordered list", style="List Number") document.add_page_break() # Table -document.add_heading('Table', level=1) +document.add_heading("Table", level=1) -headers = ['id', 'url', 'name', 'short_name', 'birthday', 'job'] +headers = ["id", "url", "name", "short_name", "birthday", "job"] rows = [ - ['#1', 'http://amiller.example.com', 'Andrew Miller', 'amiller', '11 December', 'Testing Engineer'], - ['#2', 'http://ataylor.example.com', 'Anthony Taylor', 'ataylor', '17 July', 'Software Engineer'], - ['#3', 'http://dmoore.example.com', 'Daniel Moore', 'dmoore', '2 March', 'Testing Engineer'], - ['#4', 'http://dsmith.example.com', 'David Smith', 'dsmith', '5 January', 'Testing Engineer'], - ['#5', 'http://awilson.example.com', 'Alexander Wilson', 'awilson', '11 April', 'Software Engineer'] + [ + "#1", + "http://amiller.example.com", + "Andrew Miller", + "amiller", + "11 December", + "Testing Engineer", + ], + [ + "#2", + "http://ataylor.example.com", + "Anthony Taylor", + "ataylor", + "17 July", + "Software Engineer", + ], + [ + "#3", + "http://dmoore.example.com", + "Daniel Moore", + "dmoore", + "2 March", + "Testing Engineer", + ], + [ + "#4", + "http://dsmith.example.com", + "David Smith", + "dsmith", + "5 January", + "Testing Engineer", + ], + [ + "#5", + "http://awilson.example.com", + "Alexander Wilson", + "awilson", + "11 April", + "Software Engineer", + ], ] # Без style='Table Grid' у таблицы не будет выделена решетка -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -59,28 +96,27 @@ document.add_page_break() # Image -file_name_image = 'image.jpg' +file_name_image = "image.jpg" -document.add_heading('Image', level=1) +document.add_heading("Image", level=1) -document.add_heading('image__original', level=2) +document.add_heading("image__original", level=2) document.add_picture(file_name_image) -document.add_heading('image__Inches_3', level=2) +document.add_heading("image__Inches_3", level=2) document.add_picture(file_name_image, width=Inches(3.0)) -document.add_heading('image__millimeters_30', level=2) +document.add_heading("image__millimeters_30", level=2) document.add_picture(file_name_image, width=Mm(30)) -document.add_heading('image__centimeters_5', level=2) +document.add_heading("image__centimeters_5", level=2) document.add_picture(file_name_image, width=Cm(5)) document.add_page_break() # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/hyperlink.py b/office__word__doc_docx/hyperlink.py index 663272f20..7fe12fe83 100644 --- a/office__word__doc_docx/hyperlink.py +++ b/office__word__doc_docx/hyperlink.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install python-docx @@ -9,7 +9,7 @@ # SOURCE: https://github.com/python-openxml/python-docx/issues/74#issuecomment-261169410 -def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): +def add_hyperlink(paragraph, url, text, color="1111FF", underline=True): """ A function that places a hyperlink within a paragraph object. @@ -23,32 +23,37 @@ def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): # This gets access to the document.xml.rels file and gets a new relation id value part = paragraph.part - r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True) + r_id = part.relate_to( + url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True + ) # Create the w:hyperlink tag and add needed values - hyperlink = docx.oxml.shared.OxmlElement('w:hyperlink') - hyperlink.set(docx.oxml.shared.qn('r:id'), r_id, ) + hyperlink = docx.oxml.shared.OxmlElement("w:hyperlink") + hyperlink.set( + docx.oxml.shared.qn("r:id"), + r_id, + ) # Create a w:r element - new_run = docx.oxml.shared.OxmlElement('w:r') + new_run = docx.oxml.shared.OxmlElement("w:r") # Create a new w:rPr element - rPr = docx.oxml.shared.OxmlElement('w:rPr') + rPr = docx.oxml.shared.OxmlElement("w:rPr") # Add color if it is given if not color is None: - c = docx.oxml.shared.OxmlElement('w:color') - c.set(docx.oxml.shared.qn('w:val'), color) - rPr.append(c) + c = docx.oxml.shared.OxmlElement("w:color") + c.set(docx.oxml.shared.qn("w:val"), color) + rPr.append(c) # Remove underlining if it is requested if not underline: - u = docx.oxml.shared.OxmlElement('w:u') - u.set(docx.oxml.shared.qn('w:val'), 'none') - rPr.append(u) + u = docx.oxml.shared.OxmlElement("w:u") + u.set(docx.oxml.shared.qn("w:val"), "none") + rPr.append(u) - u = docx.oxml.shared.OxmlElement('w:u') - u.set(docx.oxml.shared.qn('w:val'), 'single') + u = docx.oxml.shared.OxmlElement("w:u") + u.set(docx.oxml.shared.qn("w:val"), "single") rPr.append(u) # Join all the xml elements together add add the required text to the w:r element @@ -61,36 +66,37 @@ def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): return hyperlink -if __name__ == '__main__': +if __name__ == "__main__": document = docx.Document() - document.add_heading('Hyperlink', level=0) + document.add_heading("Hyperlink", level=0) - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google") - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', underline=False) + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", underline=False) - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', color='00FF00') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", color="00FF00") - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', color='FF8822') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", color="FF8822") # Empty line document.add_paragraph() - p = document.add_paragraph('Hello World! -> ') - add_hyperlink(p, 'https://google.ru/search?q=Hello', 'Hello') - p.add_run(' ') - add_hyperlink(p, 'https://google.ru/search?q=World', 'World') - p.add_run(' -> ') - add_hyperlink(p, 'https://google.ru/search?q=Hello World!', 'Hello World!') + p = document.add_paragraph("Hello World! -> ") + add_hyperlink(p, "https://google.ru/search?q=Hello", "Hello") + p.add_run(" ") + add_hyperlink(p, "https://google.ru/search?q=World", "World") + p.add_run(" -> ") + add_hyperlink(p, "https://google.ru/search?q=Hello World!", "Hello World!") # Save - file_name_doc = 'word.docx' + file_name_doc = "word.docx" document.save(file_name_doc) # Open file import os + os.startfile(file_name_doc) diff --git a/office__word__doc_docx/read_docx/read_docx.py b/office__word__doc_docx/read_docx/read_docx.py index 83e6bfa5b..f517e961d 100644 --- a/office__word__doc_docx/read_docx/read_docx.py +++ b/office__word__doc_docx/read_docx/read_docx.py @@ -1,20 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import re + # pip install python-docx from docx import Document + + document = Document("Обеденное меню 777.docx") # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') +multi_space_pattern = re.compile(r" {2,}") for table in document.tables: for row in table.rows: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) for i in row.cells + ] if name == weight == price or (not weight or not price): print() @@ -22,7 +27,7 @@ print(name) continue - print('{} {} {}'.format(name, weight, price)) + print(f"{name} {weight} {price}") # Таблицы в меню дублируются break diff --git a/office__word__doc_docx/table__hyperlink_cell.py b/office__word__doc_docx/table__hyperlink_cell.py index c27c8a51c..6448fec24 100644 --- a/office__word__doc_docx/table__hyperlink_cell.py +++ b/office__word__doc_docx/table__hyperlink_cell.py @@ -1,33 +1,65 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx from hyperlink import add_hyperlink -headers = ('NAME', 'DESCRIPTION') +headers = ("NAME", "DESCRIPTION") rows = [ - ('php', 'PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.'), - ('javascript', 'JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.'), - ('java', 'Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.'), - ('android', 'Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].'), - ('c#', 'C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.'), - ('html', 'Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. '), - ('c++', 'C++ — язык программирования общего назначения, синтаксис которого основан на языке C.'), - ('jquery', 'Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].'), - ('css', 'CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.'), - ('mysql', 'Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].') + ( + "php", + "PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.", + ), + ( + "javascript", + "JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.", + ), + ( + "java", + "Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.", + ), + ( + "android", + "Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].", + ), + ( + "c#", + "C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.", + ), + ( + "html", + "Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. ", + ), + ( + "c++", + "C++ — язык программирования общего назначения, синтаксис которого основан на языке C.", + ), + ( + "jquery", + "Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].", + ), + ( + "css", + "CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.", + ), + ( + "mysql", + "Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].", + ), ] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -42,13 +74,12 @@ p = cells[0].paragraphs[0] - add_hyperlink(p, "https://ru.stackoverflow.com/tags/{}/info".format(name), name) + add_hyperlink(p, f"https://ru.stackoverflow.com/tags/{name}/info", name) cells[1].text = description # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__merge_cells.py b/office__word__doc_docx/table__merge_cells.py index 6e5e3a969..ff316e499 100644 --- a/office__word__doc_docx/table__merge_cells.py +++ b/office__word__doc_docx/table__merge_cells.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx @@ -11,14 +13,15 @@ COL_COUNT = 5 ROW_COUNT = 5 -HEADERS = ['TABLE HEADER', '', '', '', ''] +HEADERS = ["TABLE HEADER", "", "", "", ""] ROWS = [ - ['{}x{}'.format(i, j) for j in range(1, COL_COUNT + 1)] for i in range(1, ROW_COUNT + 1) + [f"{i}x{j}" for j in range(1, COL_COUNT + 1)] + for i in range(1, ROW_COUNT + 1) ] def fill_table(document, headers, rows): - table = document.add_table(rows=1, cols=len(headers), style='Table Grid') + table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -54,9 +57,8 @@ def fill_table(document, headers, rows): table_2.cell(2, 2).merge(table_2.cell(2, 4)) # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__set_size_column.py b/office__word__doc_docx/table__set_size_column.py index 663e61079..26a28347f 100644 --- a/office__word__doc_docx/table__set_size_column.py +++ b/office__word__doc_docx/table__set_size_column.py @@ -1,32 +1,64 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx from docx.shared import Cm -headers = ('NAME', 'DESCRIPTION') +headers = ("NAME", "DESCRIPTION") rows = [ - ('php', 'PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.'), - ('javascript', 'JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.'), - ('java', 'Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.'), - ('android', 'Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].'), - ('c#', 'C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.'), - ('html', 'Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. '), - ('c++', 'C++ — язык программирования общего назначения, синтаксис которого основан на языке C.'), - ('jquery', 'Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].'), - ('css', 'CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.'), - ('mysql', 'Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].') + ( + "php", + "PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.", + ), + ( + "javascript", + "JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.", + ), + ( + "java", + "Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.", + ), + ( + "android", + "Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].", + ), + ( + "c#", + "C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.", + ), + ( + "html", + "Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. ", + ), + ( + "c++", + "C++ — язык программирования общего назначения, синтаксис которого основан на языке C.", + ), + ( + "jquery", + "Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].", + ), + ( + "css", + "CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.", + ), + ( + "mysql", + "Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].", + ), ] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -51,9 +83,8 @@ cell.width = size # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__split_cells.py b/office__word__doc_docx/table__split_cells.py index 2e7f44901..841757774 100644 --- a/office__word__doc_docx/table__split_cells.py +++ b/office__word__doc_docx/table__split_cells.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx -headers = ['VALUE'] -rows = [ - [''], - [''] -] +headers = ["VALUE"] +rows = [[""], [""]] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -34,21 +33,20 @@ # Row 1 sub_table = table.cell(1, 0).add_table(rows=1, cols=3) -sub_table.style = 'Table Grid' +sub_table.style = "Table Grid" for i, col in enumerate(sub_table.row_cells(0), 1): - col.text = str(2 ** i) + col.text = str(2**i) # Row 2 sub_table = table.cell(2, 0).add_table(rows=1, cols=3) for i, col in enumerate(sub_table.row_cells(0), 1): - col.text = str(2 ** i) + col.text = str(2**i) # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/online_anidub_com/get_video_list.py b/online_anidub_com/get_video_list.py index 72ed6fe03..5c29ae269 100644 --- a/online_anidub_com/get_video_list.py +++ b/online_anidub_com/get_video_list.py @@ -1,20 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List import sys -from pathlib import Path import re +from pathlib import Path + from bs4 import BeautifulSoup -DIR = Path(__file__).resolve().parent -sys.path.append(str(DIR.parent / 'proxy')) +ROOT_DIR = Path(__file__).resolve().parent + + +sys.path.append(str(ROOT_DIR.parent / "using_proxy")) import proxy_requests__upgraded + ProxyRequests = proxy_requests__upgraded.ProxyRequests @@ -24,28 +27,28 @@ def _get_title(el) -> str: title = el.get_text(strip=True) - return re.sub(r'\s{2,}', ' ', title) + return re.sub(r"\s{2,}", " ", title) -def search_video_list(text) -> List[str]: - url = 'https://online.anidub.com/index.php?do=search' +def search_video_list(text: str) -> list[str]: + url = "https://online.anidub.com/index.php?do=search" data = { - 'do': 'search', - 'subaction': 'search', - 'search_start': '0', - 'full_search': '0', - 'result_from': '1', - 'story': text, + "do": "search", + "subaction": "search", + "search_start": "0", + "full_search": "0", + "result_from": "1", + "story": text, } headers = { - 'Host': 'online.anidub.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate', - 'Origin': 'https://online.anidub.com', - 'Referer': 'https://online.anidub.com/index.php?do=search', + "Host": "online.anidub.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "gzip, deflate", + "Origin": "https://online.anidub.com", + "Referer": "https://online.anidub.com/index.php?do=search", } while True: @@ -54,24 +57,33 @@ def search_video_list(text) -> List[str]: content = rs.content - if b'Attention Required! | Cloudflare' in content \ - or b'Access denied | online.anidub.com used Cloudflare to restrict access' in content: - DEBUG_LOG and print('Fail! Cloudflare!') + if ( + b"Attention Required! | Cloudflare" in content + or b"Access denied | online.anidub.com used Cloudflare to restrict access" + in content + ): + DEBUG_LOG and print("Fail! Cloudflare!") continue break - root = BeautifulSoup(content, 'html.parser') - return [_get_title(a) for a in root.select('.th-title')] + root = BeautifulSoup(content, "html.parser") + return [_get_title(a) for a in root.select(".th-title")] -if __name__ == '__main__': - text = 'Моя геройская академия' +if __name__ == "__main__": + text = "Моя геройская академия" items = search_video_list(text) - print(f'Items ({len(items)}):') + print(f"Items ({len(items)}):") for x in items: - print(f' {x}') + print(f" {x}") import json - json.dump(items, open('video_list.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=4) + + json.dump( + items, + open("video_list.json", "w", encoding="utf-8"), + ensure_ascii=False, + indent=4, + ) diff --git a/open_tab_webbrowser/open_tab_webbrowser.py b/open_tab_webbrowser/open_tab_webbrowser.py index 5926507d2..da2a80285 100644 --- a/open_tab_webbrowser/open_tab_webbrowser.py +++ b/open_tab_webbrowser/open_tab_webbrowser.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример открытия вкладки в браузере.""" -if __name__ == '__main__': +if __name__ == "__main__": import webbrowser # Вернет True и откроет вкладку - webbrowser.open_new_tab("https://github.com/gil9red") \ No newline at end of file + webbrowser.open_new_tab("https://github.com/gil9red") diff --git a/opencv (cv2) vs pil (pillow)/main.py b/opencv (cv2) vs pil (pillow)/main.py index 4f5e51d98..9b80cc660 100644 --- a/opencv (cv2) vs pil (pillow)/main.py +++ b/opencv (cv2) vs pil (pillow)/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python @@ -9,93 +9,148 @@ import timeit -FILE_NAME = 'input.jpg' +FILE_NAME = "input.jpg" NUMBER = 100 -GLOBALS = { - 'FILE_NAME': FILE_NAME -} +GLOBALS = {"FILE_NAME": FILE_NAME} -print('Read image from file:') -print('PIL', - timeit.timeit("img = Image.open(FILE_NAME)", setup="from PIL import Image", number=NUMBER, globals=GLOBALS) +print("Read image from file:") +print( + "PIL", + timeit.timeit( + "img = Image.open(FILE_NAME)", + setup="from PIL import Image", + number=NUMBER, + globals=GLOBALS, + ), ) -print('CV2', - timeit.timeit("img = cv2.imread(FILE_NAME)", setup="import cv2", number=NUMBER, globals=GLOBALS) +print( + "CV2", + timeit.timeit( + "img = cv2.imread(FILE_NAME)", + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), ) print() -print('Read image from file and get image bytes:') -print('PIL', timeit.timeit( - """\ +print("Read image from file and get image bytes:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) bytes_io = io.BytesIO() img.save(bytes_io, format='JPEG') img_data = bytes_io.getvalue() """, - setup="from PIL import Image\nimport io", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image\nimport io", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_data = cv2.imencode('.jpg', img)[1].tostring() """, - setup="import cv2\nimport io", number=NUMBER, globals=GLOBALS -)) + setup="import cv2\nimport io", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('Invert:') -print('PIL', timeit.timeit( - """\ +print("Invert:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_invert = ImageOps.invert(img) """, - setup="from PIL import Image, ImageOps", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image, ImageOps", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_invert = cv2.bitwise_not(img) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('Grayscale:') -print('PIL ', timeit.timeit( - """\ +print("Grayscale:") +print( + "PIL ", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_gray = ImageOps.grayscale(img) """, - setup="from PIL import Image, ImageOps", number=NUMBER, globals=GLOBALS -)) -print('CV2_1', timeit.timeit( - """\ + setup="from PIL import Image, ImageOps", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2_1", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) -print('CV2_2', timeit.timeit( - """\ + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2_2", + timeit.timeit( + """\ img_gray = cv2.imread(FILE_NAME, cv2.IMREAD_GRAYSCALE) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('GaussianBlur:') -print('PIL', timeit.timeit( - """\ +print("GaussianBlur:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_blur = img.filter(ImageFilter.GaussianBlur(2)) """, - setup="from PIL import Image, ImageFilter", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image, ImageFilter", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_blur = cv2.GaussianBlur(img, (15, 15), 0) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) -print() \ No newline at end of file + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) +print() diff --git a/opencv__Color_Detection_Tool/main.py b/opencv__Color_Detection_Tool/main.py index a96de84d0..2c70c1616 100644 --- a/opencv__Color_Detection_Tool/main.py +++ b/opencv__Color_Detection_Tool/main.py @@ -1,75 +1,109 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://itnotesblog.ru/note.php?id=272#sthash.e5tCuHk0.dpbs -from PyQt5 import Qt +import sys +import traceback + +from PyQt5.QtCore import Qt, QFileInfo, QSettings +from PyQt5.QtGui import QImage, qRgb, QColor, QPixmap, QPalette, QPainter, QPen +from PyQt5.QtWidgets import ( + QWidget, + QMessageBox, + QApplication, + QSlider, + QSpinBox, + QFileDialog, + QColorDialog, + QComboBox, + QRadioButton, + QDoubleSpinBox, + QCheckBox, +) from PyQt5 import uic # pip install opencv-python import cv2 + import numpy as np -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions CONFIG_FILE_NAME = "config.ini" -WINDOW_TITLE = 'Color Detection Tool' -GRAY_COLOR_TABLE = [Qt.qRgb(i, i, i) for i in range(256)] +WINDOW_TITLE = "Color Detection Tool" +GRAY_COLOR_TABLE = [qRgb(i, i, i) for i in range(256)] -def numpy_array_to_QImage(numpy_array): +def numpy_array_to_QImage(numpy_array: np.ndarray) -> QImage | None: if numpy_array.dtype != np.uint8: return height, width = numpy_array.shape[:2] if len(numpy_array.shape) == 2: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_Indexed8) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_Indexed8, + ) img.setColorTable(GRAY_COLOR_TABLE) return img elif len(numpy_array.shape) == 3: if numpy_array.shape[2] == 3: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_RGB888) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_RGB888, + ) return img elif numpy_array.shape[2] == 4: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_ARGB32) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_ARGB32, + ) return img -class MainWindow(Qt.QWidget): - def __init__(self): +class MainWindow(QWidget): + def __init__(self) -> None: super().__init__() - uic.loadUi('mainwidget.ui', self) + uic.loadUi("mainwidget.ui", self) self.setWindowTitle(WINDOW_TITLE) - self.cbPenStyle.addItem('Solid', Qt.Qt.SolidLine) - self.cbPenStyle.addItem('Dash', Qt.Qt.DashLine) - self.cbPenStyle.addItem('Dot', Qt.Qt.DotLine) - self.cbPenStyle.addItem('Dash Dot', Qt.Qt.DashDotLine) - self.cbPenStyle.addItem('Dash Dot Dot', Qt.Qt.DashDotDotLine) + self.cbPenStyle.addItem("Solid", Qt.SolidLine) + self.cbPenStyle.addItem("Dash", Qt.DashLine) + self.cbPenStyle.addItem("Dot", Qt.DotLine) + self.cbPenStyle.addItem("Dash Dot", Qt.DashDotLine) + self.cbPenStyle.addItem("Dash Dot Dot", Qt.DashDotDotLine) - self.pen_color = Qt.QColor(Qt.Qt.green) + self.pen_color = QColor(Qt.green) self.last_load_path = "." self.image_source = None @@ -77,11 +111,11 @@ def __init__(self): self.load_settings() - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): w.sliderMoved.connect(self.refresh_HSV) name = w.objectName() - sp = self.findChild(Qt.QSpinBox, "sp" + name[2:]) + sp = self.findChild(QSpinBox, "sp" + name[2:]) if not sp: continue @@ -108,51 +142,55 @@ def __init__(self): def on_load(self): image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" - file_name = Qt.QFileDialog.getOpenFileName(self, "Load image", self.last_load_path, image_filters)[0] + file_name = QFileDialog.getOpenFileName( + self, "Load image", self.last_load_path, image_filters + )[0] if not file_name: return - self.last_load_path = Qt.QFileInfo(file_name).absolutePath() + self.last_load_path = QFileInfo(file_name).absolutePath() # Load image as bytes - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: img_data = f.read() nparr = np.frombuffer(img_data, np.uint8) self.image_source = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) height, width, channels = self.image_source.shape - self.setWindowTitle(WINDOW_TITLE + '. {}x{} ({} channels). {}'.format(width, height, channels, file_name)) + self.setWindowTitle( + f"{WINDOW_TITLE}. {width}x{height} ({channels} channels). {file_name}" + ) # Трансформация BGR->RGB если 3 канала. У картинок с прозрачностью каналов 4 и для них почему if channels == 3: code = cv2.COLOR_BGR2RGB - elif channels == 4: code = cv2.COLOR_BGRA2RGB - else: - raise Exception('Unexpected number of channels: {}'.format(channels)) + raise Exception(f"Unexpected number of channels: {channels}") self.image_source = cv2.cvtColor(self.image_source, code) self.refresh_HSV() - def show_result(self): + def show_result(self) -> None: if not self.result_img: return size = self.lbView.size() - pixmap = Qt.QPixmap.fromImage(self.result_img).scaled(size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation) + pixmap = QPixmap.fromImage(self.result_img).scaled( + size, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) self.lbView.setPixmap(pixmap) - def _update_pen_color(self): + def _update_pen_color(self) -> None: palette = self.pbPenColor.palette() - palette.setColor(Qt.QPalette.Button, self.pen_color) + palette.setColor(QPalette.Button, self.pen_color) self.pbPenColor.setPalette(palette) - def _choose_color(self): - color = Qt.QColorDialog.getColor(self.pen_color) + def _choose_color(self) -> None: + color = QColorDialog.getColor(self.pen_color) if not color.isValid(): return @@ -161,13 +199,13 @@ def _choose_color(self): self._update_pen_color() self.refresh_HSV() - def _draw_contours(self, result_img, contours): + def _draw_contours(self, result_img, contours) -> None: line_size = self.sbPenWidth.value() line_type = self.cbPenStyle.currentData() line_color = self.pen_color - p = Qt.QPainter(result_img) - p.setPen(Qt.QPen(line_color, line_size, line_type)) + p = QPainter(result_img) + p.setPen(QPen(line_color, line_size, line_type)) for c in contours: x, y, width, height = cv2.boundingRect(c) @@ -175,7 +213,7 @@ def _draw_contours(self, result_img, contours): p.end() - def refresh_HSV(self): + def refresh_HSV(self) -> None: hue_from = self.slHueFrom.value() hue_to = max(hue_from, self.slHueTo.value()) @@ -188,17 +226,17 @@ def refresh_HSV(self): hsv_min = hue_from, saturation_from, value_from hsv_max = hue_to, saturation_to, value_to - color_hsv_min = Qt.QColor.fromHsv(*hsv_min) - color_hsv_max = Qt.QColor.fromHsv(*hsv_max) + color_hsv_min = QColor.fromHsv(*hsv_min) + color_hsv_max = QColor.fromHsv(*hsv_max) - self.label_hsv_from_text.setText(', '.join(map(str, hsv_min))) - self.label_hsv_to_text.setText(', '.join(map(str, hsv_max))) + self.label_hsv_from_text.setText(", ".join(map(str, hsv_min))) + self.label_hsv_to_text.setText(", ".join(map(str, hsv_max))) - pixmap = Qt.QPixmap(1, 1) + pixmap = QPixmap(1, 1) pixmap.fill(color_hsv_min) self.lbHsvMin.setPixmap(pixmap) - pixmap = Qt.QPixmap(1, 1) + pixmap = QPixmap(1, 1) pixmap.fill(color_hsv_max) self.lbHsvMax.setPixmap(pixmap) @@ -215,27 +253,23 @@ def refresh_HSV(self): thresholded_image = cv2.inRange( thresholded_image, np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) + np.array(hsv_max, np.uint8), ) # Убираем шум thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Замыкаем оставшиеся крупные объекты thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) if self.rbCanny.isChecked(): @@ -243,13 +277,15 @@ def refresh_HSV(self): thresholded_image = cv2.Canny(thresholded_image, 100, 50, 5) if self.rbResult.isChecked(): - mode = cv2.RETR_EXTERNAL if self.chOnlyExternal.isChecked() else cv2.RETR_TREE + mode = ( + cv2.RETR_EXTERNAL + if self.chOnlyExternal.isChecked() + else cv2.RETR_TREE + ) # Находим контуры contours, _ = cv2.findContours( - thresholded_image, - mode, - cv2.CHAIN_APPROX_SIMPLE + thresholded_image, mode, cv2.CHAIN_APPROX_SIMPLE ) result_img = self.image_source.copy() @@ -266,43 +302,43 @@ def refresh_HSV(self): self.show_result() - def save_settings(self): - settings = Qt.QSettings(CONFIG_FILE_NAME, Qt.QSettings.IniFormat) + def save_settings(self) -> None: + settings = QSettings(CONFIG_FILE_NAME, QSettings.IniFormat) settings.setValue(self.objectName(), self.saveGeometry()) # TODO: рефакторинг циклов - for w in self.findChildren(Qt.QComboBox): + for w in self.findChildren(QComboBox): name = w.objectName() settings.setValue(name, w.currentIndex()) - for w in self.findChildren(Qt.QRadioButton): + for w in self.findChildren(QRadioButton): name = w.objectName() settings.setValue(name, int(w.isChecked())) - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QDoubleSpinBox): + for w in self.findChildren(QDoubleSpinBox): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QSpinBox): + for w in self.findChildren(QSpinBox): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QCheckBox): + for w in self.findChildren(QCheckBox): name = w.objectName() settings.setValue(name, int(w.isChecked())) - settings.setValue('PenColor', self.pen_color.name()) + settings.setValue("PenColor", self.pen_color.name()) settings.setValue("lastLoadPath", self.last_load_path) - def load_settings(self): - settings = Qt.QSettings(CONFIG_FILE_NAME, Qt.QSettings.IniFormat) + def load_settings(self) -> None: + settings = QSettings(CONFIG_FILE_NAME, QSettings.IniFormat) geometry = settings.value(self.objectName()) if geometry: @@ -310,59 +346,59 @@ def load_settings(self): self.last_load_path = settings.value("lastLoadPath", ".") - self.pen_color = Qt.QColor(settings.value('PenColor', Qt.Qt.green)) + self.pen_color = QColor(settings.value("PenColor", Qt.green)) # TODO: рефакторинг циклов - for w in self.findChildren(Qt.QComboBox): + for w in self.findChildren(QComboBox): name = w.objectName() value = int(settings.value(name, w.currentIndex())) w.setCurrentIndex(value) - for w in self.findChildren(Qt.QRadioButton): + for w in self.findChildren(QRadioButton): name = w.objectName() value = bool(int(settings.value(name, w.isChecked()))) w.setChecked(value) - for w in self.findChildren(Qt.QSpinBox): + for w in self.findChildren(QSpinBox): name = w.objectName() value = int(settings.value(name, w.value())) w.setValue(value) - for w in self.findChildren(Qt.QDoubleSpinBox): + for w in self.findChildren(QDoubleSpinBox): name = w.objectName() value = float(settings.value(name, w.value())) w.setValue(value) - for w in self.findChildren(Qt.QCheckBox): + for w in self.findChildren(QCheckBox): name = w.objectName() value = bool(int(settings.value(name, w.isChecked()))) w.setChecked(value) - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): name = w.objectName() value = int(settings.value(name, w.value())) w.setValue(value) - sp = self.findChild(Qt.QSpinBox, "sp" + name[2:]) + sp = self.findChild(QSpinBox, "sp" + name[2:]) if sp: sp.setMinimum(w.minimum()) sp.setMaximum(w.maximum()) sp.setValue(w.value()) - def resizeEvent(self, e): + def resizeEvent(self, e) -> None: super().resizeEvent(e) self.show_result() - def closeEvent(self, e): + def closeEvent(self, e) -> None: self.save_settings() super().closeEvent(e) -if __name__ == '__main__': - app = Qt.QApplication([]) +if __name__ == "__main__": + app = QApplication([]) mw = MainWindow() mw.show() diff --git a/opencv__examples/Find center of contour/main.py b/opencv__examples/Find center of contour/main.py index bfd4ca705..cc3aaab0b 100644 --- a/opencv__examples/Find center of contour/main.py +++ b/opencv__examples/Find center of contour/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pyimagesearch.com/2016/02/01/opencv-center-of-contour/ @@ -9,8 +9,9 @@ # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) @@ -30,13 +31,21 @@ # Draw the contour and center of the shape on the image cv2.drawContours(img, [c], -1, (0, 255, 0), 2) cv2.circle(img, (cX, cY), 7, (255, 255, 255), -1) - cv2.putText(img, "center", (cX - 20, cY - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) + cv2.putText( + img, + "center", + (cX - 20, cY - 20), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) except ZeroDivisionError: pass # Show the image cv2.imshow("result", img) -cv2.imwrite('output.jpg', img) +cv2.imwrite("output.jpg", img) cv2.waitKey() diff --git a/opencv__examples/Finding books in images/main.py b/opencv__examples/Finding books in images/main.py index ae756c27e..d4dc3703a 100644 --- a/opencv__examples/Finding books in images/main.py +++ b/opencv__examples/Finding books in images/main.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://tproger.ru/translations/finding-books-python-opencv/ + # pip install opencv-python import cv2 + # Загрузка изображения image = cv2.imread("example.jpg") # cv2.imshow('image', image) @@ -56,10 +58,18 @@ cX = int(M["m10"] / M["m00"]) cY = int(M["m01"] / M["m00"]) - cv2.putText(image_result, str(total), (cX, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 2) - -print("На картинке {0} книг(и)".format(total)) + cv2.putText( + image_result, + str(total), + (cX, cY), + cv2.FONT_HERSHEY_SIMPLEX, + 1.5, + (0, 255, 0), + 2, + ) + +print(f"На картинке {total} книг(и)") cv2.imwrite("output.jpg", image_result) -cv2.imshow('image_result', image_result) +cv2.imshow("image_result", image_result) cv2.waitKey() diff --git a/opencv__examples/decode_fourcc.py b/opencv__examples/decode_fourcc.py index 3e812021f..1ec804d51 100644 --- a/opencv__examples/decode_fourcc.py +++ b/opencv__examples/decode_fourcc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def decode_fourcc(v) -> str: @@ -9,9 +9,9 @@ def decode_fourcc(v) -> str: return "".join(chr((v >> 8 * i) & 0xFF) for i in range(4)) -if __name__ == '__main__': - assert decode_fourcc(828601953) == 'avc1' +if __name__ == "__main__": + assert decode_fourcc(828601953) == "avc1" import cv2 - assert decode_fourcc(cv2.VideoWriter_fourcc(*'XVID')) == 'XVID' - assert decode_fourcc(cv2.VideoWriter_fourcc(*'avc1')) == 'avc1' + assert decode_fourcc(cv2.VideoWriter_fourcc(*"XVID")) == "XVID" + assert decode_fourcc(cv2.VideoWriter_fourcc(*"avc1")) == "avc1" diff --git a/opencv__examples/draw_overlay_current_datetime/main.py b/opencv__examples/draw_overlay_current_datetime/main.py index 6df64688c..cec6777b6 100644 --- a/opencv__examples/draw_overlay_current_datetime/main.py +++ b/opencv__examples/draw_overlay_current_datetime/main.py @@ -1,16 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt +import sys # pip install opencv-python import cv2 +import numpy -def draw_overlay(img, text: str, color_text=(255, 255, 255), max_text_height_percent=5): + +def draw_overlay( + img: numpy.ndarray, text: str, color_text=(255, 255, 255), max_text_height_percent=5 +) -> None: h, w, _ = img.shape max_text_height = (h / 100) * max_text_height_percent @@ -31,25 +36,29 @@ def draw_overlay(img, text: str, color_text=(255, 255, 255), max_text_height_per text_y = h - text_rect_h // 2 # cv2.LINE_AA -- Anti aliased line - cv2.putText(img, text, (0, text_y), font, scale, color_text, thickness, lineType=cv2.LINE_AA) + cv2.putText( + img, text, (0, text_y), font, scale, color_text, thickness, lineType=cv2.LINE_AA + ) def draw_overlay_current_datetime( - img, datetime_fmt='%d/%m/%y %H:%M:%S', - color_text=(255, 255, 255), max_text_height_percent=5 -): - text = DT.datetime.now().strftime(datetime_fmt) + img: numpy.ndarray, + datetime_fmt="%d/%m/%y %H:%M:%S", + color_text=(255, 255, 255), + max_text_height_percent=5, +) -> None: + text = dt.datetime.now().strftime(datetime_fmt) draw_overlay(img, text, color_text, max_text_height_percent) -if __name__ == '__main__': - img = cv2.imread('example.jpg') +if __name__ == "__main__": + img = cv2.imread("../gaussian_blur/example.jpg") while True: img_overlay = img.copy() draw_overlay_current_datetime(img_overlay) - cv2.imshow('With overlay', img_overlay) + cv2.imshow("With overlay", img_overlay) if cv2.waitKey(25) == 27: # Esc to quit - quit() + sys.exit() diff --git a/opencv__examples/find_objects/main.py b/opencv__examples/find_objects/main.py index 60c1bce2c..9367e2fdb 100644 --- a/opencv__examples/find_objects/main.py +++ b/opencv__examples/find_objects/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('image.png') -cv2.imshow('img', img) + + +img = cv2.imread("image.png") +cv2.imshow("img", img) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) @@ -29,7 +31,7 @@ cv2.rectangle(img_with_rect, (x, y), (x + w, y + h), (0, 0, 255)) -cv2.imshow('img_with_rect', img_with_rect) +cv2.imshow("img_with_rect", img_with_rect) # # Draw img_with_rect_rect @@ -47,7 +49,7 @@ cv2.rectangle(img_with_rect_rect, (x, y), (x + w, y + h), (0, 0, 255)) -cv2.imshow('img_with_rect_rect', img_with_rect_rect) +cv2.imshow("img_with_rect_rect", img_with_rect_rect) cv2.waitKey() cv2.destroyAllWindows() diff --git a/opencv__examples/find_objects_by_color__hsv/main.py b/opencv__examples/find_objects_by_color__hsv/main.py index c31fdc80f..aaecb44b1 100644 --- a/opencv__examples/find_objects_by_color__hsv/main.py +++ b/opencv__examples/find_objects_by_color__hsv/main.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np @@ -14,36 +15,28 @@ def find_contours(image_source_hsv, hsv_min, hsv_max): # Отфильтровываем только то, что нужно, по диапазону цветов thresholded_image = cv2.inRange( - thresholded_image, - np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) + thresholded_image, np.array(hsv_min, np.uint8), np.array(hsv_max, np.uint8) ) # Убираем шум thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Замыкаем оставшиеся крупные объекты thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Находим контуры contours, _ = cv2.findContours( - thresholded_image, - cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE + thresholded_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) return contours @@ -65,20 +58,20 @@ def draw_rect_contours(img, hsv_min, hsv_max): return img -img = cv2.imread('example.jpg') -cv2.imshow('img', img) +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Find yellow img_yellow = draw_rect_contours(img, hsv_min=(25, 113, 220), hsv_max=(66, 255, 255)) -cv2.imshow('img_yellow', img_yellow) +cv2.imshow("img_yellow", img_yellow) # Find green img_green = draw_rect_contours(img, hsv_min=(59, 128, 130), hsv_max=(99, 255, 255)) -cv2.imshow('img_green', img_green) +cv2.imshow("img_green", img_green) # Find red img_red = draw_rect_contours(img, hsv_min=(119, 159, 184), hsv_max=(179, 255, 255)) -cv2.imshow('img_red', img_red) +cv2.imshow("img_red", img_red) cv2.waitKey() cv2.destroyAllWindows() diff --git a/opencv__examples/gaussian_blur/main.py b/opencv__examples/gaussian_blur/main.py index 40fb48e5e..9f702f6cd 100644 --- a/opencv__examples/gaussian_blur/main.py +++ b/opencv__examples/gaussian_blur/main.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) img_blur = cv2.GaussianBlur(img, (5, 5), 0) -cv2.imshow('img_blur', img_blur) +cv2.imshow("img_blur", img_blur) cv2.waitKey() diff --git a/opencv__examples/gray/main.py b/opencv__examples/gray/main.py index cbc02a822..0e8403ae9 100644 --- a/opencv__examples/gray/main.py +++ b/opencv__examples/gray/main.py @@ -1,20 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Gray img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) -cv2.imshow('img_gray', img_gray) +cv2.imshow("img_gray", img_gray) -img_2 = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE) -cv2.imshow('img_gray_2', img_2) +img_2 = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE) +cv2.imshow("img_gray_2", img_2) cv2.waitKey() diff --git a/opencv__examples/hello.py b/opencv__examples/hello.py index 99bd4168b..b5f936443 100644 --- a/opencv__examples/hello.py +++ b/opencv__examples/hello.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://docs.opencv.org/3.1.0/dc/da5/tutorial_py_drawing_functions.html + # pip install opencv-python import numpy as np + import cv2 + # Create a black image img = np.zeros((512, 512, 3), np.uint8) @@ -25,7 +28,7 @@ cv2.polylines(img, [pts], True, (0, 255, 255)) font = cv2.FONT_HERSHEY_SIMPLEX -cv2.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv2.LINE_AA) +cv2.putText(img, "OpenCV", (10, 500), font, 4, (255, 255, 255), 2, cv2.LINE_AA) cv2.imshow("Image", img) cv2.waitKey(10000) diff --git a/opencv__examples/image_to_bytes/main.py b/opencv__examples/image_to_bytes/main.py index 5f75ebec3..a0be39500 100644 --- a/opencv__examples/image_to_bytes/main.py +++ b/opencv__examples/image_to_bytes/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') + +img = cv2.imread("example.jpg") # cv2.imshow('img', img) -img_data = cv2.imencode('.jpg', img)[1].tostring() +img_data = cv2.imencode(".jpg", img)[1].tostring() print(len(img_data), img_data) diff --git a/opencv__examples/invert/main.py b/opencv__examples/invert/main.py index 2e065e90f..3877c609d 100644 --- a/opencv__examples/invert/main.py +++ b/opencv__examples/invert/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Invert img_invert = cv2.bitwise_not(img) -cv2.imshow('img_invert', img_invert) -cv2.imwrite('img_invert.jpg', img_invert) +cv2.imshow("img_invert", img_invert) +cv2.imwrite("img_invert.jpg", img_invert) # Gray img_invert_gray = cv2.cvtColor(img_invert, cv2.COLOR_BGR2GRAY) -cv2.imshow('img_invert_gray', img_invert_gray) -cv2.imwrite('img_invert_gray.jpg', img_invert_gray) +cv2.imshow("img_invert_gray", img_invert_gray) +cv2.imwrite("img_invert_gray.jpg", img_invert_gray) cv2.waitKey() diff --git a/opencv__examples/pil_image__to__opencv_image.py b/opencv__examples/pil_image__to__opencv_image.py index 377fa7e01..7badecb24 100644 --- a/opencv__examples/pil_image__to__opencv_image.py +++ b/opencv__examples/pil_image__to__opencv_image.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# pip install opencv-python -# pip install Pillow - import pyautogui + +# pip install opencv-python import cv2 + import numpy + pil_image = pyautogui.screenshot(region=(200, 200, 200, 200)) print(pil_image.size) -pil_image.show('pil_image') +pil_image.show("pil_image") opencv_image = cv2.cvtColor(numpy.array(pil_image), cv2.COLOR_RGB2BGR) print(opencv_image.shape[:2]) -cv2.imshow('opencv_image', opencv_image) +cv2.imshow("opencv_image", opencv_image) cv2.waitKey() diff --git a/opencv__examples/remove_contours/main.py b/opencv__examples/remove_contours/main.py index 311fb174b..bc7de737e 100644 --- a/opencv__examples/remove_contours/main.py +++ b/opencv__examples/remove_contours/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pyimagesearch.com/2015/02/09/removing-contours-image-using-python-opencv/ @@ -9,6 +9,7 @@ # pip install opencv-python import cv2 + import numpy as np diff --git a/opencv__examples/screenshot__full.py b/opencv__examples/screenshot__full.py index 49c2f12c8..546e993c6 100644 --- a/opencv__examples/screenshot__full.py +++ b/opencv__examples/screenshot__full.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np import pyautogui + img = pyautogui.screenshot() img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) -cv2.imshow('img', img) +cv2.imshow("img", img) cv2.waitKey() diff --git a/opencv__examples/screenshot__resize.py b/opencv__examples/screenshot__resize.py index b82ddac7f..1ed1dbf02 100644 --- a/opencv__examples/screenshot__resize.py +++ b/opencv__examples/screenshot__resize.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np import pyautogui + img = pyautogui.screenshot() img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_CUBIC) -cv2.imshow('img', img) +cv2.imshow("img", img) cv2.waitKey() diff --git a/opencv__examples/screenshots_in_window.py b/opencv__examples/screenshots_in_window.py index 8c12bef7b..9c0229788 100644 --- a/opencv__examples/screenshots_in_window.py +++ b/opencv__examples/screenshots_in_window.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/926556/ import threading + import cv2 import numpy as np + from PIL import ImageGrab target_array = None -def go(): +def go() -> None: global target_array while True: if target_array is not None: @@ -28,7 +30,7 @@ def go(): break -if __name__ == '__main__': +if __name__ == "__main__": thread = threading.Thread(target=go) thread.start() diff --git a/opencv__examples/show_edges_Canny/main.py b/opencv__examples/show_edges_Canny/main.py index 2413a32d4..0c204c21a 100644 --- a/opencv__examples/show_edges_Canny/main.py +++ b/opencv__examples/show_edges_Canny/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Edges Canny edges = cv2.Canny(img, 50, 100) -cv2.imshow('Edges Canny', edges) -cv2.imwrite('edges_canny.jpg', edges) +cv2.imshow("Edges Canny", edges) +cv2.imwrite("edges_canny.jpg", edges) # Edges Canny Invert edges_invert = cv2.bitwise_not(edges) -cv2.imshow('Edges Canny Invert', edges_invert) -cv2.imwrite('edges_canny_invert.jpg', edges_invert) +cv2.imshow("Edges Canny Invert", edges_invert) +cv2.imwrite("edges_canny_invert.jpg", edges_invert) cv2.waitKey() diff --git a/opencv__examples/show_fullscreen_window.py b/opencv__examples/show_fullscreen_window.py new file mode 100644 index 000000000..ef7738d25 --- /dev/null +++ b/opencv__examples/show_fullscreen_window.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import cv2 + + +window_name = "window" +img = cv2.imread("gaussian_blur/example.jpg") + +while True: + cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN) + cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) + + cv2.imshow(window_name, img) + + if cv2.waitKey(25) == 27: # Esc to quit + sys.exit() diff --git a/opencv__examples/show_with_pyqt5.py b/opencv__examples/show_with_pyqt5.py new file mode 100644 index 000000000..b91b975a8 --- /dev/null +++ b/opencv__examples/show_with_pyqt5.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +import cv2 + +from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton, QApplication +from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtGui import QImage, QPixmap + + +class ThreadOpenCV(QThread): + changePixmap = pyqtSignal(QImage) + + def __init__(self, source) -> None: + super().__init__() + + self.source = source + + def run(self) -> None: + # SOURCE: https://stackoverflow.com/a/44404713/5909792 + cap = cv2.VideoCapture(self.source) + while True: + ret, frame = cap.read() + if ret: + # https://stackoverflow.com/a/55468544/6622587 + rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + h, w, ch = rgbImage.shape + bytesPerLine = ch * w + convertToQtFormat = QImage( + rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888 + ) + p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio) + self.changePixmap.emit(p) + + cv2.waitKey(28) + + +class Widget(QWidget): + def __init__(self) -> None: + super().__init__() + + self.label_video = QLabel() + self.label_video.setMinimumSize(600, 480) + + self.pb_play = QPushButton("Play") + self.pb_play.clicked.connect(self.playVideo) + + self.thread = ThreadOpenCV("video.mp4") + self.thread.changePixmap.connect(self.setImage) + + main_layout = QVBoxLayout() + main_layout.addWidget(self.label_video) + main_layout.addWidget(self.pb_play) + + self.setLayout(main_layout) + + def playVideo(self) -> None: + self.thread.start() + + def setImage(self, image) -> None: + self.label_video.setPixmap(QPixmap.fromImage(image)) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + mw = Widget() + mw.show() + + sys.exit(app.exec_()) diff --git a/ordered_json.py b/ordered_json.py index 017175816..5c9737043 100644 --- a/ordered_json.py +++ b/ordered_json.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/6921760/5909792 @@ -10,17 +10,18 @@ import json from collections import OrderedDict + json_data = {"foo": 1, "bar": 2, "abc": {"a": 1, "b": 2, "c": 3}} ordered_json_data = OrderedDict() -ordered_json_data['foo'] = 1 -ordered_json_data['bar'] = 2 -ordered_json_data['abc'] = OrderedDict() -ordered_json_data['abc']['a'] = 1 -ordered_json_data['abc']['b'] = 2 -ordered_json_data['abc']['c'] = 3 +ordered_json_data["foo"] = 1 +ordered_json_data["bar"] = 2 +ordered_json_data["abc"] = OrderedDict() +ordered_json_data["abc"]["a"] = 1 +ordered_json_data["abc"]["b"] = 2 +ordered_json_data["abc"]["c"] = 3 -print('Dumps:') +print("Dumps:") print(json.dumps(json_data, indent=4)) # { @@ -46,7 +47,7 @@ # } print() -print('Loads:') +print("Loads:") data = json.loads(ordered_json_str) print(json.dumps(data, indent=4)) diff --git a/pad__unpad__example.py b/pad__unpad__example.py index ce2422f06..0868339c7 100644 --- a/pad__unpad__example.py +++ b/pad__unpad__example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def pad(s: bytes, bs=8) -> bytes: @@ -14,27 +14,28 @@ def unpad(s: bytes) -> bytes: return s[:-pad_size] -if __name__ == '__main__': - data = b'Hello' +if __name__ == "__main__": + data = b"Hello" padded_data = pad(data) - print(padded_data) # b'Hello\x03\x03\x03' - print(unpad(padded_data)) # b'Hello' - print(unpad(padded_data).decode('utf-8')) # Hello + print(padded_data) # b'Hello\x03\x03\x03' + print(unpad(padded_data)) # b'Hello' + print(unpad(padded_data).decode("utf-8")) # Hello assert data == unpad(pad(data)) print() - assert b'123' == unpad(pad(b'123')) - assert b'123' * 9999 == unpad(pad(b'123' * 9999)) - assert b'11111111' == unpad(pad(b'11111111')) - assert b'abcd123' == unpad(pad(b'abcd123')) + assert b"123" == unpad(pad(b"123")) + assert b"123" * 9999 == unpad(pad(b"123" * 9999)) + assert b"11111111" == unpad(pad(b"11111111")) + assert b"abcd123" == unpad(pad(b"abcd123")) - print(unpad(b'12\x02\x02')) # b'12' - print(unpad(b'1\x01')) # b'1' + print(unpad(b"12\x02\x02")) # b'12' + print(unpad(b"1\x01")) # b'1' print() - data = 'Привет!'.encode('utf-8') + data = "Привет!".encode("utf-8") padded_data = pad(data) - print(padded_data) # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!\x03\x03\x03' + print(padded_data) + # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!\x03\x03\x03' print(unpad(padded_data)) # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!' - print(unpad(padded_data).decode('utf-8')) # Привет! + print(unpad(padded_data).decode("utf-8")) # Привет! assert data == unpad(pad(data)) diff --git a/paint with turtle/example.py b/paint with turtle/example.py index b6e92c848..d374a5370 100644 --- a/paint with turtle/example.py +++ b/paint with turtle/example.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import turtle -turtle.color('red', 'yellow') + + +turtle.color("red", "yellow") turtle.begin_fill() while True: diff --git a/pandas__examples/Analysis jira/download_jira_log.py b/pandas__examples/Analysis jira/download_jira_log.py index 6953645f8..fab52a3f5 100644 --- a/pandas__examples/Analysis jira/download_jira_log.py +++ b/pandas__examples/Analysis jira/download_jira_log.py @@ -1,94 +1,98 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -URL = 'https://jira.compassplus.ru/sr/jira.issueviews:searchrequest-xml/24381/SearchRequest-24381.xml?tempMax=1000' +import pathlib + +from datetime import datetime +from urllib.parse import urlparse + +from bs4 import BeautifulSoup + + +URL = "https://helpdesk.compassluxe.com/sr/jira.issueviews:searchrequest-xml/24381/SearchRequest-24381.xml?tempMax=1000" HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0", } # NOTE. Get : openssl pkcs12 -nodes -out key.pem -in file.p12 -PEM_FILE_NAME = 'ipetrash.pem' +PEM_FILE_NAME = "ipetrash.pem" -FILE_NAME = 'jira_log__clear.xml' -FILE_NAME_FULL = 'jira_log.xml' +FILE_NAME = "jira_log__clear.xml" +FILE_NAME_FULL = "jira_log.xml" def generate_file_name(url: str) -> str: """ 'https://jira.foobar.ru/sr/jira.issueviews:searchrequest-xml/24381/SearchRequest-24381.xml?tempMax=1000' -> - 'SearchRequest-24381.xml__tempMax=1000__07112017.xml' + 'SearchRequest-24381.xml__tempMax=1000__2017-11-07.xml' :param url: :return: """ - from urllib.parse import urlparse result = urlparse(url) - - import pathlib path = pathlib.Path(result.path) + current_date_str = datetime.now().date().strftime("%Y-%m-%d") - from datetime import datetime - current_date_str = datetime.now().date().strftime('%d%m%Y') - - return '{}__{}__{}.xml'.format(path.name, result.query, current_date_str) + return f"{path.name}__{result.query}__{current_date_str}.xml" def clear_jira_xml(xml_content: bytes) -> bytes: - xml_content = xml_content\ - .replace(b'TranzAxis', b'FooBar')\ - .replace(b'compassplus', b'foobar')\ - .replace(b'Compass Plus', b'Foo Bar') + xml_content = ( + xml_content.replace(b"TranzAxis", b"FooBar") + .replace(b"compassplus", b"foobar") + .replace(b"Compass Plus", b"Foo Bar") + ) - from bs4 import BeautifulSoup - root = BeautifulSoup(xml_content, 'html.parser') + root = BeautifulSoup(xml_content, "html.parser") - def mask_tag_content(css_selector, attr=None): + def mask_tag_content(css_selector, attr=None) -> None: for x in root.select(css_selector): - x.string = '...' + x.string = "..." if attr: - x[attr] = '...' + x[attr] = "..." - def delete_tag(css_selector): + def delete_tag(css_selector) -> None: for x in root.select(css_selector): x.decompose() - delete_tag('comments > comment') - delete_tag('attachments > attachment') - delete_tag('customfields') - delete_tag('issuelinks') - delete_tag('subtasks') - - mask_tag_content('[username]', 'username') - mask_tag_content('item > title') - mask_tag_content('item > summary') - mask_tag_content('item > description') - mask_tag_content('item > link') - mask_tag_content('item > key') - mask_tag_content('item > version') - mask_tag_content('item > fixversion') - mask_tag_content('item > component') - mask_tag_content('item > environment') - mask_tag_content('item > project', 'key') + delete_tag("comments > comment") + delete_tag("attachments > attachment") + delete_tag("customfields") + delete_tag("issuelinks") + delete_tag("subtasks") + + mask_tag_content("[username]", "username") + mask_tag_content("item > title") + mask_tag_content("item > summary") + mask_tag_content("item > description") + mask_tag_content("item > link") + mask_tag_content("item > key") + mask_tag_content("item > version") + mask_tag_content("item > fixversion") + mask_tag_content("item > component") + mask_tag_content("item > environment") + mask_tag_content("item > project", "key") # return root.prettify(encoding='utf-8') - return str(root).encode('utf-8') + return str(root).encode("utf-8") -if __name__ == '__main__': +if __name__ == "__main__": import requests + rs = requests.get(URL, headers=HEADERS, cert=PEM_FILE_NAME) print(rs) print(len(rs.text), repr(rs.text[:50])) # file_name = generate_file_name(URL) - with open(FILE_NAME_FULL, mode='wb') as f: + with open(FILE_NAME_FULL, mode="wb") as f: f.write(rs.content) - with open(FILE_NAME, mode='wb') as f: + with open(FILE_NAME, mode="wb") as f: f.write(clear_jira_xml(rs.content)) diff --git a/pandas__examples/Analysis jira/main.py b/pandas__examples/Analysis jira/main.py index 4591e2b11..39207e39c 100644 --- a/pandas__examples/Analysis jira/main.py +++ b/pandas__examples/Analysis jira/main.py @@ -1,21 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime -from download_jira_log import FILE_NAME -data_file = FILE_NAME +import matplotlib.pyplot as plt +from matplotlib.pyplot import FormatStrFormatter + +import pandas as pd from bs4 import BeautifulSoup -root = BeautifulSoup(open(data_file, mode='rb'), 'lxml') + +from download_jira_log import FILE_NAME + + +data_file = FILE_NAME +root = BeautifulSoup(open(data_file, mode="rb"), "lxml") records = [] for resolved in root.select("item > resolved"): # date local is en_US - resolved_date_time = datetime.strptime(resolved.text.strip(), '%a, %d %b %Y %H:%M:%S %z') + resolved_date_time = datetime.strptime( + resolved.text.strip(), "%a, %d %b %Y %H:%M:%S %z" + ) resolved_year = resolved_date_time.year resolved_year_month = datetime(resolved_date_time.year, resolved_date_time.month, 1) @@ -23,39 +32,39 @@ records.sort(key=lambda x: x[0]) -import pandas as pd -df = pd.DataFrame(data=records, columns=['resolved_date_time', 'resolved_year', 'resolved_year_month']) +df = pd.DataFrame( + data=records, columns=["resolved_date_time", "resolved_year", "resolved_year_month"] +) print(df) -print('Total rows:', len(df)) +print("Total rows:", len(df)) -df_month = pd.DataFrame({'count': df.groupby("resolved_year_month").size()}).reset_index() +df_month = pd.DataFrame( + {"count": df.groupby("resolved_year_month").size()} +).reset_index() print(df_month) print() -df_year = pd.DataFrame({'count': df.groupby("resolved_year").size()}).reset_index() +df_year = pd.DataFrame({"count": df.groupby("resolved_year").size()}).reset_index() print(df_year) -import matplotlib.pyplot as plt -from matplotlib.pyplot import FormatStrFormatter - fig = plt.figure(1) -fig.suptitle('Analysis jira', fontsize=14, fontweight='bold') +fig.suptitle("Analysis jira", fontsize=14, fontweight="bold") ax1 = fig.add_subplot(121) -ax1.plot(df_month['resolved_year_month'], df_month['count']) +ax1.plot(df_month["resolved_year_month"], df_month["count"]) ax1.grid() -ax1.set_title('Jira by month') -ax1.set_xlabel('Date') -ax1.set_ylabel('Count') +ax1.set_title("Jira by month") +ax1.set_xlabel("Date") +ax1.set_ylabel("Count") plt.gcf().autofmt_xdate() ax2 = fig.add_subplot(122) -ax2.plot(df_year['resolved_year'], df_year['count']) +ax2.plot(df_year["resolved_year"], df_year["count"]) ax2.ticklabel_format(useOffset=False) -ax2.xaxis.set_major_formatter(FormatStrFormatter('%d')) -ax2.set_title('Jira by year') -ax2.set_xlabel('Date') -ax2.set_ylabel('Count') +ax2.xaxis.set_major_formatter(FormatStrFormatter("%d")) +ax2.set_title("Jira by year") +ax2.set_xlabel("Date") +ax2.set_ylabel("Count") ax2.grid() plt.gcf().autofmt_xdate() diff --git a/pandas__examples/Analysis of account detail (html)/main.py b/pandas__examples/Analysis of account detail (html)/main.py index 9193eabf0..304685cb7 100644 --- a/pandas__examples/Analysis of account detail (html)/main.py +++ b/pandas__examples/Analysis of account detail (html)/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # TODO: сделать детализацию счета и заказать в html/excel @@ -9,11 +9,16 @@ # сделать обработку excel на pandas: Analysis of account detail (excel) import zipfile -with zipfile.ZipFile('Doc_df7c89c378c04e8daf69257ea95d9a2e.zip') as f: - data_file = f.read('Doc_df7c89c378c04e8daf69257ea95d9a2e.html') +from datetime import datetime +import pandas as pd from bs4 import BeautifulSoup -root = BeautifulSoup(data_file, 'lxml') + + +with zipfile.ZipFile("Doc_df7c89c378c04e8daf69257ea95d9a2e.zip") as f: + data_file = f.read("Doc_df7c89c378c04e8daf69257ea95d9a2e.html") + +root = BeautifulSoup(data_file, "lxml") records = [] for tr in root.select("table > tbody > tr"): @@ -27,59 +32,76 @@ # td_list[6].text, # 'Зона направления вызова/номер сессии' td_list[7].text, # 'Услуга' td_list[9].text, # 'Длительность/Объем (мин.:сек.)/(Kb)' - float(td_list[10].text.replace(',', '.')), # 'Стоимость руб. без НДС' + float(td_list[10].text.replace(",", ".")), # 'Стоимость руб. без НДС' ] records.append(record) columns = [ - 'Дата', 'Время', 'GMT', 'Номер', + "Дата", + "Время", + "GMT", + "Номер", # 'Зона вызова', 'Зона направления вызова/номер сессии', - 'Услуга', 'Длительность/Объем (мин.:сек.)/(Kb)', 'Стоимость руб. без НДС' + "Услуга", + "Длительность/Объем (мин.:сек.)/(Kb)", + "Стоимость руб. без НДС", ] -import pandas as pd df = pd.DataFrame(data=records, columns=columns) # print(df) -print('Total rows:', len(df)) +print("Total rows:", len(df)) -df_with_null_price = df[df['Стоимость руб. без НДС'] == 0] -print('With null price:', len(df_with_null_price)) +df_with_null_price = df[df["Стоимость руб. без НДС"] == 0] +print("With null price:", len(df_with_null_price)) -df_with_price = df[df['Стоимость руб. без НДС'] > 0] -print('With price:', len(df_with_price)) +df_with_price = df[df["Стоимость руб. без НДС"] > 0] +print("With price:", len(df_with_price)) -phone_list = sorted(set(df['Номер'].tolist())) -print('\nPhones ({}): {}'.format(len(phone_list), phone_list)) +phone_list = sorted(set(df["Номер"].tolist())) +print(f"\nPhones ({len(phone_list)}): {phone_list}") -print('\nShow target phone:') -df_dns_shop = df[df['Номер'].str.contains('sms:DNS-SHOP')] -print(' DNS-SHOP: number: {}, total price: {}'.format(len(df_dns_shop), df_dns_shop['Стоимость руб. без НДС'].sum())) +print("\nShow target phone:") +df_dns_shop = df[df["Номер"].str.contains("sms:DNS-SHOP")] +print( + f" DNS-SHOP: number: {len(df_dns_shop)}, total price: {df_dns_shop['Стоимость руб. без НДС'].sum()}" +) -df_maginfo = df[df['Номер'].str.contains('sms:Maginfo')] -print(' Maginfo: number: {}, total price: {}'.format(len(df_maginfo), df_maginfo['Стоимость руб. без НДС'].sum())) +df_maginfo = df[df["Номер"].str.contains("sms:Maginfo")] +print( + f" Maginfo: number: {len(df_maginfo)}, total price: {df_maginfo['Стоимость руб. без НДС'].sum()}" +) -df_sms_ru = df[df['Номер'].str.contains('sms:SMS.RU')] -print(' SMS.RU: number: {}, total price: {}'.format(len(df_sms_ru), df_sms_ru['Стоимость руб. без НДС'].sum())) +df_sms_ru = df[df["Номер"].str.contains("sms:SMS.RU")] +print( + f" SMS.RU: number: {len(df_sms_ru)}, total price: {df_sms_ru['Стоимость руб. без НДС'].sum()}" +) -print('\nPrint details (Maginfo):') +print("\nPrint details (Maginfo):") print(df_maginfo.to_string()) # Посчитать сумму за июнь, июль и август -print('\nPrice for months 06, 07 and 08:') -print(' ' + str(df[df['Дата'].str.contains('.06.')]['Стоимость руб. без НДС'].sum())) -print(' ' + str(df[df['Дата'].str.contains('.07.')]['Стоимость руб. без НДС'].sum())) -print(' ' + str(df[df['Дата'].str.contains('.08.')]['Стоимость руб. без НДС'].sum())) - -print('\nPrint details sorting by price (top 10):') -df_with_price_sorted = df_with_price.sort_values(by='Стоимость руб. без НДС', ascending=True).tail(10) +print("\nPrice for months 06, 07 and 08:") +print(" " + str(df[df["Дата"].str.contains(".06.")]["Стоимость руб. без НДС"].sum())) +print(" " + str(df[df["Дата"].str.contains(".07.")]["Стоимость руб. без НДС"].sum())) +print(" " + str(df[df["Дата"].str.contains(".08.")]["Стоимость руб. без НДС"].sum())) + +print("\nPrint details sorting by price (top 10):") +df_with_price_sorted = df_with_price.sort_values( + by="Стоимость руб. без НДС", ascending=True +).tail(10) print(df_with_price_sorted.to_string()) -df_by_phone = df_with_price[df_with_price['Номер'].str.contains('79510000000')] -print('\nPrint total price by phone:', df_by_phone['Стоимость руб. без НДС'].sum()) +df_by_phone = df_with_price[df_with_price["Номер"].str.contains("79510000000")] +print("\nPrint total price by phone:", df_by_phone["Стоимость руб. без НДС"].sum()) -df_months_06_07_08 = df_with_price[df_with_price['Дата'].str.contains('|'.join(['.06.', '.07.', '.08.']))] -print('\nTotal price for months 06, 07 and 08:', df_months_06_07_08['Стоимость руб. без НДС'].sum()) +df_months_06_07_08 = df_with_price[ + df_with_price["Дата"].str.contains("|".join([".06.", ".07.", ".08."])) +] +print( + "\nTotal price for months 06, 07 and 08:", + df_months_06_07_08["Стоимость руб. без НДС"].sum(), +) print(df_months_06_07_08.to_string()) # @@ -87,20 +109,24 @@ # показать. # Таблице нужно поле Итого # -print('\n\nPrint internet info:') -df_internet = df[df['Номер'] == 'internet.mts.ru'] +print("\n\nPrint internet info:") +df_internet = df[df["Номер"] == "internet.mts.ru"] -from datetime import datetime -data_list = sorted(set(df_internet['Дата'].tolist()), key=lambda data: datetime.strptime(data, '%d.%m.%Y')) +data_list = sorted( + set(df_internet["Дата"].tolist()), + key=lambda data: datetime.strptime(data, "%d.%m.%Y"), +) total_mb = 0 for data in data_list: - kb_list = df_internet[df_internet['Дата'] == data]['Длительность/Объем (мин.:сек.)/(Kb)'].tolist() - kb_list = [int(kb.replace('Kb', '')) for kb in kb_list] + kb_list = df_internet[df_internet["Дата"] == data][ + "Длительность/Объем (мин.:сек.)/(Kb)" + ].tolist() + kb_list = [int(kb.replace("Kb", "")) for kb in kb_list] sum_mb = sum(kb_list) // 1024 total_mb += sum_mb - print(data, sum_mb, 'MB') + print(data, sum_mb, "MB") -print('Total:', total_mb, 'MB') +print("Total:", total_mb, "MB") diff --git a/pandas__examples/json_to_csv.py b/pandas__examples/json_to_csv.py index 28905b570..80c4800f0 100644 --- a/pandas__examples/json_to_csv.py +++ b/pandas__examples/json_to_csv.py @@ -1,21 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import pandas as pd # SOURCE: https://ru.stackoverflow.com/a/846931/201445 json_data = { - 'items': [ - {'first_name': 'Оля', 'id': 111111, 'last_name': 'Сущенко'}, - {'first_name': 'Георгий', 'id': 222222, 'last_name': 'Голосов'}, - {'first_name': 'Максим', 'id': 333333, 'home_phone': '+79909999999', 'last_name': 'Тупиченков'} + "items": [ + {"first_name": "Оля", "id": 111111, "last_name": "Сущенко"}, + {"first_name": "Георгий", "id": 222222, "last_name": "Голосов"}, + { + "first_name": "Максим", + "id": 333333, + "home_phone": "+79909999999", + "last_name": "Тупиченков", + }, ] } - -import pandas as pd -df = pd.io.json.json_normalize(json_data['items']) +df = pd.io.json.json_normalize(json_data["items"]) print(df) -df.to_csv('out.csv', index=False) +df.to_csv("out.csv", index=False) diff --git a/pandas__examples/json_to_excel.py b/pandas__examples/json_to_excel.py index 0e3933d11..b4ffe815d 100644 --- a/pandas__examples/json_to_excel.py +++ b/pandas__examples/json_to_excel.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import json +import pandas as pd json_str = """\ @@ -67,10 +71,9 @@ } """ -import json data = json.loads(json_str) # SOURCE: https://ru.stackoverflow.com/questions/671333 -import pandas as pd -df = pd.DataFrame(data['d']['Items']) -df.set_index('CouponCode')['TotalPrizeValue'].reset_index().to_excel('result.xlsx') + +df = pd.DataFrame(data["d"]["Items"]) +df.set_index("CouponCode")["TotalPrizeValue"].reset_index().to_excel("result.xlsx") diff --git a/pandas__examples/print_characters_tekken__who_were_in_all_series.py b/pandas__examples/print_characters_tekken__who_were_in_all_series.py index 75bcb071d..6f8f990af 100644 --- a/pandas__examples/print_characters_tekken__who_were_in_all_series.py +++ b/pandas__examples/print_characters_tekken__who_were_in_all_series.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from table_from__html_by_url__wiki__tekken import get_df @@ -10,8 +10,8 @@ df = get_df() # Столбцы-серии игр: ['Tekken', 'Tekken 2', 'Tekken 3', 'Tekken 4', 'Tekken 5', 'Tekken 6', 'Tekken 7'] -cols = [col for col in df.columns if col.startswith('Tekken')] +cols = [col for col in df.columns if col.startswith("Tekken")] for index, row in df.iterrows(): - if all(row[col] == 'Y' for col in cols): + if all(row[col] == "Y" for col in cols): print(row[0]) diff --git a/pandas__examples/process_txt_two_columns/main.py b/pandas__examples/process_txt_two_columns/main.py index 12d7f9459..2860aac5e 100644 --- a/pandas__examples/process_txt_two_columns/main.py +++ b/pandas__examples/process_txt_two_columns/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pandas as pd + df = pd.read_csv("001.txt", sep=":", header=None, dtype="str") for index, row in df.iterrows(): - row[1] = ''.join('1' if x == '0' else '0' for x in row[1].strip()) + row[1] = "".join("1" if x == "0" else "0" for x in row[1].strip()) df.to_csv("002.txt", index=None, header=None, sep=":") diff --git a/pandas__examples/show dataframe in QTableWidget.py b/pandas__examples/show dataframe in QTableWidget.py index ca82d2b8b..05dbc9130 100644 --- a/pandas__examples/show dataframe in QTableWidget.py +++ b/pandas__examples/show dataframe in QTableWidget.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import * +import pandas as pd def generate_test_data(): records = [ - (1, 'Vasya', 16), - (2, 'Vasya2', 18), - (3, 'Vasya3', 34), - (4, 'Vasya4', 10), - (5, 'Vasya5', 19), + (1, "Vasya", 16), + (2, "Vasya2", 18), + (3, "Vasya3", 34), + (4, "Vasya4", 10), + (5, "Vasya5", 19), ] + return pd.DataFrame(data=records, columns=["ID", "NAME", "AGE"]) - import pandas as pd - return pd.DataFrame(data=records, columns=['ID', 'NAME', 'AGE']) +if __name__ == "__main__": + from PyQt5.QtWidgets import * -if __name__ == '__main__': # Подготовка данных df = generate_test_data() headers = df.columns.values.tolist() diff --git a/pandas__examples/table_from__html_by_url.py b/pandas__examples/table_from__html_by_url.py index d050ad142..2c3da99b3 100644 --- a/pandas__examples/table_from__html_by_url.py +++ b/pandas__examples/table_from__html_by_url.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pandas import pandas as pd -url = 'http://www.gks.ru/bgd/regl/b12_14p/IssWWW.exe/Stg/d01/05-02.htm' + +url = "http://www.gks.ru/bgd/regl/b12_14p/IssWWW.exe/Stg/d01/05-02.htm" df = pd.read_html(url, header=0, index_col=0)[0] print(df) -df.to_excel('table.xlsx') +df.to_excel("table.xlsx") diff --git a/pandas__examples/table_from__html_by_url__wiki__tekken.py b/pandas__examples/table_from__html_by_url__wiki__tekken.py index 7707a3eab..835780c2e 100644 --- a/pandas__examples/table_from__html_by_url__wiki__tekken.py +++ b/pandas__examples/table_from__html_by_url__wiki__tekken.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pandas @@ -15,21 +15,21 @@ def get_df(): - url = 'https://ru.wikipedia.org/wiki/Список_персонажей_Tekken' + url = "https://ru.wikipedia.org/wiki/Список_персонажей_Tekken" rs = requests.get(url) # Удаление сносок -- из-за них имена персонажей и значения присутствия в конкретной серии показывается # невалидно, например "Акума14", а не "Акума" - root = BeautifulSoup(rs.content, 'html.parser') - for sub in root.select('sup'): + root = BeautifulSoup(rs.content, "html.parser") + for sub in root.select("sup"): sub.decompose() df_list = pd.read_html(str(root), header=0) return df_list[0] -if __name__ == '__main__': +if __name__ == "__main__": df = get_df() print(df) - df.to_excel('tekken_table.xlsx', index=False) + df.to_excel("tekken_table.xlsx", index=False) diff --git a/parse__radix__CacheContent_log/main.py b/parse__radix__CacheContent_log/main.py deleted file mode 100644 index 7dd1a4543..000000000 --- a/parse__radix__CacheContent_log/main.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import typing - - -def get_cache_object_info(file_name) -> typing.Dict[str, typing.Dict[str, int]]: - # new_object: Tran[180409000010273756] - # existing_object: Hold[13708] - - import re - pattern_object = re.compile('^(\w+_object): (.+)\[.+$') - - object_by_number = dict() - - for line in open(file_name): - match = pattern_object.search(line) - if match: - type_cache = match[1] - name = match[2] - - if type_cache not in object_by_number: - object_by_number[type_cache] = dict() - - if name not in object_by_number[type_cache]: - object_by_number[type_cache][name] = 0 - - object_by_number[type_cache][name] += 1 - - return object_by_number - - -def get_cache_existing_object_list(file_name, top_values=None) -> typing.List[typing.Tuple[str, int]]: - object_by_number = get_cache_object_info(file_name) - - existing_object_list = sorted(object_by_number['existing_object'].items(), key=lambda x: x[1], reverse=True) - if top_values: - existing_object_list = existing_object_list[:top_values] - - rows = [(name, number) for name, number in existing_object_list] - return rows - - -if __name__ == '__main__': - file_name = 'CacheContent_log.txt' - print(file_name) - - rows = get_cache_existing_object_list(file_name, top_values=5) - - # pip install tabulate - from tabulate import tabulate - print(tabulate(rows, headers=('NAME', 'NUMBER'), tablefmt="grid")) - print() diff --git a/parse_command_with_phone__doska.ru.py b/parse_command_with_phone__doska.ru.py index cbfe4d54b..362dd0652 100644 --- a/parse_command_with_phone__doska.ru.py +++ b/parse_command_with_phone__doska.ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -20,21 +20,22 @@ """ -def _ph_dec(g, r, k): - from base64 import b64decode - decode_base64 = b64decode(g).decode('utf-8') +import re +from base64 import b64decode + - import re - g = re.sub(r'%([a-fA-F0-9]{2})', lambda m: chr(int(m.group(1), 16)), decode_base64) +def _ph_dec(g, r, k): + decode_base64 = b64decode(g).decode("utf-8") + g = re.sub(r"%([a-fA-F0-9]{2})", lambda m: chr(int(m.group(1), 16)), decode_base64) n = len(r) d = len(g) - c = '' + c = "" f = 0 while f < d: - q = g[f: f + 1] - p = r[f % n: f % n + 1] + q = g[f : f + 1] + p = r[f % n : f % n + 1] if k == 1: q = ord(q[0]) - ord(p[0]) @@ -60,44 +61,56 @@ def gpzd(data, key): def get_phones_from_command(cmd): phones = list() - cmd = cmd.split('\t') + cmd = cmd.split("\t") for i in cmd: - data = i.split('|') + data = i.split("|") b = gpzd(data[1], data[2]) - phone = _ph_dec(b, 'K0dbVwzGrpLa-wRs2', 2) + phone = _ph_dec(b, "K0dbVwzGrpLa-wRs2", 2) phones.append(phone) return phones -if __name__ == '__main__': - cmd = '1|JTg0JTkxbyU5NnVqJTdDJThGc2drWnR6JTkxWA==|22481814 ' \ - '2|JTg1JTk3cCU5NHZscyU5NHloeVlyJTdCJTk0aXIlN0UlOEUlNjAlODQlOTElN0MlNUR1aWclNjA=|31950540 ' \ - '3|JTg1JTk0cCU5NnVmJTg0JTk2dWhuJTVCdHolOEQlNjA=|44314626' +if __name__ == "__main__": + cmd = ( + "1|JTg0JTkxbyU5NnVqJTdDJThGc2drWnR6JTkxWA==|22481814 " + "2|JTg1JTk3cCU5NHZscyU5NHloeVlyJTdCJTk0aXIlN0UlOEUlNjAlODQlOTElN0MlNUR1aWclNjA=|31950540 " + "3|JTg1JTk0cCU5NnVmJTg0JTk2dWhuJTVCdHolOEQlNjA=|44314626" + ) print(get_phones_from_command(cmd)) # ['-00-00', '11-822-14', '-00-00'] - cmd = '1|JTg3JTkyJTdDJTk2dmZxJTk2c2hwWHQlN0IlOEVmdHglOEUlNUIlODQlN0QlN0NXciVBMWElNjA=|23649509 ' \ - '2|JThBJTk1JTdEJTk0dGtvJTk3cWx3WnJ6bCU5QnV2JTkyJTVFJTg2a3olNUN0bF9k|95446977 ' \ - '3|JTg1JTkybyU5N3pvJTgzJTkycWhsWnUlN0YlOTZf|40841102' + cmd = ( + "1|JTg3JTkyJTdDJTk2dmZxJTk2c2hwWHQlN0IlOEVmdHglOEUlNUIlODQlN0QlN0NXciVBMWElNjA=|23649509 " + "2|JThBJTk1JTdEJTk0dGtvJTk3cWx3WnJ6bCU5QnV2JTkyJTVFJTg2a3olNUN0bF9k|95446977 " + "3|JTg1JTkybyU5N3pvJTgzJTkycWhsWnUlN0YlOTZf|40841102" + ) print(get_phones_from_command(cmd)) # ['94-711-03', '54-829-39', '-00-00'] - cmd = '1|JTg4V2slOTN1ZnYlOTV1aSU3QiU4RXVWdyU5NXglN0J2JTkycSU3QiU5MiU5OXMlN0QlOTFa|56764591 ' \ - '2|JThBJTk1dSU4RXlpcCU5NXFsbiU5NHBabiU4Rnh3cCU5NXd2JTk2JTk3bSU3RCU4RCU1Qw==|96021657 ' \ - '3|JTg4JThGbiU4RnZsJTgzJTkzcWtpWW0lN0IlOTNf|85533937' + cmd = ( + "1|JTg4V2slOTN1ZnYlOTV1aSU3QiU4RXVWdyU5NXglN0J2JTkycSU3QiU5MiU5OXMlN0QlOTFa|56764591 " + "2|JThBJTk1dSU4RXlpcCU5NXFsbiU5NHBabiU4Rnh3cCU5NXd2JTk2JTk3bSU3RCU4RCU1Qw==|96021657 " + "3|JTg4JThGbiU4RnZsJTgzJTkzcWtpWW0lN0IlOTNf|85533937" + ) print(get_phones_from_command(cmd)) # ['60-164-75', '51-632-25', '-00-00'] - cmd = '1|JTg3JTdDJTgwVXklOEV6JTVCbyU5RCU3RV9yJUExell0dXpreGclN0RadCU3RSU4RCU5MHglODB1ZQ==|46551687 ' \ - '2|JTg3ZyU3Q1QlN0MlOEV5JTVFcCU5RCU3Q1clN0RoeSU1RHQlOEQlN0NYJTdEeHlfciU3QiU5RSU4Rnp6bWc=|2094350 ' \ - '3|JTg0JTk2byU5NnVtJTdDJTkycm5sJTVFcCU3RCU4RSU1Qg==|3088581' + cmd = ( + "1|JTg3JTdDJTgwVXklOEV6JTVCbyU5RCU3RV9yJUExell0dXpreGclN0RadCU3RSU4RCU5MHglODB1ZQ==|46551687 " + "2|JTg3ZyU3Q1QlN0MlOEV5JTVFcCU5RCU3Q1clN0RoeSU1RHQlOEQlN0NYJTdEeHlfciU3QiU5RSU4Rnp6bWc=|2094350 " + "3|JTg0JTk2byU5NnVtJTdDJTkycm5sJTVFcCU3RCU4RSU1Qg==|3088581" + ) print(get_phones_from_command(cmd)) # ['49-397-09', '79-935-37', '-00-00'] - cmd = '1|JTg4JTkwayU5M3htJTdFJThGdWtqVnElN0QlOTRa|86770681 ' \ - '2|JTg5JTk2biU5MnZndiU5NndqJThFJTVCcCU3Q2glQTF0JTdDJTkxXyU4N3klN0NYdyU3QmVj|80583093 ' \ - '3|JTg3JTkycSU4RXFtJTdCJTk1d2psJTVDbHYlOTRX|74342308' + cmd = ( + "1|JTg4JTkwayU5M3htJTdFJThGdWtqVnElN0QlOTRa|86770681 " + "2|JTg5JTk2biU5MnZndiU5NndqJThFJTVCcCU3Q2glQTF0JTdDJTkxXyU4N3klN0NYdyU3QmVj|80583093 " + "3|JTg3JTkycSU4RXFtJTdCJTk1d2psJTVDbHYlOTRX|74342308" + ) print(get_phones_from_command(cmd)) # ['-00-00', '50-328-41', '-00-00'] - cmd = '1|JTg3JThFbyU4RXlpJTdGJThFc2poWmwlN0UlOTAlNUI=|67355029 ' \ - '2|JTg5cHclOTB2bnMlOTN1aiU4NFduJTdCJTk2aXF6JTkxXyU4NCU4RCU3Q193JTlFY2M=|80050871 ' \ - '3|JTg4JThFcyU5M3dvJTgwJTk3eWtoJTVFcSU3QyU5NiU1Qw==|84769395' + cmd = ( + "1|JTg3JThFbyU4RXlpJTdGJThFc2poWmwlN0UlOTAlNUI=|67355029 " + "2|JTg5cHclOTB2bnMlOTN1aiU4NFduJTdCJTk2aXF6JTkxXyU4NCU4RCU3Q193JTlFY2M=|80050871 " + "3|JTg4JThFcyU5M3dvJTgwJTk3eWtoJTVFcSU3QyU5NiU1Qw==|84769395" + ) print(get_phones_from_command(cmd)) # ['-00-00', '33-212-58', '-00-00'] diff --git a/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py b/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py deleted file mode 100644 index d6a32d714..000000000 --- a/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests -rs = requests.get('http://iturizmo.ru/razgovorniki/russko-anglijskij-razgovornik.html') - -from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'html.parser') - -en_ru_items = [] - -for tr in root.select(".entry-content tr"): - td_list = [td.text.strip() for td in tr.select('td')] - if not td_list: - continue - - ru, en = td_list[:2] - en_ru_items.append((en, ru)) - -print(len(en_ru_items), en_ru_items) - -import json -json.dump(en_ru_items, open('en_ru.json', 'w', encoding='utf-8'), indent=4, ensure_ascii=False) diff --git a/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py b/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py deleted file mode 100644 index 6d2c49476..000000000 --- a/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests -rs = requests.get('http://www.7english.ru/dictionary.php?id=2000&letter=all') - -from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'html.parser') - -en_ru_items = [] - -for tr in root.select('tr'): - # У строк в таблице перевода есть аттрибут onmouseover - if 'onmouseover' not in tr.attrs: - continue - - td_list = [td.text.strip() for td in tr.select('td')] - - # Количество ячеек в таблице со словами -- 9 - if len(td_list) != 9 or not td_list[1] or not td_list[5]: - continue - - en = td_list[1] - - # Русские слова могут быть перечислены через запятую 'ты, вы', а нам достаточно одного слова - # 'ты, вы' -> 'ты' - ru = td_list[5].split(', ')[0] - - en_ru_items.append((en, ru)) - -print(len(en_ru_items), en_ru_items) - -import json -json.dump(en_ru_items, open('en_ru.json', 'w', encoding='utf-8'), indent=4, ensure_ascii=False) diff --git a/parse_games_mail_ru__future_hits.py b/parse_games_mail_ru__future_hits.py deleted file mode 100644 index 163a2aa0e..000000000 --- a/parse_games_mail_ru__future_hits.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_game_list(): - from urllib.parse import urljoin - - import requests - rs = requests.get('https://games.mail.ru/pc/games/future_hits/') - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - items = [] - - # Перебор табличек с играми - for item in root.select('.b-pc__entities-item'): - a = item.select_one('.b-pc__entities-title > a') - title = a.text.strip() - - url = urljoin(rs.url, a['href']) - - description = item.select_one('.b-pc__entities-descr').text.strip() - img_url = item.select_one('.b-pc__entities-img')['src'] - release_date = item.select_one('.b-pc__entities-author').text.strip() - - items.append((title, description, release_date, url, img_url)) - - return items - - -if __name__ == '__main__': - game_list = get_game_list() - - def print_list(items): - for i, (title, description, release_date, url, img_url) in enumerate(items, 1): - print('{:2}. "{}" ({}): {} [{}]\n{}\n'.format(i, title, release_date, url, img_url, description)) - - # Full - print_list(game_list) - - # # First 5 - # new_game_list = game_list[:5] - # print_list(new_game_list) - # - # # Sorted by title - # new_game_list = sorted(game_list, key=lambda x: x[0]) - # print_list(new_game_list) - - # # Sorted by year, reverse - # import re - # get_year = lambda text: int(re.search('\d{4}', text).group()) - # new_game_list = sorted(game_list, key=lambda x: get_year(x[2]), reverse=True) - # print_list(new_game_list) diff --git a/parse_jira_Assigned_Open_Issues_per_Project/common.py b/parse_jira_Assigned_Open_Issues_per_Project/common.py deleted file mode 100644 index 4f8b495a6..000000000 --- a/parse_jira_Assigned_Open_Issues_per_Project/common.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import logging -from typing import Dict - -# For import ascii_table__simple_pretty__ljust.py -import sys - -sys.path.append('..') -from ascii_table__simple_pretty__ljust import pretty_table - - -def get_table(assigned_open_issues_per_project: Dict[str, int]) -> str: - data = [("PROJECT", 'Issues')] + list(assigned_open_issues_per_project.items()) - return pretty_table(data) - - -def print_table(assigned_open_issues_per_project: Dict[str, int]): - print(get_table(assigned_open_issues_per_project)) - # PROJECT | Issues - # --------+------- - # xxx | 1 - # yyy | 2 - # zzz | 3 - - -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True) -> logging.Logger: - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - if log_file: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - if log_stdout: - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -logger = get_logger('parse_jira_Assigned_Open_Issues_per_Project') diff --git a/parse_jira_Assigned_Open_Issues_per_Project/db.py b/parse_jira_Assigned_Open_Issues_per_Project/db.py deleted file mode 100644 index 7838253dd..000000000 --- a/parse_jira_Assigned_Open_Issues_per_Project/db.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from common import print_table -import datetime as DT -import pathlib -from typing import Dict, Optional - -from peewee import * - - -# Absolute file name -DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / 'database.sqlite') - - -def db_create_backup(backup_dir='backup'): - import datetime as DT - import os - import shutil - - os.makedirs(backup_dir, exist_ok=True) - - file_name = str(DT.datetime.today().date()) + '.sqlite' - file_name = os.path.join(backup_dir, file_name) - - shutil.copy(DB_FILE_NAME, file_name) - - -# Ensure foreign-key constraints are enforced. -db = SqliteDatabase(DB_FILE_NAME, pragmas={'foreign_keys': 1}) - - -class BaseModel(Model): - class Meta: - database = db - - -class Run(BaseModel): - date = DateField(default=DT.date.today) - - def get_total_issues(self) -> int: - return sum(x.value for x in self.issue_numbers) - - def get_project_by_issue_numbers(self) -> Dict[str, int]: - return {issue.project.name: issue.value for issue in self.issue_numbers} - - def __str__(self): - return f'{self.__class__.__name__}(id={self.id}, date={self.date}, total_issues={self.get_total_issues()})' - - -class Project(BaseModel): - name = TextField() - - def __str__(self): - return f'{self.__class__.__name__}(id={self.id}, name={self.name})' - - -class IssueNumber(BaseModel): - value = IntegerField() - run = ForeignKeyField(Run, backref='issue_numbers') - project = ForeignKeyField(Project, backref='issue_numbers') - - def __str__(self): - return f'{self.__class__.__name__}(id={self.id}, project={self.project.name!r}, ' \ - f'value={self.value}, run_id={self.run.id})' - - -def add(assigned_open_issues_per_project: Dict[str, int]) -> Optional[bool]: - last_run = Run.select().order_by(Run.id.desc()).get() - if assigned_open_issues_per_project == last_run.get_project_by_issue_numbers(): - return - - run, created = Run.get_or_create(date=DT.date.today()) - if not created: - return False - - for project_name, issue_numbers in assigned_open_issues_per_project.items(): - project, _ = Project.get_or_create(name=project_name) - - IssueNumber.create( - value=issue_numbers, - run=run, - project=project - ) - - db_create_backup() - - return True - - -db.connect() -db.create_tables([Run, Project, IssueNumber]) - - -if __name__ == '__main__': - projects = [p.name for p in Project.select()] - print(f"Projects ({len(projects)}): {projects}\n") - - # Print last rows - for run in Run.select().order_by(Run.id.desc()).limit(5): - print(run, '\n') - print_table(run.get_project_by_issue_numbers()) - - print('\n' + '-' * 100 + '\n') diff --git a/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py b/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py deleted file mode 100644 index b3716187a..000000000 --- a/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import Dict -from pathlib import Path - -import requests -from bs4 import BeautifulSoup - -from common import print_table - - -URL = 'https://jira.compassplus.ru/secure/ViewProfile.jspa?name=ipetrash' -HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', -} - -# NOTE. Get : openssl pkcs12 -nodes -out key.pem -in file.p12 -PEM_FILE_NAME = 'ipetrash.pem' -PEM_FILE_NAME = str(Path(__file__).resolve().parent / PEM_FILE_NAME) - - -def get_assigned_open_issues_per_project() -> Dict[str, int]: - rs = requests.get(URL, headers=HEADERS, cert=PEM_FILE_NAME) - root = BeautifulSoup(rs.content, 'html.parser') - - data = dict() - - for item in root.select('#assigned-and-open > .mod-content > .stat-list > li'): - name = item.select_one('a[title]').get_text(strip=True) - value = int(item.select_one('.stat').get_text(strip=True)) - - data[name] = value - - return data - - -def get_and_prints() -> Dict[str, int]: - assigned_open_issues_per_project = get_assigned_open_issues_per_project() - # {'xxx': 1, 'yyy': 2, 'zzz': 3} - - print('Total issues:', sum(assigned_open_issues_per_project.values())) - print() - - print_table(assigned_open_issues_per_project) - # PROJECT | Issues - # --------+------- - # xxx | 1 - # yyy | 2 - # zzz | 3 - - return assigned_open_issues_per_project - - -if __name__ == '__main__': - get_and_prints() diff --git a/parse_jira_Assigned_Open_Issues_per_Project/gui.py b/parse_jira_Assigned_Open_Issues_per_Project/gui.py deleted file mode 100644 index cdef6b4e3..000000000 --- a/parse_jira_Assigned_Open_Issues_per_Project/gui.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import calendar -import datetime as DT -import math -from pathlib import Path -from typing import List -import sys - -from PyQt5.QtWidgets import ( - QApplication, QMessageBox, QMainWindow, QSystemTrayIcon, QTabWidget, QTableWidget, - QTableWidgetItem, QVBoxLayout, QWidget, QPushButton, QSplitter, QLabel, QGridLayout, - QHeaderView -) -from PyQt5.QtGui import QIcon, QPainter -from PyQt5.QtCore import QEvent, QTimer, Qt -from PyQt5.QtChart import QChart, QLineSeries, QDateTimeAxis, QValueAxis - -from common import get_table -from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project -from db import Run - -sys.path.append('../qt__pyqt__pyside__pyqode') -from chart_line__show_tooltip_on_series__QtChart import ChartViewToolTips - - -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print(text) - QMessageBox.critical(None, 'Error', text) - sys.exit(1) - - -sys.excepthook = log_uncaught_exceptions - - -CURRENT_DIR = Path(__file__).resolve().parent -WINDOW_TITLE = CURRENT_DIR.name - - -def get_table_widget(header_labels: list) -> QTableWidget: - table = QTableWidget() - table.setAlternatingRowColors(True) - table.setEditTriggers(QTableWidget.NoEditTriggers) - table.setSelectionBehavior(QTableWidget.SelectRows) - table.setSelectionMode(QTableWidget.SingleSelection) - table.setColumnCount(len(header_labels)) - table.setHorizontalHeaderLabels(header_labels) - table.horizontalHeader().setStretchLastSection(True) - return table - - -class TableWidgetRun(QWidget): - def __init__(self): - super().__init__() - - self.table_run = get_table_widget(['DATE', 'TOTAL ISSUES']) - self.table_run.itemClicked.connect(self._on_table_run_item_clicked) - - self.table_issues = get_table_widget(['PROJECT', 'NUMBER']) - self.table_run.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.table_issues.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - - splitter = QSplitter(Qt.Horizontal) - splitter.addWidget(self.table_run) - splitter.addWidget(self.table_issues) - - main_layout = QVBoxLayout() - main_layout.addWidget(splitter) - - self.setLayout(main_layout) - - def refresh(self, items: List[Run]): - # Удаление строк таблицы - while self.table_run.rowCount(): - self.table_run.removeRow(0) - - for i, run in enumerate(items): - self.table_run.setRowCount(self.table_run.rowCount() + 1) - - item = QTableWidgetItem(run.date.strftime('%d/%m/%Y')) - item.setData(Qt.UserRole, run.get_project_by_issue_numbers()) - self.table_run.setItem(i, 0, item) - - item = QTableWidgetItem(str(run.get_total_issues())) - self.table_run.setItem(i, 1, item) - - self.table_run.setCurrentCell(0, 0) - self.table_run.setFocus() - self._on_table_run_item_clicked() - - def _on_table_run_item_clicked(self): - # Удаление строк таблицы - while self.table_issues.rowCount(): - self.table_issues.removeRow(0) - - item = self.table_run.item(self.table_run.currentRow(), 0) - if not item: - return - - for i, (project_name, number) in enumerate(item.data(Qt.UserRole).items()): - self.table_issues.setRowCount(self.table_issues.rowCount() + 1) - - self.table_issues.setItem(i, 0, QTableWidgetItem(project_name)) - self.table_issues.setItem(i, 1, QTableWidgetItem(str(number))) - - -class CurrentAssignedOpenIssues(QWidget): - def __init__(self): - super().__init__() - - self.table = get_table_widget(['PROJECT', 'NUMBER']) - self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - - self.pb_refresh = QPushButton('REFRESH') - self.pb_refresh.clicked.connect(self.refresh) - - self.label_total = QLabel() - font = self.label_total.font() - font.setPixelSize(30) - self.label_total.setFont(font) - - self.label_last_refresh_date = QLabel() - - main_layout = QGridLayout() - - main_layout.addWidget(self.label_total, 0, 0) - main_layout.addWidget(self.pb_refresh, 0, 1) - main_layout.addWidget(self.label_last_refresh_date, 0, 2) - main_layout.addWidget(self.table, 1, 0, 2, 0) - - self.setLayout(main_layout) - - self._update_total_issues('-') - - def _update_total_issues(self, value): - self.label_total.setText(f'Total issues: {value}') - - def refresh(self): - items = get_assigned_open_issues_per_project() - - self._update_total_issues(sum(items.values())) - - # Удаление строк таблицы - while self.table.rowCount(): - self.table.removeRow(0) - - for i, (project_name, number) in enumerate(items.items()): - self.table.setRowCount(self.table.rowCount() + 1) - - self.table.setItem(i, 0, QTableWidgetItem(project_name)) - self.table.setItem(i, 1, QTableWidgetItem(str(number))) - - self.label_last_refresh_date.setText( - "Last refresh date: " + DT.datetime.now().strftime('%d/%m/%Y %H:%M:%S') - ) - - -class MyChartViewToolTips(ChartViewToolTips): - def __init__(self, timestamp_by_run: dict): - super().__init__() - - self._callout_font_family = 'Courier' - self.timestamp_by_run = timestamp_by_run - - def show_series_tooltip(self, point, state: bool): - # value -> pos - point = self.chart().mapToPosition(point) - - if not self._tooltip: - self._tooltip = self._add_Callout() - - if state: - distance = 25 - - for series in self.chart().series(): - for p_value in series.pointsVector(): - p = self.chart().mapToPosition(p_value) - - current_distance = math.sqrt( - (p.x() - point.x()) * (p.x() - point.x()) - + (p.y() - point.y()) * (p.y() - point.y()) - ) - - if current_distance < distance: - date_value = int(p_value.x()) - run = self.timestamp_by_run[date_value] - text = f"Total issues: {run.get_total_issues()}\n\n" \ - f"{get_table(run.get_project_by_issue_numbers())}" - - self._tooltip.setText(text) - self._tooltip.setAnchor(p_value) - self._tooltip.setZValue(11) - self._tooltip.updateGeometry() - self._tooltip.show() - else: - self._tooltip.hide() - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle(WINDOW_TITLE) - - file_name = str(CURRENT_DIR / 'favicon.ico') - icon = QIcon(file_name) - self.setWindowIcon(icon) - - self.timestamp_by_run = dict() - - self.tray = QSystemTrayIcon(icon) - self.tray.setToolTip(self.windowTitle()) - self.tray.activated.connect(self._on_tray_activated) - self.tray.show() - - self.chart_view = MyChartViewToolTips(self.timestamp_by_run) - self.chart_view.setRenderHint(QPainter.Antialiasing) - - self.table_run = TableWidgetRun() - self.table_run.layout().setContentsMargins(0, 0, 0, 0) - - self.tab_widget = QTabWidget() - self.tab_widget.addTab(self.chart_view, 'CHART') - self.tab_widget.addTab(self.table_run, 'TABLE RUN') - self.tab_widget.addTab(CurrentAssignedOpenIssues(), 'Current Assigned Open Issues') - - self.pb_refresh = QPushButton('REFRESH') - self.pb_refresh.clicked.connect(self.refresh) - - main_layout = QVBoxLayout() - main_layout.addWidget(self.tab_widget) - main_layout.addWidget(self.pb_refresh) - - central_widget = QWidget() - central_widget.setLayout(main_layout) - - self.setCentralWidget(central_widget) - - def _fill_chart(self, items: List[Run]): - series = QLineSeries() - series.setPointsVisible(True) - series.setPointLabelsVisible(True) - series.setPointLabelsFormat("@yPoint") - series.hovered.connect(self.chart_view.show_series_tooltip) - - self.timestamp_by_run.clear() - - for run in items: - date_value = calendar.timegm(run.date.timetuple()) * 1000 - total_issues = run.get_total_issues() - series.append(date_value, total_issues) - - self.timestamp_by_run[date_value] = run - - chart = QChart() - chart.setTheme(QChart.ChartThemeDark) - chart.setAnimationOptions(QChart.SeriesAnimations) - chart.addSeries(series) - chart.legend().hide() - - # No margin - chart.layout().setContentsMargins(0, 0, 0, 0) - chart.setBackgroundRoundness(0) - - axisX = QDateTimeAxis() - axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') - chart.addAxis(axisX, Qt.AlignBottom) - series.attachAxis(axisX) - - axisY = QValueAxis() - axisY.setLabelFormat('%d') - axisY.setTitleText('Total issues') - chart.addAxis(axisY, Qt.AlignLeft) - series.attachAxis(axisY) - - self.chart_view.clear_all_tooltips() - self.chart_view.setChart(chart) - - def refresh(self): - items = list(Run.select()) - self._fill_chart(items) - - items.reverse() - self.table_run.refresh(items) - - self.setWindowTitle( - WINDOW_TITLE + ". Last refresh date: " + DT.datetime.now().strftime('%d/%m/%Y %H:%M:%S') - ) - - def _on_tray_activated(self, reason): - self.setVisible(not self.isVisible()) - - if self.isVisible(): - self.showNormal() - self.activateWindow() - - def changeEvent(self, event: QEvent): - if event.type() == QEvent.WindowStateChange: - # Если окно свернули - if self.isMinimized(): - # Прячем окно с панели задач - QTimer.singleShot(0, self.hide) - - -if __name__ == '__main__': - app = QApplication([]) - - mw = MainWindow() - mw.resize(1200, 800) - mw.show() - - mw.refresh() - - app.exec() diff --git a/parse_jira_Assigned_Open_Issues_per_Project/main.py b/parse_jira_Assigned_Open_Issues_per_Project/main.py deleted file mode 100644 index 7002e24e6..000000000 --- a/parse_jira_Assigned_Open_Issues_per_Project/main.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -sys.path.append('../wait') -from wait import wait - -from common import get_table, logger -from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project -import db - - -def run(): - while True: - try: - logger.info(f'Начало') - - assigned_open_issues_per_project = get_assigned_open_issues_per_project() - logger.info( - 'Всего задач: %s\n\n%s\n', - sum(assigned_open_issues_per_project.values()), - get_table(assigned_open_issues_per_project) - ) - - ok = db.add(assigned_open_issues_per_project) - if ok is None: - logger.info("Количество открытых задач в проектах не поменялось. Пропускаю...") - elif ok: - logger.info("Добавляю запись") - else: - logger.info("Сегодня запись уже была добавлена. Пропускаю...") - - logger.info('\n' + '-' * 100 + '\n') - break - - except Exception: - logger.exception('Ошибка:') - - logger.info('Через 15 минут попробую снова...') - wait(minutes=15) - - -if __name__ == '__main__': - # pip install schedule - import schedule - import time - - # Каждую неделю, в субботу, в 12:00 - schedule\ - .every().week\ - .saturday.at("12:00")\ - .do(run) - - while True: - schedule.run_pending() - time.sleep(60) diff --git a/parse_jira_logged_time/favicon.ico b/parse_jira_logged_time/favicon.ico deleted file mode 100644 index b5cb32db6..000000000 Binary files a/parse_jira_logged_time/favicon.ico and /dev/null differ diff --git a/parse_jira_logged_time/gui.py b/parse_jira_logged_time/gui.py deleted file mode 100644 index 1e39c129f..000000000 --- a/parse_jira_logged_time/gui.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from PyQt5.Qt import ( - QApplication, QMessageBox, QThread, pyqtSignal, QMainWindow, QPushButton, QCheckBox, QPlainTextEdit, - QVBoxLayout, QHBoxLayout, QTextOption, QTableWidget, QWidget, QSizePolicy, QSplitter, Qt, QTableWidgetItem, - QProgressDialog, QHeaderView, QSystemTrayIcon, QIcon, QEvent, QTimer -) - -from main import ( - get_rss_jira_log, parse_logged_dict, get_logged_list_by_now_utc_date, get_logged_total_seconds, - get_sorted_logged, seconds_to_str -) - - -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print(text) - QMessageBox.critical(None, 'Error', text) - sys.exit(1) - - -import sys -sys.excepthook = log_uncaught_exceptions - - -class RunFuncThread(QThread): - run_finished = pyqtSignal(object) - - def __init__(self, func): - super().__init__() - - self.func = func - - def run(self): - self.run_finished.emit(self.func()) - - -WINDOW_TITLE = 'parse_jira_logged_time' - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle(WINDOW_TITLE) - - from pathlib import Path - file_name = str(Path(__file__).resolve().parent / 'favicon.ico') - icon = QIcon(file_name) - - self.setWindowIcon(icon) - - self.tray = QSystemTrayIcon(icon) - self.tray.setToolTip(self.windowTitle()) - self.tray.activated.connect(self._on_tray_activated) - self.tray.show() - - self.logged_dict = dict() - - self.pb_refresh = QPushButton('REFRESH') - self.pb_refresh.clicked.connect(self.refresh) - - self.cb_show_log = QCheckBox() - self.cb_show_log.setChecked(True) - - self.log = QPlainTextEdit() - self.log.setReadOnly(True) - self.log.setWordWrapMode(QTextOption.NoWrap) - log_font = self.log.font() - log_font.setFamily('Courier New') - self.log.setFont(log_font) - - self.cb_show_log.clicked.connect(self.log.setVisible) - self.log.setVisible(self.cb_show_log.isChecked()) - - header_labels = ['DATE', 'TOTAL LOGGED TIME'] - self.table_logged = QTableWidget() - self.table_logged.setEditTriggers(QTableWidget.NoEditTriggers) - self.table_logged.setSelectionBehavior(QTableWidget.SelectRows) - self.table_logged.setSelectionMode(QTableWidget.SingleSelection) - self.table_logged.setColumnCount(len(header_labels)) - self.table_logged.setHorizontalHeaderLabels(header_labels) - self.table_logged.horizontalHeader().setStretchLastSection(True) - self.table_logged.itemClicked.connect(self._on_table_logged_item_clicked) - - header_labels = ['TIME', 'LOGGED', 'JIRA'] - self.table_logged_info = QTableWidget() - self.table_logged_info.setEditTriggers(QTableWidget.NoEditTriggers) - self.table_logged_info.setSelectionBehavior(QTableWidget.SelectRows) - self.table_logged_info.setSelectionMode(QTableWidget.SingleSelection) - self.table_logged_info.setColumnCount(len(header_labels)) - self.table_logged_info.setHorizontalHeaderLabels(header_labels) - self.table_logged_info.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) - self.table_logged_info.horizontalHeader().setStretchLastSection(True) - self.table_logged_info.itemDoubleClicked.connect(self._on_table_logged_info_item_double_clicked) - - main_layout = QVBoxLayout() - - central_widget = QWidget() - central_widget.setLayout(main_layout) - - self.setCentralWidget(central_widget) - - self.pb_refresh.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) - - h_layout = QHBoxLayout() - h_layout.addWidget(self.pb_refresh) - h_layout.addWidget(self.cb_show_log) - - layout_table_widget = QVBoxLayout() - layout_table_widget.setContentsMargins(0, 0, 0, 0) - layout_table_widget.addWidget(self.table_logged) - layout_table_widget.addWidget(self.table_logged_info) - - table_widget = QWidget() - table_widget.setLayout(layout_table_widget) - - splitter = QSplitter(Qt.Horizontal) - splitter.addWidget(table_widget) - splitter.addWidget(self.log) - - main_layout.addLayout(h_layout) - main_layout.addWidget(splitter) - - def _fill_tables(self, xml_data: bytes): - import io - buffer_io = io.StringIO() - - from contextlib import redirect_stdout - - try: - with redirect_stdout(buffer_io): - print(len(xml_data), repr(xml_data[:50])) - - # Структура документа -- xml - self.logged_dict = parse_logged_dict(xml_data) - print(self.logged_dict) - - if not self.logged_dict: - return - - import json - print(json.dumps(self.logged_dict, indent=4, ensure_ascii=False)) - print() - - logged_list = get_logged_list_by_now_utc_date(self.logged_dict) - - logged_total_seconds = get_logged_total_seconds(logged_list) - logged_total_seconds_str = seconds_to_str(logged_total_seconds) - print('entry_logged_list:', logged_list) - print('today seconds:', logged_total_seconds) - print('today time:', logged_total_seconds_str) - print() - - # Для красоты выводим результат в табличном виде - lines = [] - - # Удаление строк таблицы - while self.table_logged.rowCount(): - self.table_logged.removeRow(0) - - for i, (date_str, logged_list) in enumerate(get_sorted_logged(self.logged_dict)): - total_seconds = get_logged_total_seconds(logged_list) - total_seconds_str = seconds_to_str(total_seconds) - row = date_str, total_seconds_str, total_seconds - lines.append(row) - - self.table_logged.setRowCount(self.table_logged.rowCount() + 1) - - self.table_logged.setItem(i, 0, QTableWidgetItem(date_str)) - - item = QTableWidgetItem(total_seconds_str) - item.setToolTip('Total seconds: {}'.format(total_seconds)) - self.table_logged.setItem(i, 1, item) - - self.table_logged.setCurrentCell(0, 0) - self.table_logged.setFocus() - self._on_table_logged_item_clicked(self.table_logged.currentItem()) - - # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина - max_len_columns = [max(map(len, map(str, col))) for col in zip(*lines)] - - # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" - my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) - - for line in lines: - print(my_table_format.format(*line)) - - finally: - text = buffer_io.getvalue() - self.log.setPlainText(text) - - print(text) - - def refresh(self): - progress_dialog = QProgressDialog(self) - - thread = RunFuncThread(func=get_rss_jira_log) - thread.run_finished.connect(self._fill_tables) - thread.run_finished.connect(progress_dialog.close) - thread.start() - - progress_dialog.setWindowTitle('Please wait...') - progress_dialog.setLabelText(progress_dialog.windowTitle()) - progress_dialog.setRange(0, 0) - progress_dialog.exec() - - from datetime import datetime - self.setWindowTitle(WINDOW_TITLE + ". Last refresh date: " + datetime.now().strftime('%d/%m/%Y %H:%M:%S')) - - def _on_table_logged_item_clicked(self, item: QTableWidgetItem): - # Удаление строк таблицы - while self.table_logged_info.rowCount(): - self.table_logged_info.removeRow(0) - - row = item.row() - date_str = self.table_logged.item(row, 0).text() - logged_list = self.logged_dict[date_str] - logged_list = reversed(logged_list) - - for i, logged in enumerate(logged_list): - self.table_logged_info.setRowCount(self.table_logged_info.rowCount() + 1) - - self.table_logged_info.setItem(i, 0, QTableWidgetItem(logged['time'])) - self.table_logged_info.setItem(i, 1, QTableWidgetItem(logged['logged_human_time'])) - - item = QTableWidgetItem(logged['jira_id']) - item.setToolTip(logged['jira_title']) - self.table_logged_info.setItem(i, 2, item) - - def _on_table_logged_info_item_double_clicked(self, item: QTableWidgetItem): - row = item.row() - jira_id = self.table_logged_info.item(row, 2).text() - - url = 'https://jira.compassplus.ru/browse/' + jira_id - - import webbrowser - webbrowser.open(url) - - def _on_tray_activated(self, reason): - self.setVisible(not self.isVisible()) - - if self.isVisible(): - self.showNormal() - self.activateWindow() - - def changeEvent(self, event: QEvent): - if event.type() == QEvent.WindowStateChange: - # Если окно свернули - if self.isMinimized(): - # Прячем окно с панели задач - QTimer.singleShot(0, self.hide) - - -if __name__ == '__main__': - app = QApplication([]) - - mw = MainWindow() - mw.resize(1200, 800) - mw.show() - - mw.refresh() - - app.exec() diff --git a/parse_jira_logged_time/main.py b/parse_jira_logged_time/main.py deleted file mode 100644 index cc9f97e2b..000000000 --- a/parse_jira_logged_time/main.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from bs4 import BeautifulSoup -from datetime import datetime, timezone -import re -from typing import Dict, List, Tuple - -import sys -sys.path.append('..') - -from logged_human_time_to_seconds import logged_human_time_to_seconds -from seconds_to_str import seconds_to_str - -URL = 'https://jira.compassplus.ru/activity?maxResults=100&streams=user+IS+ipetrash&os_authType=basic&title=undefined' -HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', -} - -# NOTE. Get : openssl pkcs12 -nodes -out key.pem -in file.p12 -PEM_FILE_NAME = 'ipetrash.pem' - -from pathlib import Path - -# CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) -PEM_FILE_NAME = str(Path(__file__).resolve().parent / PEM_FILE_NAME) - - -# SOURCE: https://stackoverflow.com/a/13287083/5909792 -def utc_to_local(utc_dt: datetime) -> datetime: - return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) - - -def get_rss_jira_log() -> bytes: - import requests - rs = requests.get(URL, headers=HEADERS, cert=PEM_FILE_NAME) - # print(rs) - - return rs.content - - -def get_logged_dict(root) -> Dict[str, List[Dict]]: - from collections import defaultdict - logged_dict = defaultdict(list) - - for entry in root.select('entry'): - # Ищем в строку с логированием - match = re.search("logged '(.+?)'", entry.text, flags=re.IGNORECASE) - if not match: - continue - - # TODO: удалить - # title = entry.title - # - # # Содержимое тега title -- экранированное html - # title_node = BeautifulSoup(title.text, 'html.parser') - # - # title_text = title_node.text.strip() - # title_text = re.sub('\s{2,}', ' ', title_text) - # - # # Ищем в - # # Пример: "Ilya A. Petrash logged '30 minutes' on ..." - # match = re.search("Ilya A. Petrash logged '(.+?)'", title_text, flags=re.IGNORECASE) - # if not match: - # if not entry.content: - # continue - # - # # Если в <title> не нашли, ищем в <content> - # # Пример: <li>Logged '30 minutes' - # match = re.search("logged '(.+?)'", entry.content.text, flags=re.IGNORECASE) - # if not match: - # continue - - logged_human_time = match.group(1) - logged_seconds = logged_human_time_to_seconds(logged_human_time) - - jira_id = entry.object.title.text.strip() - jira_title = entry.object.summary.text.strip() - - entry_dt = datetime.strptime(entry.published.text, "%Y-%m-%dT%H:%M:%S.%fZ") - - # Переменная entry_dt имеет время в UTC, и желательно его привести в локальное время - entry_dt = utc_to_local(entry_dt) - - entry_date = entry_dt.date() - date_str = entry_date.strftime('%d/%m/%Y') - - logged_dict[date_str].append({ - 'date_time': entry_dt.strftime('%d/%m/%Y %H:%M:%S'), - 'date': entry_dt.strftime('%d/%m/%Y'), - 'time': entry_dt.strftime('%H:%M:%S'), - 'logged_human_time': logged_human_time, - 'logged_seconds': logged_seconds, - 'jira_id': jira_id, - 'jira_title': jira_title, - }) - - return logged_dict - - -def parse_logged_dict(xml_data: bytes) -> Dict[str, List[Dict]]: - # Структура документа -- xml - root = BeautifulSoup(xml_data, 'xml') - - return get_logged_dict(root) - - -def get_sorted_logged(date_str_by_logged_list: Dict, reverse=True) -> List[Tuple[str, List[Dict]]]: - sorted_items = date_str_by_logged_list.items() - sorted_items = sorted(sorted_items, key=lambda x: datetime.strptime(x[0], '%d/%m/%Y'), reverse=reverse) - - return list(sorted_items) - - -def get_logged_list_by_now_utc_date(date_str_by_entry_logged_list: Dict[str, List[Dict]]) -> List[Dict]: - current_utc_date_str = datetime.utcnow().strftime('%d/%m/%Y') - return date_str_by_entry_logged_list.get(current_utc_date_str, []) - - -def get_logged_total_seconds(entry_logged_list: List[Dict]) -> int: - return sum(entry['logged_seconds'] for entry in entry_logged_list) - - -if __name__ == '__main__': - xml_data = get_rss_jira_log() - print(len(xml_data), repr(xml_data[:50])) - - # open('rs.xml', 'wb').write(xml_data) - # xml_data = open('rs.xml', 'rb').read() - - # Структура документа -- xml - logged_dict = parse_logged_dict(xml_data) - print(logged_dict) - - import json - print(json.dumps(logged_dict, indent=4, ensure_ascii=False)) - print() - - logged_list = get_logged_list_by_now_utc_date(logged_dict) - logged_total_seconds = get_logged_total_seconds(logged_list) - print('entry_logged_list:', logged_list) - print('today seconds:', logged_total_seconds) - print('today time:', seconds_to_str(logged_total_seconds)) - print() - - # Для красоты выводим результат в табличном виде - lines = [] - - for date_str, logged_list in get_sorted_logged(logged_dict): - total_seconds = get_logged_total_seconds(logged_list) - lines.append((date_str, total_seconds, seconds_to_str(total_seconds))) - - # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина - max_len_columns = [max(map(len, map(str, col))) for col in zip(*lines)] - - # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" - my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) - - for line in lines: - print(my_table_format.format(*line)) diff --git a/parse_magnet_links.py b/parse_magnet_links.py index 0a04b20ae..1e6a09090 100644 --- a/parse_magnet_links.py +++ b/parse_magnet_links.py @@ -1,29 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://en.wikipedia.org/wiki/Magnet_URI_scheme # https://ru.wikipedia.org/wiki/Magnet-ссылка + +from urllib.parse import urlparse, parse_qs + + def parse(url: str) -> dict: - from urllib.parse import urlparse, parse_qs result = urlparse(url) return parse_qs(result.query) -if __name__ == '__main__': +if __name__ == "__main__": + import json + links = [ - 'magnet:?xt=urn:btih:19270c27523f428d6b9d23c264e4a0ff57869275&dn=rutor.info_%D0%94%D0%BE%D0%BA%D1%82%D0%BE%D1%80+%D0%A8%D0%B0%D0%BD%D1%81+%2F+Chance+%5B02x01-04+%D0%B8%D0%B7+10%5D+%282017%29+WEB-DLRip+1080p+%7C+Profix+Media&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xt=urn:btih:1ff5a51d2f65e7c2fa200ada0896075543398a73&dn=rutor.info_Heroes+%26+Generals+%5B12.10.17%5D+%282016%29+PC+%7C+Online-only&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xt=urn:btih:08c3a30e88b38ccf6926c073ecd41f983cbf2b20&dn=rutor.info_%D0%A4%D0%B8%D0%B7%D1%80%D1%83%D0%BA+%5B04%D1%8501-10+%D0%B8%D0%B7+21%5D+%282017%29+HDTVRip-AVC+%D0%BE%D1%82+GeneralFilm&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xl=10826029&dn=mediawiki-1.15.1.tar.gz&xt=urn:sha1:XRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&as=https%3A%2F%2Freleases.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz', + "magnet:?xt=urn:btih:19270c27523f428d6b9d23c264e4a0ff57869275&dn=rutor.info_%D0%94%D0%BE%D0%BA%D1%82%D0%BE%D1%80+%D0%A8%D0%B0%D0%BD%D1%81+%2F+Chance+%5B02x01-04+%D0%B8%D0%B7+10%5D+%282017%29+WEB-DLRip+1080p+%7C+Profix+Media&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xt=urn:btih:1ff5a51d2f65e7c2fa200ada0896075543398a73&dn=rutor.info_Heroes+%26+Generals+%5B12.10.17%5D+%282016%29+PC+%7C+Online-only&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xt=urn:btih:08c3a30e88b38ccf6926c073ecd41f983cbf2b20&dn=rutor.info_%D0%A4%D0%B8%D0%B7%D1%80%D1%83%D0%BA+%5B04%D1%8501-10+%D0%B8%D0%B7+21%5D+%282017%29+HDTVRip-AVC+%D0%BE%D1%82+GeneralFilm&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xl=10826029&dn=mediawiki-1.15.1.tar.gz&xt=urn:sha1:XRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&as=https%3A%2F%2Freleases.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz", ] - import json pretty = lambda data: json.dumps(data, indent=4, ensure_ascii=False) for i, link in enumerate(links, 1): data = parse(link) - print('{}.\n{}\n'.format(i, pretty(data))) + print(f"{i}.\n{pretty(data)}\n") diff --git a/parse_molecule__Molecule to atoms.py b/parse_molecule__Molecule to atoms.py deleted file mode 100644 index 15cea451d..000000000 --- a/parse_molecule__Molecule to atoms.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -My solution from codewars. - -""" - - -def get_sub_formula(formula): - """ - "(SO3)2" -> ("(SO3)2", "(SO3)", "2") - "(SO3)" -> ("(SO3)", "(SO3)", "") - - """ - - import re - match = re.search('(\([a-zA-Z\d]+\))(\d*)', formula) - if not match: - return - - full_sub_formula = match.group(0) - sub_formula = match.group(1) - multiplier = match.group(2) - - return full_sub_formula, sub_formula, multiplier - - -def split_by_full_tokens(formula): - """ - 'Fe7S10FBO3' -> ['Fe7', 'S10', 'F', 'B', 'O3'] - - """ - - import re - return re.findall('([A-Z][a-z]*\d*)', formula) - - -def split_by_tokens(formula): - """ - 'Fe7S10F1B1O3' -> ['Fe', '7', 'S', '10', 'F', '1', 'B', '1', 'O', '3'] - - """ - - import re - return re.findall('([A-Z][a-z]*|\d+)', formula) - - -def complete_element_atom_number_with_one_atom(formula): - """ - Append '1' for elements without atom number: 'FeK' -> 'Fe1K1' - - """ - - tokens = split_by_full_tokens(formula) - tokens = [token if token[-1] in '0123456789' else token + '1' for token in tokens] - return ''.join(tokens) - - -def parse_molecule(formula): - formula = formula.replace('[', '(').replace(']', ')') - formula = formula.replace('{', '(').replace('}', ')') - - while '(' in formula: - match = get_sub_formula(formula) - if not match: - break - - full_sub_formula, sub_formula, multiplier = match - new_sub_formula = complete_element_atom_number_with_one_atom(sub_formula) - - if multiplier: - multiplier = int(multiplier) - - tokens = split_by_tokens(new_sub_formula) - - # ['K', '4', 'O', '2'] * 2 -> ['K', '8', 'O', '4'] - tokens = [str(int(token) * multiplier) if token.isdigit() else token for token in tokens] - new_sub_formula = ''.join(tokens) - - formula = formula.replace(full_sub_formula, new_sub_formula) - - formula = complete_element_atom_number_with_one_atom(formula) - tokens = split_by_tokens(formula) - - from collections import defaultdict - element_by_number_atom = defaultdict(int) - - for i in range(0, len(tokens), 2): - element_by_number_atom[tokens[i]] += int(tokens[i + 1]) - - return element_by_number_atom - - -if __name__ == '__main__': - def equals_atomically(obj1, obj2): - if len(obj1) != len(obj2): - return False - for k in obj1: - if obj1[k] != obj2[k]: - return False - return True - - assert equals_atomically(parse_molecule("H2O"), {'H': 2, 'O': 1}), "Should parse water" - assert equals_atomically(parse_molecule("Mg(OH)2"), {'Mg': 1, 'O' : 2, 'H': 2}), "Should parse magnesium hydroxide: Mg(OH)2" - assert equals_atomically(parse_molecule("K4[ON(SO3)2]2"), {'K': 4, 'O': 14, 'N': 2, 'S': 4}), "Should parse Fremy's salt: K4[ON(SO3)2]2" diff --git a/parse_test_progress.py b/parse_test_progress.py index af2e0d2a0..306f1b0bd 100644 --- a/parse_test_progress.py +++ b/parse_test_progress.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def parse(text, append_test_case_list=True): +import re + + +def parse(text, append_test_case_list=True) -> None: total_value = 0 total_max_value = 0 @@ -14,18 +17,17 @@ def parse(text, append_test_case_list=True): print(line) continue - import re - match = re.search(r'{@} +/ +(\d+)', line) + match = re.search(r"{@} +/ +(\d+)", line) if not match: print(line) continue max_value = int(match.group(1)) - match = re.search('{#}:(.*)$', line) + match = re.search("{#}:(.*)$", line) if match: # Получение списка значений - values = match.group(1).split(',') + values = match.group(1).split(",") # Удаление пустых символов values = map(str.strip, values) @@ -41,21 +43,23 @@ def parse(text, append_test_case_list=True): total_value += value total_max_value += max_value - line = line.replace('{@}', str(value)) - line = line.replace('{#}', "({}%)".format(int(value / max_value * 100))) + line = line.replace("{@}", str(value)) + line = line.replace("{#}", f"({int(value / max_value * 100)}%)") if not append_test_case_list: - index = line.rfind(':') + index = line.rfind(":") if index != -1: line = line[:index] print(line) print() - print("Итого: {} / {} ({}%)".format(total_value, total_max_value, int(total_value / total_max_value * 100))) + print( + f"Итого: {total_value} / {total_max_value} ({int(total_value / total_max_value * 100)}%)" + ) -if __name__ == '__main__': +if __name__ == "__main__": text = """\ Набивка тестов: ATM - VSDC Issuing: {@} / 48 {#}: diff --git a/parse_text_http_headers.py b/parse_text_http_headers.py index bafe092e8..7132c30ee 100644 --- a/parse_text_http_headers.py +++ b/parse_text_http_headers.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re -from typing import Dict -HTTP_HEADER_PATTERN = re.compile(r'([\w-]+): (.*)', flags=re.IGNORECASE) +HTTP_HEADER_PATTERN = re.compile(r"([\w-]+): (.*)", flags=re.IGNORECASE) -def parse(text: str) -> Dict[str, str]: +def parse(text: str) -> dict[str, str]: return dict(HTTP_HEADER_PATTERN.findall(text)) -if __name__ == '__main__': +if __name__ == "__main__": text_http_headers = """ POST /index.php?do=search HTTP/1.1 @@ -39,18 +38,18 @@ def parse(text: str) -> Dict[str, str]: headers = parse(text_http_headers) print(headers) assert headers == { - 'Host': 'online.anidub.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate, br', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': '112', - 'Origin': 'https://online.anidub.com', - 'Connection': 'keep-alive', - 'Referer': 'https://online.anidub.com/index.php?do=search', - 'Upgrade-Insecure-Requests': '1', - 'TE': 'Trailers', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache' + "Host": "online.anidub.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": "112", + "Origin": "https://online.anidub.com", + "Connection": "keep-alive", + "Referer": "https://online.anidub.com/index.php?do=search", + "Upgrade-Insecure-Requests": "1", + "TE": "Trailers", + "Pragma": "no-cache", + "Cache-Control": "no-cache", } diff --git a/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py b/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py deleted file mode 100644 index 4dea2cb26..000000000 --- a/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def download_torrent_file(torrent_file_url): - """ - Функция скачает по ссылке торрент файл и вернет его название. - Если не получится, вернет None - - """ - - user_agent = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' - - import requests - rs = requests.get(torrent_file_url, headers={'User-Agent': user_agent}) - if not rs.ok: - print('Не получилось скачать: {}\n\n{}'.format(rs.status_code, rs.text)) - return - - # Теперь нужно вытащить название торрент-файла - file_name = rs.headers['Content-Disposition'] - file_name = file_name.replace('attachment; filename=', '').replace('"', '') - - with open(file_name, 'wb') as f: - f.write(rs.text.encode()) - - return file_name - - -import re -GET_MINIMAL_TORRENT_URL = re.compile(r'(https?://.+/torrent/\d+/?)') - - -def get_download_url_from_torrent_url(torrent_url): - """ - Функция по ссылке на торрент вернет ссылку на торрент-файл. - Если не получится, вернется None. - - """ - - match = GET_MINIMAL_TORRENT_URL.search(torrent_url) - if match: - torrent_url = match.group(1) - download_torrent_url = torrent_url.replace('/torrent/', '/download/') - return download_torrent_url - - -if __name__ == '__main__': - torrent_url = 'http://anti-tor.org/torrent/539888/7-days-to-die-v-15.1-2013-pc-repack-ot-pioneer' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) - - torrent_url = 'http://anti-tor.org/torrent/474477/hyperdimension-neptunia-rebirth2-sisters-generation-2015-pc-repack-ot-r.g-games' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) - - torrent_url = 'http://anti-tor.org/torrent/543446/the-binding-of-isaac-rebirth-complete-bundle-v.1.??-2014-pc-steam-rip-ot-letsrlay' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) diff --git a/parse_torrent_sites/rutor/get_info_from_torrent_page.py b/parse_torrent_sites/rutor/get_info_from_torrent_page.py deleted file mode 100644 index 494b789a5..000000000 --- a/parse_torrent_sites/rutor/get_info_from_torrent_page.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# TODO: парсинг страницы торрент, вытаскивание характеристик, описания, скриншотов, торрент-файла и магнет-ссылки - -from bs4 import BeautifulSoup -root = BeautifulSoup(open('1.htm', 'rb'), 'lxml') - -details = root.select_one('#details') -print(details.text.strip()) diff --git a/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py b/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py deleted file mode 100644 index 8e348f164..000000000 --- a/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_rutor_torrent_download_info(torrent_url): - """ - Parse torrent url and return tuple: (torrent_file_url, magnet_url, info_hash) - - """ - - import requests - rs = requests.get(torrent_url) - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - magnet_url = root.select_one('#download > a[href^="magnet"]')['href'] - - # For get info hash from magnet url - import re - match = re.compile(r'btih:([abcdef\d]+?)&', flags=re.IGNORECASE).search(magnet_url) - if match: - info_hash = match.group(1) - - return torrent_url.replace('/torrent/', '/download/'), magnet_url, info_hash - - -if __name__ == '__main__': - torrent_url = 'http://anti-tor.org/torrent/544942' - torrent_file_url, magnet_url, info_hash = get_rutor_torrent_download_info(torrent_url) - print(torrent_file_url) - print(magnet_url) - print(info_hash) diff --git a/parsing_profile_twitter.py b/parsing_profile_twitter.py deleted file mode 100644 index 9a93b2a87..000000000 --- a/parsing_profile_twitter.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# TODO: from dev_window - -# from PySide.QtWebKit import * -# -# QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) -# -# self.view = QWebView() -# self.setCentralWidget(self.view) -# -# self.view.load('https://twitter.com/Misty_Shadow') -# self.doc = self.view.page().mainFrame().documentElement() -# -# def el_text(el, css_selector): -# return el.findFirst(css_selector).toPlainText() -# -# profile = self.doc.findFirst('.ProfileHeaderCard') -# -# profile_info = { -# 'name': el_text(profile, '.ProfileHeaderCard-name'), -# 'screenname': el_text(profile, '.ProfileHeaderCard-screenname'), -# 'location': el_text(profile, '.ProfileHeaderCard-location'), -# 'url': el_text(profile, '.ProfileHeaderCard-url'), -# 'joinDate': el_text(profile, '.ProfileHeaderCard-joinDate'), -# 'birthdate': el_text(profile, '.ProfileHeaderCard-birthdate'), -# } -# -# text_mess = '' -# -# for k in sorted(profile_info.keys()): -# text_mess += '{}: {}'.format(k, profile_info[k].strip()) + '\n' -# -# print(text_mess) -# -# QMessageBox.information(self, None, text_mess) \ No newline at end of file diff --git a/password_hash.py b/password_hash.py index 2a85fc1b8..f54011003 100644 --- a/password_hash.py +++ b/password_hash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib @@ -14,13 +14,19 @@ def get_password_hash(name: str, password: str) -> str: if not password: raise ValueError('"password" is empty!') - data = (name.upper() + "-" + password).encode('UTF-8') + data = (name.upper() + "-" + password).encode("UTF-8") return hashlib.sha256(data).hexdigest().upper() -if __name__ == '__main__': - print(get_password_hash('abc', '123')) - assert get_password_hash('abc', '123') == '4D156D7BC9C38C000AA1E5B06E46E1F40CAFE651F3CABFAE9BAD48803BAF17BF' +if __name__ == "__main__": + print(get_password_hash("abc", "123")) + assert ( + get_password_hash("abc", "123") + == "4D156D7BC9C38C000AA1E5B06E46E1F40CAFE651F3CABFAE9BAD48803BAF17BF" + ) - print(get_password_hash('123', 'abc')) - assert get_password_hash('123', 'abc') == '84A990F85C3CDD4A5FE71B027996AF81E3F87062BE029FAC4C09DDC40B3C42F0' + print(get_password_hash("123", "abc")) + assert ( + get_password_hash("123", "abc") + == "84A990F85C3CDD4A5FE71B027996AF81E3F87062BE029FAC4C09DDC40B3C42F0" + ) diff --git a/pathlib__examples/create_file_in_user_directory.py b/pathlib__examples/create_file_in_user_directory.py index 32fabc505..53f16591d 100644 --- a/pathlib__examples/create_file_in_user_directory.py +++ b/pathlib__examples/create_file_in_user_directory.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path -f = Path('~/test_file.txt').expanduser() + + +f = Path("~/test_file.txt").expanduser() print(f.exists()) # False print(f) # C:\Users\ipetrash\test_file.txt print(f.exists()) # False -print(f.write_text('Hello World', encoding='utf-8')) # 11 +print(f.write_text("Hello World", encoding="utf-8")) # 11 print(f.exists()) # True -print(f.read_text(encoding='utf-8')) # Hello World +print(f.read_text(encoding="utf-8")) # Hello World print(f.unlink()) # None print(f.exists()) # False diff --git a/pathlib__examples/get_filename__without_suffix.py b/pathlib__examples/get_filename__without_suffix.py index 69a42188d..e5a62f038 100644 --- a/pathlib__examples/get_filename__without_suffix.py +++ b/pathlib__examples/get_filename__without_suffix.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib -file_name = 'C:/Users/111/video.mp4' + +file_name = "C:/Users/111/video.mp4" path = pathlib.Path(file_name) print(path.name) # video.mp4 print(path.stem) # video print() -file_name = 'video.mp4' +file_name = "video.mp4" path = pathlib.Path(file_name) print(path.name) # video.mp4 print(path.stem) # video diff --git a/pathlib__examples/get_user_directory.py b/pathlib__examples/get_user_directory.py index 499e27370..25ee45bd3 100644 --- a/pathlib__examples/get_user_directory.py +++ b/pathlib__examples/get_user_directory.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path -print(Path('~').expanduser()) -print(str(Path('~').expanduser())) + + +print(Path("~").expanduser()) +print(str(Path("~").expanduser())) diff --git a/pathlib__examples/print_current_files.py b/pathlib__examples/print_current_files.py index b2052c4a6..2baca32b5 100644 --- a/pathlib__examples/print_current_files.py +++ b/pathlib__examples/print_current_files.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib + + current_dir = pathlib.Path(__file__).parent print(current_dir) -for file_name in current_dir.glob('*.py'): - print(' {}:\n {}\n'.format(file_name, file_name.read_bytes())) +for file_name in current_dir.glob("*.py"): + print(f" {file_name}:\n {file_name.read_bytes()}\n") diff --git a/pathlib__examples/search_and_rename_directory_in_hierarchy.py b/pathlib__examples/search_and_rename_directory_in_hierarchy.py index d36883e8f..7d10927c3 100644 --- a/pathlib__examples/search_and_rename_directory_in_hierarchy.py +++ b/pathlib__examples/search_and_rename_directory_in_hierarchy.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path -os.makedirs('./dir1/dir2/dir3/dir4', exist_ok=True) - from pathlib import Path -n = 'dir3' -m = 'new_dir3' + +os.makedirs("./dir1/dir2/dir3/dir4", exist_ok=True) + +n = "dir3" +m = "new_dir3" # Рекурсивный поиск -items = list(Path('.').rglob(n)) +items = list(Path(".").rglob(n)) # Если нашли папку if items: diff --git a/pathlib__examples/search_with_glob.py b/pathlib__examples/search_with_glob.py index 7470c27bc..d99333c86 100644 --- a/pathlib__examples/search_with_glob.py +++ b/pathlib__examples/search_with_glob.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib # Print current folders and files -for file_name in pathlib.Path().glob('*'): +for file_name in pathlib.Path().glob("*"): # print abs path print(file_name.resolve()) print() # Print current folders and files with filter -for file_name in pathlib.Path().glob('get_*'): +for file_name in pathlib.Path().glob("get_*"): # print abs path print(file_name.resolve()) @@ -23,6 +23,6 @@ # for file_name in pathlib.Path('../').rglob('*'): # OR: -for file_name in pathlib.Path('../').glob('**/*'): +for file_name in pathlib.Path("../").glob("**/*"): # print abs path print(file_name.resolve()) diff --git a/pathlib__examples/syntax_path.py b/pathlib__examples/syntax_path.py index 8d1167ca4..c1957a6e2 100644 --- a/pathlib__examples/syntax_path.py +++ b/pathlib__examples/syntax_path.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# C:\Windows\System32\Boot import pathlib -path = pathlib.Path('C:/') / 'Windows' / 'System32' / 'Boot' + + +path = pathlib.Path("C:/") / "Windows" / "System32" / "Boot" print(path) # C:\Windows\System32\Boot diff --git a/pdf/extract_text__PyMuPDF.py b/pdf/extract_text__PyMuPDF.py new file mode 100644 index 000000000..9f655e124 --- /dev/null +++ b/pdf/extract_text__PyMuPDF.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install PyMuPDF +import fitz + + +with fitz.open("merger__PyPDF2/1.pdf") as doc: + items = [] + for page in doc: + items.append(page.get_text()) + text = "\n".join(items) + +print(text) diff --git a/pdf/merger__PyPDF2/1.pdf b/pdf/merger__PyPDF2/1.pdf new file mode 100644 index 000000000..846f6ae18 Binary files /dev/null and b/pdf/merger__PyPDF2/1.pdf differ diff --git a/pdf/merger__PyPDF2/2.pdf b/pdf/merger__PyPDF2/2.pdf new file mode 100644 index 000000000..aeedee9fc Binary files /dev/null and b/pdf/merger__PyPDF2/2.pdf differ diff --git a/pdf/merger__PyPDF2/3.pdf b/pdf/merger__PyPDF2/3.pdf new file mode 100644 index 000000000..671f69115 Binary files /dev/null and b/pdf/merger__PyPDF2/3.pdf differ diff --git a/pdf/merger__PyPDF2/main.py b/pdf/merger__PyPDF2/main.py new file mode 100644 index 000000000..41f4f5516 --- /dev/null +++ b/pdf/merger__PyPDF2/main.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pypdf2 +from PyPDF2 import PdfFileMerger + + +pdfs = ["1.pdf", "2.pdf", "3.pdf"] + +merger = PdfFileMerger() +for pdf in pdfs: + merger.append(pdf) + +merger.write("result.pdf") +merger.close() diff --git a/pdf/merger__PyPDF2/result.pdf b/pdf/merger__PyPDF2/result.pdf new file mode 100644 index 000000000..74cc7671f Binary files /dev/null and b/pdf/merger__PyPDF2/result.pdf differ diff --git a/peewee__examples/SqliteQueueDatabase/config.py b/peewee__examples/SqliteQueueDatabase/config.py new file mode 100644 index 000000000..0c98459a0 --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/config.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +DIR: Path = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" diff --git a/peewee__examples/SqliteQueueDatabase/db.py b/peewee__examples/SqliteQueueDatabase/db.py new file mode 100644 index 000000000..2ea9957cd --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/db.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import sys +import time +from typing import Type, Iterable, Self, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DB_FILE_NAME, DIR + +sys.path.append(str(DIR.parent)) +from shorten import shorten + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + def get_new(self) -> Self: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> Self: + return cls.select().first() + + @classmethod + def get_last(cls) -> Self: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type[Self]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Parameter(BaseModel): + name = TextField(primary_key=True) + value = TextField() + description = TextField(null=True) + + @classmethod + def get_by_name(cls, name: str) -> Self | None: + return cls.get_or_none(cls.name == name) + + @classmethod + def add( + cls, + name: str, + value: str, + description: str = None, + ) -> Self: + obj = cls.get_by_name(name) + if obj: + return obj + + return cls.create( + name=name, + value=value, + description=description, + ) + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() diff --git a/peewee__examples/SqliteQueueDatabase/main.py b/peewee__examples/SqliteQueueDatabase/main.py new file mode 100644 index 000000000..389edd94b --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/main.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import concurrent.futures +import db + + +def create_parameter(name: str, value: str) -> db.Parameter: + return db.Parameter.add(name, value) + + +with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + N = 10000 + + futures = [ + executor.submit(create_parameter, f"name_{i}", f"value_{i}") + for i in range(N) + ] + + items = [] + for future in concurrent.futures.as_completed(futures): + data: db.Parameter = future.result() + items.append(data) + + print("items:", len(items)) + print("count:", db.Parameter.count()) + assert len(items) == N diff --git a/peewee__examples/backup__examples/backup_via_api.py b/peewee__examples/backup__examples/backup_via_api.py new file mode 100644 index 000000000..bde8de1dd --- /dev/null +++ b/peewee__examples/backup__examples/backup_via_api.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +from pathlib import Path + +from peewee import SqliteDatabase + + +def backup(db: SqliteDatabase, file_name: Path | str) -> None: + dst = sqlite3.connect(file_name) + db.connection().backup(dst) + dst.close() + + +if __name__ == "__main__": + from common import run_test + + run_test( + backup=backup, + file_name_backup=Path(__file__).resolve().name + ".db", + ) diff --git a/peewee__examples/backup__examples/backup_via_vacuum_into.py b/peewee__examples/backup__examples/backup_via_vacuum_into.py new file mode 100644 index 000000000..522700bce --- /dev/null +++ b/peewee__examples/backup__examples/backup_via_vacuum_into.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from peewee import SqliteDatabase + + +def backup(db: SqliteDatabase, file_name: Path | str) -> None: + Path(file_name).unlink(missing_ok=True) + + db.connection().execute("VACUUM INTO ?", (str(file_name),)) + + +if __name__ == "__main__": + from common import run_test + + run_test( + backup=backup, + file_name_backup=Path(__file__).resolve().name + ".db", + ) diff --git a/peewee__examples/backup__examples/common.py b/peewee__examples/backup__examples/common.py new file mode 100644 index 000000000..c1178a22a --- /dev/null +++ b/peewee__examples/backup__examples/common.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +from pathlib import Path +from typing import Callable + +# pip install peewee +from peewee import Model, SqliteDatabase, TextField + + +db = SqliteDatabase("db.sqlite") + + +class Info(Model): + first_name = TextField() + second_name = TextField() + + class Meta: + database = db + + @classmethod + def fill(cls) -> None: + count = cls.select().count() + for i in range(5): + cls.create( + first_name=f"first_name_{i + count}", + second_name=f"second_name_{i + count}", + ) + + @classmethod + def print(cls) -> None: + for info in cls.select(): + print(info) + + @classmethod + def print_from(cls, file_name: str | Path) -> None: + connection = sqlite3.connect(file_name) + try: + for info in connection.execute(f"SELECT * FROM {cls._meta.table_name}"): + print(info) + except Exception as e: + print(f"[#] {e}") + + def __str__(self) -> str: + return f"Info<#{self.id} first_name={self.first_name!r} second_name={self.second_name!r}>" + + +def run_test( + backup: Callable, + file_name_backup: Path | str, +) -> None: + print("[db backup] Print:") + Info.print_from(file_name_backup) + print() + + Info.fill() + + print("[db] After fill:") + Info.print() + + backup(db, file_name_backup) + + +db.connect() +db.create_tables([Info]) + + +if __name__ == "__main__": + Info.fill() + + Info.print() + """ + Info<#1 first_name='first_name_0' second_name='second_name_0'> + Info<#2 first_name='first_name_1' second_name='second_name_1'> + Info<#3 first_name='first_name_2' second_name='second_name_2'> + Info<#4 first_name='first_name_3' second_name='second_name_3'> + Info<#5 first_name='first_name_4' second_name='second_name_4'> + """ + + Info.print_from("db.sqlite") + """ + (1, 'first_name_0', 'second_name_0') + (2, 'first_name_1', 'second_name_1') + (3, 'first_name_2', 'second_name_2') + (4, 'first_name_3', 'second_name_3') + (5, 'first_name_4', 'second_name_4') + """ diff --git a/peewee__examples/custom_field__EnumField/db_enum_field.py b/peewee__examples/custom_field__EnumField/db_enum_field.py new file mode 100644 index 000000000..ca8c83c1e --- /dev/null +++ b/peewee__examples/custom_field__EnumField/db_enum_field.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +from typing import Type, Any + +from peewee import CharField + + +class EnumField(CharField): + """ + This class enable an Enum like field for Peewee + """ + + def __init__(self, choices: Type[enum.Enum], *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.choices: Type[enum.Enum] = choices + self.max_length: int = 255 + + def db_value(self, value: Any) -> Any: + if value is None: + return + + if isinstance(value, enum.Enum): + return value.value + + return value + + def python_value(self, value: Any) -> Any: + if value is None: + return + + type_value_enum = type(list(self.choices)[0].value) + value_enum = type_value_enum(value) + return self.choices(value_enum) diff --git a/peewee__examples/custom_field__EnumField/main.py b/peewee__examples/custom_field__EnumField/main.py new file mode 100644 index 000000000..06fb225f5 --- /dev/null +++ b/peewee__examples/custom_field__EnumField/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum + +# pip install peewee +from peewee import SqliteDatabase, Model + +from db_enum_field import EnumField + + +@enum.unique +class TaskRunStatusEnum(enum.StrEnum): + PENDING = enum.auto() + RUNNING = enum.auto() + FINISHED = enum.auto() + STOPPED = enum.auto() + UNKNOWN = enum.auto() + ERROR = enum.auto() + + +db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1}) + + +class BaseModel(Model): + class Meta: + database = db + + +class TaskRun(BaseModel): + status = EnumField(choices=TaskRunStatusEnum, default=TaskRunStatusEnum.PENDING) + + +db.connect() +db.create_tables([TaskRun]) + + +if __name__ == "__main__": + run1 = TaskRun.create() + run2 = TaskRun.create(status=TaskRunStatusEnum.FINISHED) + + query = TaskRun.select().where(TaskRun.status == TaskRunStatusEnum.FINISHED) + print(query) + print(list(query)) + # SELECT "t1"."id", "t1"."status" FROM "taskrun" AS "t1" WHERE ("t1"."status" = 'finished') + # [<TaskRun: 2>] + + print() + + query = TaskRun.select().where(TaskRun.status.contains("ING")) + print(query) + print(list(query)) + # SELECT "t1"."id", "t1"."status" FROM "taskrun" AS "t1" WHERE ("t1"."status" LIKE '%ING%') + # [<TaskRun: 1>] diff --git a/peewee__examples/custom_field__ListField/main.py b/peewee__examples/custom_field__ListField/main.py index 97044217e..4f430cf86 100644 --- a/peewee__examples/custom_field__ListField/main.py +++ b/peewee__examples/custom_field__ListField/main.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json -from typing import List, Iterable, Optional +from typing import Iterable # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, Field, CharField class ListField(Field): - def python_value(self, value: str) -> List: - return json.loads(value, encoding='utf-8') + def python_value(self, value: str) -> list: + return json.loads(value) - def db_value(self, value: Optional[Iterable]) -> str: + def db_value(self, value: Iterable | None) -> str: if value is not None: if not isinstance(value, list): - raise Exception('Type must be a list') + raise Exception("Type must be a list") return json.dumps(value, ensure_ascii=False) -db = SqliteDatabase('db.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("db.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -40,7 +40,7 @@ class KeyByList(BaseModel): db.create_tables([KeyByList]) -if __name__ == '__main__': +if __name__ == "__main__": if not KeyByList.get_or_none(key="a"): KeyByList.create(key="a", values=[1, 2, 3]) @@ -67,6 +67,6 @@ class KeyByList(BaseModel): # [1, 2, 3] if not KeyByList.get_or_none(key="d"): - KeyByList.create(key="d", values=list(str(i ** 2) for i in range(1, 10))) + KeyByList.create(key="d", values=list(str(i**2) for i in range(1, 10))) print(KeyByList.get(key="d").values) # ['1', '4', '9', '16', '25', '36', '49', '64', '81'] diff --git a/peewee__examples/custom_func__get_http_status.py b/peewee__examples/custom_func__get_http_status.py new file mode 100644 index 000000000..055b09db0 --- /dev/null +++ b/peewee__examples/custom_func__get_http_status.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests +from peewee import SqliteDatabase + + +db = SqliteDatabase(":memory:") + + +@db.func("get_http_status") +def get_http_status(url: str) -> int: + return requests.get(url).status_code + + +db.connect() + +with db.connection() as connect: + print(connect.execute("""SELECT get_http_status("https://ya.ru") """).fetchone()[0]) + # 200 + + print(connect.execute("""SELECT get_http_status("https://ya.ru/404") """).fetchone()[0]) + # 404 diff --git a/peewee__examples/custom_func__redefine_upper.py b/peewee__examples/custom_func__redefine_upper.py new file mode 100644 index 000000000..5fe5d73fd --- /dev/null +++ b/peewee__examples/custom_func__redefine_upper.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from peewee import SqliteDatabase + + +def run_sql(connect) -> None: + print(connect.execute("""SELECT "ы", UPPER("ы") """).fetchone()) + print(connect.execute("""SELECT "s", UPPER("s") """).fetchone()) + print(connect.execute("""SELECT 1 WHERE UPPER("ы") LIKE UPPER("Ы") """).fetchone()) + print(connect.execute("""SELECT 1 WHERE "ы" LIKE "Ы" """).fetchone()) + print(connect.execute("""SELECT 1 WHERE "s" LIKE "S" """).fetchone()) + + +def db_func() -> None: + print("[db_func]") + + db = SqliteDatabase(":memory:") + db.connect() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'ы') + ('s', 'S') + None + None + (1,) + """ + + print() + + @db.func("upper") + def upper(value: str) -> str | None: + if value is None: + return + return value.upper() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'Ы') + ('s', 'S') + (1,) + None + (1,) + """ + + +def db_create_function() -> None: + print("[db_create_function]") + + db = SqliteDatabase(":memory:") + db.connect() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'ы') + ('s', 'S') + None + None + (1,) + """ + + print() + connect.create_function("upper", narg=1, func=str.upper) + + run_sql(connect) + """ + ('ы', 'Ы') + ('s', 'S') + (1,) + None + (1,) + """ + + +db_create_function() + +print() + +db_func() diff --git a/peewee__examples/custom_func__redefine_upper__check_speed.py b/peewee__examples/custom_func__redefine_upper__check_speed.py new file mode 100644 index 000000000..cee35cd06 --- /dev/null +++ b/peewee__examples/custom_func__redefine_upper__check_speed.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# NOTE: Analog https://github.com/gil9red/SimplePyScripts/blob/7b0238202c6e6b5750ff6909d0a78b7892ee1767/sqlite3__examples/custom_func__redefine_upper__check_speed.py + + +import random + +from timeit import default_timer, timeit +from typing import Type, Iterable, Self, Any + +# pip install peewee +from peewee import ( + SqliteDatabase, + Model, + TextField, + DecimalField, + fn +) +from playhouse.shortcuts import model_to_dict + + +db = SqliteDatabase(":memory:") + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type[Self]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + +class Stocks(BaseModel): + date = TextField() + trans = TextField() + symbol = TextField() + qty = DecimalField() + price = DecimalField() + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + +print("Generate items...") +t = default_timer() +purchases = [ + Stocks( + date=random.choice(["2006-03-28", "2006-04-05", "2006-04-06"]), + trans=random.choice(["SELL", "BUY"]), + symbol=random.choice(["IBM", "MSFT"]), + qty=random.choice([500, 1000, 600]), + price=random.choice([53.00, 45.00, 72.00]), + ) + for _ in range(500_000) +] +print(f"Elapsed {default_timer() - t:.3f} secs") +# Elapsed 2.097 secs + +print() + +print("INSERT INTO...") +t = default_timer() + +with db.atomic(): + Stocks.bulk_create(purchases, batch_size=5_000) + +print(f"Elapsed {default_timer() - t:.3f} secs") +# Elapsed 6.842 secs + +print() + + +def run_test() -> None: + elapsed = timeit( + stmt="Stocks.select().where(fn.UPPER(Stocks.trans).ilike(fn.UPPER('%SELL%'))).count()", + globals=dict(Stocks=Stocks, fn=fn), + number=100, + ) + print(f"Elapsed {elapsed:.3f} secs") + + +print("SELECT COUNT DEFAULT UPPER...") +run_test() +# Elapsed 11.765 secs + +print() + + +@db.func("upper") +def upper(value: str) -> str | None: + if value is None: + return + return value.upper() + + +print("SELECT COUNT PYTHON UPPER...") +run_test() +# Elapsed 47.276 secs diff --git a/peewee__examples/db_peewee_meta_model.py b/peewee__examples/db_peewee_meta_model.py new file mode 100644 index 000000000..6564853ef --- /dev/null +++ b/peewee__examples/db_peewee_meta_model.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from enum import Enum +from typing import Type, Any, Iterable + +from peewee import CharField, TextField, ForeignKeyField, Model, Field +from playhouse.shortcuts import model_to_dict + + +def shorten(text: str, length: int = 30, placeholder: str = "...") -> str: + if not text: + return text + + if len(text) > length: + text = text[: length - len(placeholder)] + placeholder + return text + + +class MetaModel(Model): + def get_new(self) -> "MetaModel": + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> "MetaModel": + return cls.select().first() + + @classmethod + def get_last(cls) -> "MetaModel": + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def paginating( + cls, + page: int = 1, + items_per_page: int = 1, + filters: Iterable | None = None, + order_by: Field | None = None, + ) -> list["MetaModel"]: + query = cls.select() + + if filters: + query = query.filter(*filters) + + if order_by: + query = query.order_by(order_by) + + query = query.paginate(page, items_per_page) + return list(query) + + @classmethod + def get_inherited_models(cls) -> list[Type["MetaModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +if __name__ == "__main__": + from peewee import SqliteDatabase + + db = SqliteDatabase(":memory:") + + class BaseModel(MetaModel): + class Meta: + database = db + + class Data(BaseModel): + name = CharField() + value = TextField() + + db.connect() + db.create_tables(BaseModel.get_inherited_models()) + + BaseModel.print_count_of_tables() + # Data: 0 + + data = dict( + a="aAA", + b="Foo", + bar="!!!", + ) + for k, v in data.items(): + Data.create(name=k, value=v) + + BaseModel.print_count_of_tables() + # Data: 3 + + print() + + print(Data.get_first()) + print(Data.get_last()) + # Data(id=1, name='a', value='aAA') + # Data(id=3, name='bar', value='!!!') + + print() + + print(Data.get_last().to_dict()) + # {'id': 3, 'name': 'bar', 'value': '!!!'} + + print() + filters = [ + Data.name.in_(["a", "b"]), + ] + order_by = Data.name.desc() + print(Data.paginating(page=1, filters=filters, order_by=order_by)) + print(Data.paginating(page=2, filters=filters, order_by=order_by)) + print(Data.paginating(page=3, filters=filters, order_by=order_by)) + # [<Data: Data(id=2, name='b', value='Foo')>] + # [<Data: Data(id=1, name='a', value='aAA')>] + # [] diff --git a/peewee__examples/games_and_genres/main.py b/peewee__examples/games_and_genres/main.py index 85e834429..08b171d27 100644 --- a/peewee__examples/games_and_genres/main.py +++ b/peewee__examples/games_and_genres/main.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, TextField, ForeignKeyField -db = SqliteDatabase('games.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("games.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -21,10 +19,10 @@ class Meta: class Game(BaseModel): name = CharField(unique=True) - def get_genres(self) -> List['Genre']: + def get_genres(self) -> list["Genre"]: return [link.genre for link in self.links_to_genres] - def append_genres(self, *genres: List['Genre']): + def append_genres(self, *genres: list["Genre"]) -> None: for genre in genres: GameToGenre.get_or_create(game=self, genre=genre) @@ -33,17 +31,17 @@ class Genre(BaseModel): name = CharField(unique=True) description = TextField(null=True) - def get_games(self) -> List[Game]: + def get_games(self) -> list[Game]: return [link.game for link in self.links_to_games] class GameToGenre(BaseModel): - game = ForeignKeyField(Game, backref='links_to_genres') - genre = ForeignKeyField(Genre, backref='links_to_games') + game = ForeignKeyField(Game, backref="links_to_genres") + genre = ForeignKeyField(Genre, backref="links_to_games") class Meta: indexes = ( - (('game', 'genre'), True), + (("game", "genre"), True), ) @@ -75,30 +73,32 @@ class Meta: game.append_genres(GENRE__ACTION, GENRE__RPG, GENRE__ACTION_ADVENTURE) -if __name__ == '__main__': - print(f'Genre {GENRE__SURVIVAL_HORROR.name!r}:') +if __name__ == "__main__": + print(f"Genre {GENRE__SURVIVAL_HORROR.name!r}:") for game in GENRE__SURVIVAL_HORROR.get_games(): - print(f' {game.name}') + print(f" {game.name}") # Genre 'Survival horror': # Dead Space # Dead Island # Dying Light - print('\n' + '-' * 10 + '\n') + print("\n" + "-" * 10 + "\n") game, _ = Game.get_or_create(name="Dying Light") - print(f'Game {game.name!r}:') + print(f"Game {game.name!r}:") for genre in game.get_genres(): - print(f' {genre.name}: {genre.description!r}') + print(f" {genre.name}: {genre.description!r}") # Game 'Dying Light': # Survival horror: None # RPG: 'Role playing game' # Action: None - print('\n' + '-' * 10 + '\n') + print("\n" + "-" * 10 + "\n") for game in Game.select(): - print(f"{game.name}\n Genres: {', '.join(genre.name for genre in game.get_genres())}") + print( + f"{game.name}\n Genres: {', '.join(genre.name for genre in game.get_genres())}" + ) print() # Dead Space diff --git a/peewee__examples/hello_world/main.py b/peewee__examples/hello_world/main.py index 4636e72ba..c84820266 100644 --- a/peewee__examples/hello_world/main.py +++ b/peewee__examples/hello_world/main.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, IntegerField -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class Info(Model): @@ -19,28 +19,33 @@ class Info(Model): class Meta: database = db - def __str__(self): - return f'Info<#{self.id} first_name={self.first_name} second_name={self.second_name} state={self.state}>' + def __str__(self) -> str: + return f"Info<#{self.id} first_name={self.first_name!r} second_name={self.second_name!r} state={self.state}>" db.connect() db.create_tables([Info]) -if __name__ == '__main__': +if __name__ == "__main__": # Вызываем в первый раз, чтобы заполнить таблицу if not Info.select().count(): for i in range(5): - Info.create(first_name="first_name_" + str(i), second_name="second_name_" + str(i), state=i) + Info.create( + first_name="first_name_" + str(i), + second_name="second_name_" + str(i), + state=i, + ) for info in Info.select(): print(info) - # - # Info<#1 first_name=first_name_0 second_name=second_name_0 state=0> - # Info<#2 first_name=first_name_1 second_name=second_name_1 state=1> - # Info<#3 first_name=first_name_2 second_name=second_name_2 state=2> - # Info<#4 first_name=first_name_3 second_name=second_name_3 state=3> - # Info<#5 first_name=first_name_4 second_name=second_name_4 state=4> + """ + Info<#1 first_name='Четный state!' second_name='second_name_0' state=0> + Info<#2 first_name='first_name_1' second_name='second_name_1' state=1> + Info<#3 first_name='Четный state!' second_name='second_name_2' state=2> + Info<#4 first_name='first_name_3' second_name='second_name_3' state=3> + Info<#5 first_name='Четный state!' second_name='second_name_4' state=4> + """ for info in Info.select(): if info.state % 2 == 0: @@ -51,9 +56,10 @@ def __str__(self): for info in Info.select(): print(info) - # - # Info<#1 first_name=Четный state! second_name=second_name_0 state=0> - # Info<#2 first_name=first_name_1 second_name=second_name_1 state=1> - # Info<#3 first_name=Четный state! second_name=second_name_2 state=2> - # Info<#4 first_name=first_name_3 second_name=second_name_3 state=3> - # Info<#5 first_name=Четный state! second_name=second_name_4 state=4> + """ + Info<#1 first_name='Четный state!' second_name='second_name_0' state=0> + Info<#2 first_name='first_name_1' second_name='second_name_1' state=1> + Info<#3 first_name='Четный state!' second_name='second_name_2' state=2> + Info<#4 first_name='first_name_3' second_name='second_name_3' state=3> + Info<#5 first_name='Четный state!' second_name='second_name_4' state=4> + """ diff --git a/peewee__examples/hello_world__diary/main.py b/peewee__examples/hello_world__diary/main.py index e45f93d04..22baaa69d 100644 --- a/peewee__examples/hello_world__diary/main.py +++ b/peewee__examples/hello_world__diary/main.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -from collections import OrderedDict -import datetime as DT +import datetime as dt + from textwrap import shorten # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, DateTimeField -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -25,14 +25,14 @@ class Meta: class Diary(BaseModel): content = TextField() - created_date = DateTimeField(default=DT.datetime.now) + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def print_table(search_text: str = None): + def print_table(search_text: str = None) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}" print(header_fmt.format(*map(str.upper, Diary._meta.fields))) @@ -41,20 +41,24 @@ def print_table(search_text: str = None): query = query.where(Diary.content.contains(search_text)) for diary in query: - print(row_fmt.format( - id=diary.id, - content=shorten(diary.content, width=50, placeholder='...'), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + content=shorten(diary.content, width=50, placeholder="..."), + created_date=diary.created_date, + ) + ) print() - def __str__(self): - return f'Diary<' \ - f'#{self.id} ' \ - f'content={repr(shorten(self.content, width=50, placeholder="..."))} ' \ - f"created_date='{self.created_date:%d/%m/%Y %H:%M:%S}'" \ - f">" + def __str__(self) -> str: + return ( + f"Diary<" + f"#{self.id} " + f'content={repr(shorten(self.content, width=50, placeholder="..."))} ' + f"created_date='{self.created_date:%d/%m/%Y %H:%M:%S}'" + f">" + ) db.connect() @@ -67,16 +71,16 @@ def __str__(self): Diary.create(content="The quick brown fox jumps over the lazy dog.") -def add_diary(): +def add_diary() -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create(content=data) - print('Saved successfully.') + print("Saved successfully.") -def view_diaries(search_query=None): +def view_diaries(search_query=None) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) @@ -85,42 +89,44 @@ def view_diaries(search_query=None): for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.content) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break -def search_diaries(): +def search_diaries() -> None: """Search diaries""" - view_diaries(input('Search query: ')) + view_diaries(input("Search query: ")) -MENU = OrderedDict([ - ('a', add_diary), - ('v', view_diaries), - ('s', search_diaries), - ('p', Diary.print_table), -]) +MENU = dict( + [ + ("a", add_diary), + ("v", view_diaries), + ("s", search_diaries), + ("p", Diary.print_table), + ] +) -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary/my_database.sqlite b/peewee__examples/hello_world__diary/my_database.sqlite index 73b394087..d6aa5bfd4 100644 Binary files a/peewee__examples/hello_world__diary/my_database.sqlite and b/peewee__examples/hello_world__diary/my_database.sqlite differ diff --git a/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py b/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py index 8687ea1fb..a3cb75b15 100644 --- a/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py +++ b/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py @@ -1,29 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -from collections import OrderedDict -import datetime as DT +import datetime as dt import hashlib +import sys + from getpass import getpass # pip install peewee -from peewee import * - -# For search utils -import sys -sys.path.append('../hello_world__diary__encryption_all_in_one_AES') +from peewee import Model, SqliteDatabase, TextField, DateTimeField +sys.path.append("../hello_world__diary__encryption_all_in_one_AES") from utils.security import CryptoAES, AuthenticationError from utils.utils import shorten -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -32,7 +30,7 @@ class Meta: # Password: 123 -ENCRYPT_MASTER_KEY = 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3' +ENCRYPT_MASTER_KEY = "A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3" # OR: # ENCRYPT_MASTER_KEY = None @@ -43,25 +41,27 @@ class Meta: # Need password password = getpass() if not password: - print('Required password!') - quit() + print("Required password!") + sys.exit() - ENCRYPT_MASTER_KEY = hashlib.sha256(bytes(password, 'utf-8')).hexdigest().upper() + ENCRYPT_MASTER_KEY = hashlib.sha256(bytes(password, "utf-8")).hexdigest().upper() class Diary(BaseModel): encrypted_content = TextField() # Содержимое будет шифровано ключом - encrypted_key = TextField() # Ключ будет шифрован мастер ключом - created_date = DateTimeField(default=DT.datetime.now) + encrypted_key = TextField() # Ключ будет шифрован мастер ключом + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def create_encrypted_content(content: str, master_key: str) -> 'Diary': + def create_encrypted_content(content: str, master_key: str) -> "Diary": key = CryptoAES.get_random_key_hex() encrypted_content = CryptoAES(key).encrypt(content) encrypted_key = CryptoAES(master_key).encrypt(key) - return Diary(encrypted_content=encrypted_content, encrypted_key=encrypted_key).save() + return Diary( + encrypted_content=encrypted_content, encrypted_key=encrypted_key + ).save() def get_content(self, master_key: str) -> str: try: @@ -72,7 +72,7 @@ def get_content(self, master_key: str) -> str: return CryptoAES(key).decrypt(self.encrypted_content) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" def get_key(self, master_key: str) -> str: try: @@ -80,26 +80,37 @@ def get_key(self, master_key: str) -> str: return CryptoAES(master_key).decrypt(self.encrypted_key) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" @staticmethod - def print_table(master_key: str = ENCRYPT_MASTER_KEY): + def print_table(master_key: str = ENCRYPT_MASTER_KEY) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<50} | {:<50} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {encrypted_content:<50} | {content:<50} | {encrypted_key:<50} | {key:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<50} | {:<50} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {encrypted_content:<50} | {content:<50} | {encrypted_key:<50} | {key:<50} | {created_date:%d/%m/%Y %H:%M:%S}" - print(header_fmt.format('ID', 'ENCRYPTED_CONTENT', 'CONTENT', 'ENCRYPTED_KEY', 'KEY', 'CREATED_DATE')) + print( + header_fmt.format( + "ID", + "ENCRYPTED_CONTENT", + "CONTENT", + "ENCRYPTED_KEY", + "KEY", + "CREATED_DATE", + ) + ) for diary in Diary.select(): - print(row_fmt.format( - id=diary.id, - encrypted_content=shorten(diary.encrypted_content), - content=shorten(diary.get_content(master_key)), - encrypted_key=shorten(diary.encrypted_key), - key=shorten(diary.get_key(master_key)), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + encrypted_content=shorten(diary.encrypted_content), + content=shorten(diary.get_content(master_key)), + encrypted_key=shorten(diary.encrypted_key), + key=shorten(diary.get_key(master_key)), + created_date=diary.created_date, + ) + ) print() @@ -110,56 +121,61 @@ def print_table(master_key: str = ENCRYPT_MASTER_KEY): # Вызываем в первый раз, чтобы заполнить таблицу if not Diary.select().count(): - Diary.create_encrypted_content(content="Hello World!", master_key=ENCRYPT_MASTER_KEY) - Diary.create_encrypted_content(content="The quick brown fox jumps over the lazy dog.", master_key=ENCRYPT_MASTER_KEY) + Diary.create_encrypted_content( + content="Hello World!", master_key=ENCRYPT_MASTER_KEY + ) + Diary.create_encrypted_content( + content="The quick brown fox jumps over the lazy dog.", + master_key=ENCRYPT_MASTER_KEY, + ) -def add_diary(master_key: str = ENCRYPT_MASTER_KEY): +def add_diary(master_key: str = ENCRYPT_MASTER_KEY) -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create_encrypted_content(content=data, master_key=master_key) - print('Saved successfully.') + print("Saved successfully.") print() -def view_diaries(master_key: str = ENCRYPT_MASTER_KEY): +def view_diaries(master_key: str = ENCRYPT_MASTER_KEY) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.encrypted_content) print(diary.get_content(master_key)) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break -MENU = OrderedDict([ - ('a', add_diary), - ('v', view_diaries), - ('p', Diary.print_table), -]) +MENU = { + "a": add_diary, + "v": view_diaries, + "p": Diary.print_table, +} -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py index 6cd4c8718..892ae1c94 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py @@ -1,25 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -from collections import OrderedDict -import datetime as DT +import datetime as dt import hashlib +import sys + from getpass import getpass # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, DateTimeField from utils.security import CryptoAES, AuthenticationError from utils.utils import shorten -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -28,7 +29,7 @@ class Meta: # Password: 123 -ENCRYPT_KEY = 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3' +ENCRYPT_KEY = "A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3" # OR: # ENCRYPT_KEY = None @@ -39,18 +40,18 @@ class Meta: # Need password password = getpass() if not password: - print('Required password!') - quit() + print("Required password!") + sys.exit() - ENCRYPT_KEY = hashlib.sha256(bytes(password, 'utf-8')).hexdigest().upper() + ENCRYPT_KEY = hashlib.sha256(bytes(password, "utf-8")).hexdigest().upper() class Diary(BaseModel): encrypted_content = TextField() - created_date = DateTimeField(default=DT.datetime.now) + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def create_encrypted_content(content: str, key: str) -> 'Diary': + def create_encrypted_content(content: str, key: str) -> "Diary": encrypted_content = CryptoAES(key).encrypt(content) return Diary(encrypted_content=encrypted_content).save() @@ -58,24 +59,26 @@ def get_content(self, key: str) -> str: try: return CryptoAES(key).decrypt(self.encrypted_content) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" @staticmethod - def print_table(key: str = ENCRYPT_KEY): + def print_table(key: str = ENCRYPT_KEY) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {encrypted_content:<50} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {encrypted_content:<50} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}" - print(header_fmt.format('ID', 'ENCRYPTED_CONTENT', 'CONTENT', 'CREATED_DATE')) + print(header_fmt.format("ID", "ENCRYPTED_CONTENT", "CONTENT", "CREATED_DATE")) for diary in Diary.select(): - print(row_fmt.format( - id=diary.id, - encrypted_content=shorten(diary.encrypted_content), - content=shorten(diary.get_content(key)), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + encrypted_content=shorten(diary.encrypted_content), + content=shorten(diary.get_content(key)), + created_date=diary.created_date, + ) + ) print() @@ -87,55 +90,57 @@ def print_table(key: str = ENCRYPT_KEY): # Вызываем в первый раз, чтобы заполнить таблицу if not Diary.select().count(): Diary.create_encrypted_content(content="Hello World!", key=ENCRYPT_KEY) - Diary.create_encrypted_content(content="The quick brown fox jumps over the lazy dog.", key=ENCRYPT_KEY) + Diary.create_encrypted_content( + content="The quick brown fox jumps over the lazy dog.", key=ENCRYPT_KEY + ) -def add_diary(key: str = ENCRYPT_KEY): +def add_diary(key: str = ENCRYPT_KEY) -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create_encrypted_content(content=data, key=key) - print('Saved successfully.') + print("Saved successfully.") print() -def view_diaries(key: str = ENCRYPT_KEY): +def view_diaries(key: str = ENCRYPT_KEY) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.encrypted_content) print(diary.get_content(key)) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break -MENU = OrderedDict([ - ('a', add_diary), - ('v', view_diaries), - ('p', Diary.print_table), -]) +MENU = { + "a": add_diary, + "v": view_diaries, + "p": Diary.print_table, +} -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py index 06334aa61..f25732790 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py index 934863819..c91120fb1 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/328be435db9872cfcab35d6df33751006e3e8a64/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py @@ -19,9 +19,9 @@ class AuthenticationError(Exception): class CryptoAES: - def __init__(self, key: (str, bytes)): + def __init__(self, key: (str, bytes)) -> None: if isinstance(key, str): - key = key.encode('utf-8') + key = key.encode("utf-8") self.key = hashlib.sha256(key).digest() @@ -30,18 +30,22 @@ def get_random_key_hex() -> str: return get_random_bytes(32).hex() def encrypt(self, plain_text: str) -> str: - data = plain_text.encode('utf-8') + data = plain_text.encode("utf-8") cipher = AES.new(self.key, AES.MODE_EAX) cipher_text, tag = cipher.encrypt_and_digest(data) encrypted_data = cipher.nonce + tag + cipher_text - return base64.b64encode(encrypted_data).decode('utf-8') + return base64.b64encode(encrypted_data).decode("utf-8") def decrypt(self, encrypted_text: str) -> str: encrypted_data = base64.b64decode(encrypted_text) - nonce, tag, cipher_text = encrypted_data[:16], encrypted_data[16:32], encrypted_data[32:] + nonce, tag, cipher_text = ( + encrypted_data[:16], + encrypted_data[16:32], + encrypted_data[32:], + ) cipher = AES.new(self.key, AES.MODE_EAX, nonce) try: @@ -49,12 +53,12 @@ def decrypt(self, encrypted_text: str) -> str: except ValueError as e: raise AuthenticationError(e) - return data.decode('utf-8') + return data.decode("utf-8") -if __name__ == '__main__': - text = 'Hello World!' - password = '123' +if __name__ == "__main__": + text = "Hello World!" + password = "123" crypto = CryptoAES(password) encrypted_text = crypto.encrypt(text) @@ -65,6 +69,6 @@ def decrypt(self, encrypted_text: str) -> str: # Decrypt with invalid password try: - CryptoAES('abc').decrypt(encrypted_text) + CryptoAES("abc").decrypt(encrypted_text) except AuthenticationError as e: - assert str(e) == 'MAC check failed' + assert str(e) == "MAC check failed" diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py index 70d753506..6aca51bbd 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def shorten(text: str, width: int = 50) -> str: if len(text) <= width: return text - placeholder = '...' - return text[:width - len(placeholder)] + placeholder + placeholder = "..." + return text[: width - len(placeholder)] + placeholder diff --git a/peewee__examples/persons/main.py b/peewee__examples/persons/main.py index ec993cf75..f949fcab0 100644 --- a/peewee__examples/persons/main.py +++ b/peewee__examples/persons/main.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import json # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, DateField, ForeignKeyField -db = SqliteDatabase('persons.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("persons.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -25,7 +25,7 @@ class Person(BaseModel): class Pet(BaseModel): - owner = ForeignKeyField(Person, backref='pets') + owner = ForeignKeyField(Person, backref="pets") name = CharField() animal_type = CharField() @@ -33,19 +33,21 @@ class Pet(BaseModel): db.connect() db.create_tables([Person, Pet]) -for person_data in json.load(open('persons.json', encoding='utf-8')): - birthday = DT.datetime.strptime(person_data['birthday'], '%Y-%M-%d') - person, _ = Person.get_or_create(name=person_data['name'], birthday=birthday) +for person_data in json.load(open("persons.json", encoding="utf-8")): + birthday = dt.datetime.strptime(person_data["birthday"], "%Y-%M-%d") + person, _ = Person.get_or_create(name=person_data["name"], birthday=birthday) - for pet in person_data['pets']: - Pet.get_or_create(owner=person, name=pet['name'], animal_type=pet['animal_type']) + for pet in person_data["pets"]: + Pet.get_or_create( + owner=person, name=pet["name"], animal_type=pet["animal_type"] + ) for person in Person.select(): - print(f'{person.name} ({person.birthday}). Pets: {person.pets.count()}') + print(f"{person.name} ({person.birthday}). Pets: {person.pets.count()}") for pet in person.pets: - print(f' {pet.name} ({pet.animal_type}). Owner: {pet.owner.name}') + print(f" {pet.name} ({pet.animal_type}). Owner: {pet.owner.name}") print() diff --git a/peewee__examples/serialization__model_to_dict__dict_to_model/main.py b/peewee__examples/serialization__model_to_dict__dict_to_model/main.py index 7cd6864da..85a10a0e5 100644 --- a/peewee__examples/serialization__model_to_dict__dict_to_model/main.py +++ b/peewee__examples/serialization__model_to_dict__dict_to_model/main.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, DateField, ForeignKeyField from playhouse.shortcuts import model_to_dict, dict_to_model -db = SqliteDatabase(':memory:', pragmas={'foreign_keys': 1}) +db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -24,27 +24,29 @@ class Person(BaseModel): name = CharField() birthday = DateField() - def __str__(self): - return f'Person(id={self.id} name={self.name!r} birthday={self.birthday} ' \ - f'pets={", ".join(p.name for p in self.pets)!r})' + def __str__(self) -> str: + return ( + f"Person(id={self.id} name={self.name!r} birthday={self.birthday} " + f'pets={", ".join(p.name for p in self.pets)!r})' + ) class Pet(BaseModel): - owner = ForeignKeyField(Person, backref='pets') + owner = ForeignKeyField(Person, backref="pets") name = CharField() animal_type = CharField() - def __str__(self): - return f'Pet(id={self.id} name={self.name!r} owner={self.owner.name!r} self.animal_type={self.animal_type!r})' + def __str__(self) -> str: + return f"Pet(id={self.id} name={self.name!r} owner={self.owner.name!r} self.animal_type={self.animal_type!r})" db.connect() db.create_tables([Person, Pet]) -person = Person.create(name='Ivan', birthday=DT.date.today()) +person = Person.create(name="Ivan", birthday=dt.date.today()) -Pet.create(owner=person, name='Oval', animal_type='Dog') -Pet.create(owner=person, name='Bortik', animal_type='Cat') +Pet.create(owner=person, name="Oval", animal_type="Dog") +Pet.create(owner=person, name="Bortik", animal_type="Cat") print(person) # Person(id=1 name='Ivan' birthday=2020-01-09 pets='Oval, Bortik') @@ -63,7 +65,7 @@ def __str__(self): print() # Create another database and import this -db = SqliteDatabase('persons.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("persons.sqlite", pragmas={"foreign_keys": 1}) Person._meta.database = db Pet._meta.database = db db.connect() diff --git a/pens_calcs/common.py b/pens_calcs/common.py index c1c00972b..ea8e09133 100644 --- a/pens_calcs/common.py +++ b/pens_calcs/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://www.pfrf.ru/thm/common/mod/pensCalc/js/params.js @@ -45,7 +45,7 @@ 7: 1.74, 8: 1.9, 9: 2.09, - 10: 2.32 + 10: 2.32, } VSkoef = 1.8 # Коэффициент за 1 год военной службы diff --git a/pens_calcs/number_of_pension_points_for_year.py b/pens_calcs/number_of_pension_points_for_year.py index 9c12bd721..42d0bc365 100644 --- a/pens_calcs/number_of_pension_points_for_year.py +++ b/pens_calcs/number_of_pension_points_for_year.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import common @@ -21,8 +21,10 @@ def number_of_pension_points_for_year(zp: float) -> float: # Зарплата меньше мрот if zp < common.MROT: - raise Exception('Ошибка! Введите зарплату выше, чем минимальный размер оплаты труда в ' - 'Российской Федерации в 2017 году - 7 500 рублей') + raise Exception( + "Ошибка! Введите зарплату выше, чем минимальный размер оплаты труда в " + "Российской Федерации в 2017 году - 7 500 рублей" + ) kpk_trud = zp / common.ZPM * 10 if kpk_trud > 8.26: @@ -31,7 +33,7 @@ def number_of_pension_points_for_year(zp: float) -> float: return round(kpk_trud * 100) / 100 -if __name__ == '__main__': +if __name__ == "__main__": # Сколько пенсионных баллов может быть начислено Вам за 2017 год? # # Введите размер Вашей ежемесячной заработной платы до вычета НДФЛ: diff --git a/pens_calcs/pens_calc.py b/pens_calcs/pens_calc.py deleted file mode 100644 index 6d51873a1..000000000 --- a/pens_calcs/pens_calc.py +++ /dev/null @@ -1,555 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import common - - -# TODO: сделать -# http://www.pfrf.ru/thm/common/mod/pensCalc/js/public.js -# TODO: добавить аннотации параметрам и возвращаемому значению - -# # Служба в армии -# yearsInArmy, monthInArmy, daysInArmy - -# # Уход за нетрудоспособными -# careYears, careMonth, careDays - -# # Дети, не более 4х -# TODO: посмотреть к каким полям относится и переименвать соответственно и в формулах их использования -# childrenCount1, childrenVac1 - - -# def pens_calc(gender, birthDate, pensionTarif, -# yearsInArmy, monthInArmy, daysInArmy, -# careYears, careMonth, careDays, -# childrenCount1, childrenVac1, -# retireWorkWithoutPension, -# careerLength, -# revenue, -# SZPeriod -# ): - - - -# var pensionForm = $(".calc-form") -# var persionFormInputs = pensionForm.find("input,select") -# var calcResult = $(".pensionCalcResult") -# var calcResultToday = $(".pensionCalcResultToday") -# var socialPensionWarning = $("#socialPensionWarning") -# var socialPensionWarning2 = $("#socialPensionWarning2") -# var combinationWarning = $("#combinationWarning") -# var enterParamsWarning = $("#enterParamsWarning") -# var enterGenderWarning = $("#enterGenderWarning") -# var enterBYWarning = $("#enterBYWarning") -# var enterBYWarning2 = $("#enterBYWarning2") -# var enterBYWarning3 = $("#enterBYWarning3") -# var alreadyPensioneer = $("#alreadyPensioneer") -# var wrongFee = $("#wrongFee") -# var noTarif = $("#noTarif") -# var ending = $(".ending") -# var newCoefSummSmall = $(".newCoefSummSmall") -# var personOSsmall = $(".personOSsmall") -# -# var armySection = pensionForm.find('.army-section') - -# TODO: возможно будет полезно узнать что скрыто в той секции -# armySection.hide() -# persionFormInputs.filter('[type="radio"][name="noArmy"]').click(function () { -# if($(this).val() == 2) { -# armySection.slideDown(150) -# } -# else { -# armySection.slideUp(150) -# } -# }) - -# var textInputs = persionFormInputs.filter("input[type='text']") -# var prevValues = [] - - -# var careerPlanQuestions = $("div.careerPlanQuestions") -# var careerPlanSwitch = $("input[name='careerPlan']") -# -# function revealCPQuestionsBlocks(switchValue) { -# careerPlanQuestions.hide() -# switch(switchValue) { -# case '1': -# //наемный работник -# careerPlanQuestions.filter(".careerPlan1").show() -# break -# case '2': -# //самозанятый -# careerPlanQuestions.filter(".careerPlan2").show() -# break -# case '3': -# //совмещающий -# careerPlanQuestions.show() -# break -# } -# } -# -# revealCPQuestionsBlocks(careerPlanSwitch.filter(':checked').val()) -# careerPlanSwitch.change(function () { -# revealCPQuestionsBlocks($(this).val()) -# }) - - -# TODO: поле в скрипте не имеет значения, вообще -gender = "1" -# if(gender.length < 1) { -# enterGenderWarning.show() -# $(output_area).slideDown() -# return -# } - -# TODO: поле в скрипте не имеет значения, вообще -birthDate = "1992" -# if(!birthDate.match(/\d{4}/)) { -# enterBYWarning.show() -# $(output_area).slideDown() -# return -# } - -# TODO: узнать что скрывается за полем -# TODO: пусть будет пока 0 -pensionTarif = 0 -# var pensionTarif = persionFormInputs.filter('[name="pensionTarif"]:checked') -# if(pensionTarif.length < 1) { -# noTarif.show() -# $(output_area).slideDown() -# return -# } -# pensionTarif = parseInt(pensionTarif.val()) - -# Общий расчет - -# Служба в армии -yearsInArmy = 1 -monthInArmy = 0 -daysInArmy = 0 - -# Стаж в армии -VS = ((((yearsInArmy * 12) + monthInArmy) / 12 * 365) + daysInArmy) / 365 - -# Коэффициент стажа -VSK = VS * common.VSkoef - -# Уход за нетрудоспособными -careYears = 1 -careMonth = 0 -careDays = 0 - -# Стаж ухода за нетрудоспособными -CR = ((((careYears * 12) + careMonth) / 12 * 365) + careDays) / 365 - -# Коэффициент ухода -CRK = CR * common.CRkoef - -# Дети, не более 4х -childrenCount1 = 1 -if childrenCount1 < 0: - childrenCount1 = 0 - -elif childrenCount1 > 4: - childrenCount1 = 4 - -# TODO: узнать что за childrenVac1 -# TODO: и что за стаж -# Стаж -childrenVac1 = 0 -if childrenVac1 < 0: - childrenVac1 = 0 - -elif childrenVac1 > 1.5: - childrenVac1 = 1.5 - - -# NOTE: только внутри кода объявляется -# Коэффициент -KD = 0.0 - -if childrenCount1 > 0: - if childrenCount1 > 0: - KD += 1.8 - - if childrenCount1 > 1: - KD += 3.6 - - if childrenCount1 > 2: - KD += 5.4 - - if childrenCount1 > 3: - KD += 5.4 - - KD *= childrenVac1 - childrenVac1 *= childrenCount1 - -# Коэффициент за нетрудовые периоды -NK = KD + CRK + VSK - -# Стаж за нетрудовые периоды -NS = childrenVac1 + VS + CR - -# NOTE: сколько после пенсионного возраста собираешься работать неполучая пенсию -retireWorkWithoutPension = 5 -if retireWorkWithoutPension > 10: - retireWorkWithoutPension = 10 - -# Расчеты в зависимости от типа занятости -# TODO: узнать возможные варианты значения -careerPlan = '1' - -# Наемный работник -def calcEmpl(fee: int, careerLength: int, pensionTarif: int): - """ - - :param fee: Зарплата - :param careerLength: Стаж - - # TODO: узнать что за тариф такой - :param pensionTarif: - :return: - """ - - if fee < 0: - fee = 0 - - if fee > common.ZPM: - fee = common.ZPM - - if careerLength < 0: - careerLength = 0 - - # TODO: бросать исключение, узнать какой текст тут скрыт - # if careerLength > 60) { - # enterBYWarning3.show() - # $(output_area).slideDown() - # return false - - # TODO: бросать исключение, узнать какой текст тут скрыт - # # Зарплата меньше мрот - # if careerLength > 0 and fee < common.MROT: - # wrongFee.show() - # $(output_area).slideDown() - # return false - # } - - # Пенсионные коэффициенты за трудовой период - IPKtrud = (fee / common.ZPM) * common.KNPG[pensionTarif] * (careerLength * 10) - - # TODO: Проследить за S и заменить - return { - 'S': careerLength, - 'IPKtrud': IPKtrud, - } - -def calcSZ(SZPeriod: int, revenue: int, pensionTarif: int): - """ - - :param revenue: Годовой доход - :param SZPeriod: Стаж - - # TODO: узнать что за тариф такой - :param pensionTarif: - :return: - """ - - if SZPeriod < 0: - SZPeriod = 0 - - if revenue < 0: - revenue = 0 - - # TODO: разобраться какие значения может иметь pensionTarif - SVGDkoeff = 16 if pensionTarif == 0 else 10 - - # Сумма страховых взносов на страховую пенсию, начисленных исходя из размера годового дохода - if revenue < common.GDmax: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12)) / 26 - else: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12) + ((revenue - common.GDmax) * 0.01)) / 26 - - # Пенсионные коэффициенты за трудовой период - IPKtrud = (SVGD / common.MSSV) * SZPeriod * 10 - - # TODO: отследить и заменить S - return { - 'S': SZPeriod, - 'IPKtrud': IPKtrud - } - -calcPart = None -calcPartEmpl = None -calcPartSZ = None -IPKtrud = None -S = None - -if careerPlan == '1': - # Наемный работник - calcPart = calcEmpl() - # TODO: добавить проверку, проверить какое значение может возвращаться из функции и как влияет - # возврат значения тут - # - # if(calcPart === false) return false - - # Стаж - S = calcPart.S - - # Пенсионные коэффициенты - IPKtrud = calcPart.IPKtrud - -elif careerPlan == '2': - # Самозанятый - calcPart = calcSZ() - - # Стаж - S = calcPart.S - - # Пенсионные коэффициенты - IPKtrud = calcPart.IPKtrud - -elif careerPlan == '3': - # Совмещающий - calcPartEmpl = calcEmpl() - # TODO: добавить проверку, проверить какое значение может возвращаться из функции и как влияет - # возврат значения тут - # - # if(calcPartEmpl === false) return false - - calcPartSZ = calcSZ() - - # Количество пенсионных коэффициентов свыше максимально установленного значения в год, полученных при совмещённой деятельности - combinePeriod = 0 - # TODO: проверить - # if(combinePeriod.length < 1) { - # combinePeriod = 0 - # persionFormInputs.filter('#combinePeriod').val(combinePeriod) - # } - # TODO: проверить - # if combinePeriod > Math.min(calcPartEmpl.S, calcPartSZ.S)) { - # combinationWarning.show() - # $(output_area).slideDown() - # return false - # } - - # Годовой пенсионный коэффициент, получаемый гражданином в года совмещения деятельности - IPKemp = calcPartEmpl.IPKtrud * combinePeriod / calcPartEmpl.S - IPKsz = calcPartSZ.IPKtrud * combinePeriod / calcPartSZ.S - IPKo = (IPKsz + IPKemp) / combinePeriod - if IPKo > 10: - IPKo = 10 - - IPKis = IPKo * combinePeriod - - # Стаж - S = calcPartEmpl.S + calcPartSZ.S - combinePeriod - - # Пенсионные коэффициенты - IPKtrud = (calcPartSZ.IPKtrud - IPKsz) + (calcPartEmpl.IPKtrud - IPKemp) + IPKis - if combinePeriod == 0: - IPKtrud = calcPartSZ.IPKtrud + calcPartEmpl.IPKtrud - - -# Переходный период для наемных и самозанятых -if careerPlan == '1' or careerPlan == '2': - IPKtrud2015 = 0 - IPKtrud2021 = 0 - - fee = 10000 - revenue = 1 - if revenue < 0: - revenue = 0 - - # Сумма страховых взносов на страховую пенсию, начисленных исходя из размера годового дохода - SVGD = 0 - SVGDkoeff = 16 if pensionTarif == 0 else 10 - - if revenue < common.GDmax: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12)) / 26 - else: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12) + ((revenue - common.GDmax) * 0.01)) / 26 - - if S > 5: - IPKtrud2021 = IPKtrud * ((S - 5) / S) - - if careerPlan == '1': - IPKtrud2015 = fee / common.ZPM * 10 - - elif careerPlan == '2': - IPKtrud2015 = (SVGD / common.MSSV) * 10 - - elif S < 6: - if careerPlan == '1': - IPKtrud2015 = fee / common.ZPM * 10 - - elif careerPlan == '2': - IPKtrud2015 = (SVGD / common.MSSV) * 10 - - # KNPG = 1 - if pensionTarif == '0': - IPKtrud2015 = (S > 0 ? Math.min(8.26, IPKtrud2015) : 0) + (S > 1 ? Math.min(8.70, IPKtrud2015) : 0) + - (S > 2 ? Math.min(9.13, IPKtrud2015) : 0) + (S > 3 ? Math.min(9.57, IPKtrud2015) : 0) - - # KNPG = 0.625 - else: - IPKtrud2015 = (S > 0 ? Math.min(8.26, IPKtrud2015) : 0) + (S > 1 ? Math.min(5.43, IPKtrud2015) : 0) + - (S > 2 ? Math.min(5.71, IPKtrud2015) : 0) + (S > 3 ? Math.min(5.98, IPKtrud2015) : 0) - - IPKtrud = IPKtrud2015 + IPKtrud2021 - - -# TODO: какое-то обнуление значений в редакторе полей -# if(careerPlan == '1') { -# persionFormInputs.filter('#SZPeriod').val(0) -# persionFormInputs.filter('#revenue').val(0) -# } -# else { -# if(careerPlan == '2') { -# persionFormInputs.filter('#careerLength').val(0) -# persionFormInputs.filter('#fee').val(0) -# } -# } - -# Пенсионные коэффициенты -IPK = (IPKtrud + NK) * common.SPKop[retireWorkWithoutPension] - -# Общий стаж -OS = S + NS - -# # NOTE: Установка значений -# newCoefSummSmall.html((Math.round(IPK * 100) / 100).toString()) -# personOSsmall.html((Math.round(OS * 100) / 100).toString()) - - -var WR = OS.toString().substr(-1) - -ending.html('лет') - -if(WR == 1) { - ending.html('год') -} -else { - if(WR >= 2 && WR <= 4) { - ending.html('года') - } -} - -# пересчёт права выхода на пенсию по стажу (каждому году свой минимальный стаж) -$(output_area).hide() -if(S == 0 && OS < 8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 1 && OS < 9) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 2 && OS < 10) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 3 && OS < 11) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 4 && OS < 12) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 5 && OS < 13) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 6 && OS < 14) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 7 && OS < 15) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -//пересчёт права выхода на пенсию по ИПК (каждому году свой минимальный ИПК) -if(S == 0 && IPK < 11.4) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 1 && IPK < 13.8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 2 && IPK < 16.2) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 3 && IPK < 18.6) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 4 && IPK < 21) { - socialPensionWarning2.show() - $(output_area).slideDown() - return false -} - -if(S == 5 && IPK < 23.4) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 6 && IPK < 25.8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 7 && IPK < 28.2) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 8 && IPK < 30) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -# Страховая пенсия -SP = (common.FIKS * common.BPKop[retireWorkWithoutPension]) + (IPK * common.CPK) - -var newCoefSummCont = $("#newCoefSumm") -newCoefSummCont.html((Math.round(IPK * 100) / 100).toString()) - -var pensionIPartCont = $("#pensionIPart") -pensionIPartCont.html((Math.round(SP * 100) / 100).toString()) - -var personOSCont = $("#personOS") -personOSCont.html((Math.round(OS * 100) / 100).toString()) diff --git a/percentage_values_in_list.py b/percentage_values_in_list.py index 6d9aba2c6..b485ac97d 100644 --- a/percentage_values_in_list.py +++ b/percentage_values_in_list.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -26,10 +26,10 @@ def get_percentage_values(items): """Возвращает список с процентным значением элемента списка.""" sum_items = sum(items) - return ['{:.1f}%'.format(i / sum_items * 100) for i in items] + return [f"{i / sum_items * 100:.1f}%" for i in items] -if __name__ == '__main__': +if __name__ == "__main__": print(get_percentage_values([100, 100])) print(get_percentage_values([100, 100, 200])) print(get_percentage_values([100, 100, 200, 400])) diff --git a/permutations.py b/permutations.py index 326c93870..83c5c183d 100644 --- a/permutations.py +++ b/permutations.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from itertools import permutations -items = list(permutations('abc', r=3)) -print(items) # [('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')] +items = list(permutations("abc", r=3)) +print(items) +# [('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')] -items = list(permutations('abc', r=2)) +items = list(permutations("abc", r=2)) print(items) # [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')] diff --git a/photon__crawler_spider__examples/main.py b/photon__crawler_spider__examples/main.py deleted file mode 100644 index 4fa62e3ec..000000000 --- a/photon__crawler_spider__examples/main.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://github.com/s0md3v/Photon/wiki/Photon-Library -# pip install photon -import photon -photon.crawl('https://github.com/s0md3v/Photon/wiki/Photon-Library') -result = photon.result() -print(result) - -with open('result.json', 'w', encoding='utf-8') as f: - import json - json.dump(result, f, indent=4, ensure_ascii=False) diff --git a/pikabu_ru/download_svg_emotions/main.py b/pikabu_ru/download_svg_emotions/main.py new file mode 100644 index 000000000..acd5da042 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/main.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from textwrap import dedent + +# pip install selenium +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.options import Options + + +DIR = Path(__file__).resolve().parent + +DIR_OUT = DIR / "out" +DIR_OUT.mkdir(parents=True, exist_ok=True) + + +options = Options() +options.add_argument("--headless") + +driver = webdriver.Firefox(options=options) +driver.implicitly_wait(10) + +try: + driver.get("https://pikabu.ru/") + print(f'Title: "{driver.title}"') + + # Содержит иконки в тегах symbol + svg_app_el = driver.find_element(By.CSS_SELECTOR, "svg.app-svg") + + # Содержит предустановленные фигуры. Без него некоторые из иконок-эмоций отображались частично + svg_defs_el = svg_app_el.find_element(By.CSS_SELECTOR, "defs") + defs = svg_defs_el.get_attribute("outerHTML") + + # Перебор и сохранение svg с эмоциями + for svg_symbol_el in svg_app_el.find_elements(By.CSS_SELECTOR, "symbol"): + el_id = svg_symbol_el.get_attribute("id") + if "icon--emotions__" in el_id: + svg_symbol = svg_symbol_el.get_attribute("outerHTML") + svg = dedent( + f"""\ + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + {defs} + {svg_symbol} + <use xlink:href="#{el_id}"/> + </svg> + """ + ) + file_name = DIR_OUT / f"{el_id}.svg" + print(f"Saving {file_name} ...") + + file_name.write_text(svg, encoding="utf-8") + +finally: + driver.quit() diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg new file mode 100644 index 000000000..f1cc3ee26 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs><linearGradient id="B" x1="10" y1="1.043" x2="10" y2="22.435" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="A" x1="18" y1="2" x2="18" y2="48.452" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><clipPath id="eye-crossed-a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><linearGradient id="paint_angry_linear" x1="16" y1="4.666" x2="16" y2="24.261" gradientUnits="userSpaceOnUse"><stop stop-color="#F7B259"></stop><stop offset="1" stop-color="#EE3323"></stop></linearGradient><linearGradient id="paint_sad_linear" x1="16" y1="3.937" x2="16" y2="25.501" gradientUnits="userSpaceOnUse"><stop stop-color="#5EAEE5"></stop><stop offset=".455" stop-color="#AAB09F"></stop><stop offset=".823" stop-color="#E2B16D"></stop><stop offset="1" stop-color="#F7B259"></stop></linearGradient><rect id="a" width="36" height="20" rx="4"></rect><mask id="b" x="0" y="0" width="36" height="20" fill="#fff"><use xlink:href="#a"></use></mask><clipPath id="clip0comm"><path fill="#fff" transform="translate(.5)" d="M0 0h16v16H0z"></path></clipPath><mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="43" height="42"><path d="M24.094 45c11.598 0 21-9.402 21-21s-9.402-21-21-21-21 9.402-21 21 9.402 21 21 21Z" fill="#8AC858"></path></mask><radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 5.57802 -6.33866 0 10.278 10)"><stop stop-color="#3E671B"></stop><stop offset="1" stop-color="#77AB4F"></stop></radialGradient><linearGradient id="paint0_linear_2" x1="23.5" y1="-4.5" x2="10" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".505" stop-color="#FFC000" stop-opacity=".495"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="paint10_linear" x1="2.83" y1="19.846" x2="5.026" y2="26.633" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear_2" x1="9.659" y1="15.773" x2="18.46" y2="20.493" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear_2" x1="9.551" y1="16.552" x2="18.583" y2="20.803" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear_2" x1="7.139" y1="18.488" x2="14.598" y2="24.775" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset="1" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint5_linear" x1="41.782" y1="18.301" x2="9.337" y2="-7.723" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".222" stop-color="#FFCB7D"></stop><stop offset=".577" stop-color="#FF9333"></stop><stop offset="1" stop-color="#FFD99C"></stop></linearGradient><radialGradient id="paint3_radial_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.15404 .55622 -.33024 4.84121 14.294 14.948)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint66_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.84356 -2.56095 8.4484 6.0818 18.636 4.431)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.23117 3.3889 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.3077 4.51068 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.76699 0 0 5.75381 1.884 8.65)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.43105 -.26644 .38005 4.89394 4.493 9.557)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.9258 .69277 -.81353 2.26147 9.542 12.416)"><stop stop-color="#C13B12"></stop><stop offset=".723" stop-color="#DD5D28" stop-opacity=".414"></stop><stop offset=".994" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint9_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-155.017 6.478 1.388) scale(14.9871 12.8928)"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".299" stop-color="#F9C16E"></stop><stop offset=".528" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-54.251 6.526 -7.925) scale(2.20888 7.28696)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="paint0_linear" x1="-1.92" y1="12.792" x2="-.382" y2="17.543" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear" x1="2.861" y1="9.941" x2="9.022" y2="13.245" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear" x1="2.785" y1="10.486" x2="9.108" y2="13.462" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear" x1="1.097" y1="11.842" x2="6.318" y2="16.243" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><radialGradient id="pkb1" cx="0" cy="0" r="1" gradientTransform="matrix(11.6095 .75457 -.56841 8.74533 13.24 18.89)" gradientUnits="userSpaceOnUse"><stop stop-color="#BD350E"></stop><stop offset=".44" stop-color="#BF3710" stop-opacity=".51"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".3"></stop><stop offset=".76" stop-color="#CE4B20" stop-opacity=".15"></stop><stop offset=".87" stop-color="#DB5C2E" stop-opacity=".03"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb2" cx="0" cy="0" r="1" gradientTransform="matrix(7.21222 0 0 10.9971 4.56 17.34)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb3" cx="0" cy="0" r="1" gradientTransform="matrix(6.56903 -.50925 .72514 9.35388 9.55 19.07)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb4" cx="0" cy="0" r="1" gradientTransform="matrix(3.6871 1.32408 -1.5528 4.32403 19.22 24.54)" gradientUnits="userSpaceOnUse"><stop stop-color="#C13B12"></stop><stop offset=".72" stop-color="#DD5D28" stop-opacity=".41"></stop><stop offset=".99" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb5" cx="0" cy="0" r="1" gradientTransform="rotate(-155.06 12.96 2.95) scale(28.6853 24.6493)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".13" stop-color="#FFE491"></stop><stop offset=".3" stop-color="#F9C16E"></stop><stop offset=".53" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="pkb6" cx="0" cy="0" r="1" gradientTransform="matrix(2.47103 -3.42617 11.3089 8.15624 18.457 4.628)" gradientUnits="userSpaceOnUse"><stop stop-color="#E74C29"></stop><stop offset=".26" stop-color="#DE4926"></stop><stop offset=".67" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="pkb0" x1="3.05" x2="13.03" y1="23.44" y2="31.87" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".65" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint-preloader_linear" x1="12.083" y1="2.75" x2="14.417" y2="8" gradientUnits="userSpaceOnUse"><stop stop-color="#BDBDBF"></stop><stop offset=".526" stop-color="#BDBDBF" stop-opacity=".6"></stop><stop offset="1" stop-color="#BDBDBF" stop-opacity="0"></stop></linearGradient><circle id="loadingCircle" cx="50" cy="50" r="50"></circle><mask id="loadingCircleMask" x="0" y="0" width="100" height="100" fill="#fff"><use xlink:href="#loadingCircle"></use></mask><linearGradient id="telegram-logo-16-a" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram-logo-16-b" x1="9.003" y1="8.001" x2="10.251" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><linearGradient id="telegram_paint11_linear" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram_paint1_linear" x1="9.003" y1="8.001" x2="10.252" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><clipPath id="clip1"><path fill="#fff" transform="translate(6 6)" d="M0 0h12v12H0z"></path></clipPath></defs> + <symbol id="icon--emotions__angry" viewBox="0 0 32 32"><path d="M3.465 10.652C3.919 7.779 5.074 2.156 16 2.156c11.388 0 12.147 5.688 12.649 8.496-4.522 4.38-25.184 0-25.184 0Z" fill="#FCD996"></path><path d="M29.206 16.568c-.133 8.36-5.773 12.729-13.164 12.729s-13.25-5.22-13.254-12.503C2.784 4.668 8.606 2.924 15.997 2.924c7.39 0 13.4 1.569 13.209 13.644Z" fill="url(#paint_angry_linear)"></path><path d="M3.266 20.274S7.112 26.75 16 26.75s12.734-6.476 12.734-6.476-1.38 9.57-12.691 9.57c-11.312 0-12.777-9.57-12.777-9.57Z" fill="#B52121"></path><path d="M6.31 14.34s.432 2.475 2.514 2.977c2.325.56 3.503-1.606 3.503-1.606S5.42 8.3 6.31 14.34Z" fill="#590402"></path><path d="M6.378 13.077s-.742.08-.55 1.485c.136.983.682 2.49 2.265 2.86 2.286.535 3.539-.338 4.006-1.443 0 0-.788 1.578-3.348 1.092-1.924-.366-2.373-3.994-2.373-3.994Z" fill="#460301"></path><path d="M11.274 15.698s-.548.935-1.618.688c-1.07-.247-1.061-1.63-1.061-1.63s.649.066 1.472.204a1.858 1.858 0 0 1 1.207.738Z" fill="#A32B2B"></path><path d="M10.423 15.917c.504.153.404.64 0 .47-.404-.168-.774-.706 0-.47ZM8.707 15.116c.272.058.078.362-.13.284-.206-.077-.232-.361.13-.284Z" fill="#fff"></path><path d="M14.438 13.514c.552.649-.138 2.419-1.375 2.61-1.237.193-4.807-1.413-6.362-2.612C5.145 12.314 4.387 11.8 4.81 10.95c.423-.851.957-.742 2.065-.195 1.108.547 3.19 1.503 4.75 1.923 1.03.276 2.406.358 2.813.836Z" fill="#590402"></path><path d="M14.609 14.387s-.327 1.72-1.727 1.848c-1.4.127-4.516-1.492-6.045-2.525-1.53-1.033-2.73-1.73-1.924-2.955 0 0 .369 1.162 2.558 2.478 2.189 1.316 4.548 2.182 5.512 2.098.965-.084 1.626-.944 1.626-.944Z" fill="#460301"></path><path d="M25.983 14.698s-.432 2.475-2.514 2.977c-2.325.56-3.503-1.606-3.503-1.606s6.906-7.411 6.017-1.371Z" fill="#590402"></path><path d="M25.915 13.435s.742.08.549 1.485c-.135.983-.705 2.652-2.287 3.022-2.286.534-3.515-.5-3.982-1.605 0 0 .788 1.578 3.348 1.092 1.923-.366 2.372-3.994 2.372-3.994Z" fill="#460301"></path><path d="M21.019 16.056s.548.935 1.618.688c1.07-.247 1.06-1.63 1.06-1.63s-.648.066-1.471.204c-.823.137-1.207.738-1.207.738Z" fill="#A32B2B"></path><path d="M21.803 16.33c-.548.168-.44.696 0 .513.439-.183.84-.768 0-.512ZM23.46 15.619c-.271.058-.077.362.13.284.207-.077.233-.362-.13-.284Z" fill="#fff"></path><path d="M18.109 14.096c-.552.649-.116 2.195 1.12 2.387 1.238.192 4.808-1.414 6.363-2.613 1.556-1.2 2.313-1.711 1.89-2.563-.421-.852-1.093-.61-2.201-.063-1.11.546-3.143 1.425-4.703 1.845-1.03.278-2.062.529-2.47 1.007Z" fill="#590402"></path><path d="M17.86 14.769s.15 1.697 1.55 1.824c1.4.127 4.517-1.492 6.046-2.525s2.73-1.73 1.924-2.955c0 0-.37 1.162-2.558 2.478-2.19 1.316-4.548 2.182-5.512 2.098-.965-.084-1.45-.92-1.45-.92Z" fill="#460301"></path><path d="M17.503 19.313c-1.132-.047-3.588.316-3.871.646l-1.98 2.307-1.037.519s0 .707.896.849c.896.14 1.462-.236 1.698-.66.236-.424.347-1.242 1.478-1.902 1.132-.66 2.688-.475 3.254.044.566.519.874 1.262 1.027 1.725.333 1.01 1.036 1.09 1.696.902.66-.19.787-.82.787-.82s-2.816-3.563-3.948-3.61Z" fill="#FF6C6C"></path><path d="M16.323 18.502c-2.612.01-3.627.933-4.357 1.746-1.204 1.34-1.836 2.647-1.006 2.819.978.203 1.572.22 1.792-.283.22-.503.439-2.306 3.038-2.53 1.808-.154 2.703.408 3.135 1.407.445 1.03.575 1.734 1.172 1.734s1.383-.126 1.352-.534c-.032-.408-1.273-4.374-5.126-4.36Z" fill="#510402"></path></symbol> + <use xlink:href="#icon--emotions__angry"/> +</svg> diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg new file mode 100644 index 000000000..25adc02e9 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs><linearGradient id="B" x1="10" y1="1.043" x2="10" y2="22.435" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="A" x1="18" y1="2" x2="18" y2="48.452" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><clipPath id="eye-crossed-a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><linearGradient id="paint_angry_linear" x1="16" y1="4.666" x2="16" y2="24.261" gradientUnits="userSpaceOnUse"><stop stop-color="#F7B259"></stop><stop offset="1" stop-color="#EE3323"></stop></linearGradient><linearGradient id="paint_sad_linear" x1="16" y1="3.937" x2="16" y2="25.501" gradientUnits="userSpaceOnUse"><stop stop-color="#5EAEE5"></stop><stop offset=".455" stop-color="#AAB09F"></stop><stop offset=".823" stop-color="#E2B16D"></stop><stop offset="1" stop-color="#F7B259"></stop></linearGradient><rect id="a" width="36" height="20" rx="4"></rect><mask id="b" x="0" y="0" width="36" height="20" fill="#fff"><use xlink:href="#a"></use></mask><clipPath id="clip0comm"><path fill="#fff" transform="translate(.5)" d="M0 0h16v16H0z"></path></clipPath><mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="43" height="42"><path d="M24.094 45c11.598 0 21-9.402 21-21s-9.402-21-21-21-21 9.402-21 21 9.402 21 21 21Z" fill="#8AC858"></path></mask><radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 5.57802 -6.33866 0 10.278 10)"><stop stop-color="#3E671B"></stop><stop offset="1" stop-color="#77AB4F"></stop></radialGradient><linearGradient id="paint0_linear_2" x1="23.5" y1="-4.5" x2="10" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".505" stop-color="#FFC000" stop-opacity=".495"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="paint10_linear" x1="2.83" y1="19.846" x2="5.026" y2="26.633" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear_2" x1="9.659" y1="15.773" x2="18.46" y2="20.493" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear_2" x1="9.551" y1="16.552" x2="18.583" y2="20.803" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear_2" x1="7.139" y1="18.488" x2="14.598" y2="24.775" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset="1" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint5_linear" x1="41.782" y1="18.301" x2="9.337" y2="-7.723" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".222" stop-color="#FFCB7D"></stop><stop offset=".577" stop-color="#FF9333"></stop><stop offset="1" stop-color="#FFD99C"></stop></linearGradient><radialGradient id="paint3_radial_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.15404 .55622 -.33024 4.84121 14.294 14.948)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint66_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.84356 -2.56095 8.4484 6.0818 18.636 4.431)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.23117 3.3889 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.3077 4.51068 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.76699 0 0 5.75381 1.884 8.65)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.43105 -.26644 .38005 4.89394 4.493 9.557)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.9258 .69277 -.81353 2.26147 9.542 12.416)"><stop stop-color="#C13B12"></stop><stop offset=".723" stop-color="#DD5D28" stop-opacity=".414"></stop><stop offset=".994" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint9_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-155.017 6.478 1.388) scale(14.9871 12.8928)"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".299" stop-color="#F9C16E"></stop><stop offset=".528" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-54.251 6.526 -7.925) scale(2.20888 7.28696)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="paint0_linear" x1="-1.92" y1="12.792" x2="-.382" y2="17.543" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear" x1="2.861" y1="9.941" x2="9.022" y2="13.245" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear" x1="2.785" y1="10.486" x2="9.108" y2="13.462" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear" x1="1.097" y1="11.842" x2="6.318" y2="16.243" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><radialGradient id="pkb1" cx="0" cy="0" r="1" gradientTransform="matrix(11.6095 .75457 -.56841 8.74533 13.24 18.89)" gradientUnits="userSpaceOnUse"><stop stop-color="#BD350E"></stop><stop offset=".44" stop-color="#BF3710" stop-opacity=".51"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".3"></stop><stop offset=".76" stop-color="#CE4B20" stop-opacity=".15"></stop><stop offset=".87" stop-color="#DB5C2E" stop-opacity=".03"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb2" cx="0" cy="0" r="1" gradientTransform="matrix(7.21222 0 0 10.9971 4.56 17.34)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb3" cx="0" cy="0" r="1" gradientTransform="matrix(6.56903 -.50925 .72514 9.35388 9.55 19.07)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb4" cx="0" cy="0" r="1" gradientTransform="matrix(3.6871 1.32408 -1.5528 4.32403 19.22 24.54)" gradientUnits="userSpaceOnUse"><stop stop-color="#C13B12"></stop><stop offset=".72" stop-color="#DD5D28" stop-opacity=".41"></stop><stop offset=".99" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb5" cx="0" cy="0" r="1" gradientTransform="rotate(-155.06 12.96 2.95) scale(28.6853 24.6493)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".13" stop-color="#FFE491"></stop><stop offset=".3" stop-color="#F9C16E"></stop><stop offset=".53" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="pkb6" cx="0" cy="0" r="1" gradientTransform="matrix(2.47103 -3.42617 11.3089 8.15624 18.457 4.628)" gradientUnits="userSpaceOnUse"><stop stop-color="#E74C29"></stop><stop offset=".26" stop-color="#DE4926"></stop><stop offset=".67" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="pkb0" x1="3.05" x2="13.03" y1="23.44" y2="31.87" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".65" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint-preloader_linear" x1="12.083" y1="2.75" x2="14.417" y2="8" gradientUnits="userSpaceOnUse"><stop stop-color="#BDBDBF"></stop><stop offset=".526" stop-color="#BDBDBF" stop-opacity=".6"></stop><stop offset="1" stop-color="#BDBDBF" stop-opacity="0"></stop></linearGradient><circle id="loadingCircle" cx="50" cy="50" r="50"></circle><mask id="loadingCircleMask" x="0" y="0" width="100" height="100" fill="#fff"><use xlink:href="#loadingCircle"></use></mask><linearGradient id="telegram-logo-16-a" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram-logo-16-b" x1="9.003" y1="8.001" x2="10.251" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><linearGradient id="telegram_paint11_linear" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram_paint1_linear" x1="9.003" y1="8.001" x2="10.252" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><clipPath id="clip1"><path fill="#fff" transform="translate(6 6)" d="M0 0h12v12H0z"></path></clipPath></defs> + <symbol id="icon--emotions__kind" viewBox="0 0 32 32"><path d="M3.465 10.652C3.919 7.779 5.074 2.156 16 2.156c11.388 0 12.147 5.688 12.649 8.496-4.522 4.38-25.184 0-25.184 0Z" fill="#FCD996"></path><path d="M29.206 16.568c-.133 8.36-5.773 12.729-13.164 12.729s-13.25-5.22-13.254-12.503C2.784 4.668 8.606 2.924 15.997 2.924c7.39 0 13.4 1.569 13.209 13.644Z" fill="#F7B259"></path><path d="M3.266 20.274S7.112 26.75 16 26.75s12.734-6.476 12.734-6.476-1.38 9.57-12.691 9.57c-11.312 0-12.777-9.57-12.777-9.57Z" fill="#D17715"></path><path d="M11.313 7.382c-.466-.1-1.494 1.508-3.414 2.642-1.92 1.134-2.8 1.147-2.86 1.562-.061.416-.254 1.349.087 1.342.34-.007 3.26-.43 5.085-1.492 1.825-1.063 1.704-1.59 1.713-2.041.009-.451-.2-1.924-.611-2.013Z" fill="#6A3614"></path><path d="M4.96 12.098s-.192.81.144.838c.336.028 3.229-.462 4.779-1.264 1.55-.803 1.904-1.41 2-1.702.096-.294.044-1.097-.05-1.408 0 0-.026.834-.567 1.275-.543.44-1.6 1.358-3.604 2.073-2.004.715-2.387.58-2.49.559-.165-.035-.211-.371-.211-.371Z" fill="#512A10"></path><path d="M20.687 7.382c.466-.1 1.494 1.508 3.414 2.642 1.92 1.134 2.799 1.147 2.86 1.562.06.415.254 1.349-.087 1.342-.341-.007-3.26-.43-5.085-1.492-1.825-1.063-1.704-1.59-1.713-2.041-.01-.451.2-1.924.61-2.013Z" fill="#6A3614"></path><path d="M27.039 12.098s.193.81-.143.838c-.336.028-3.23-.462-4.78-1.264-1.55-.802-1.903-1.41-2-1.702-.095-.294-.043-1.097.05-1.408 0 0 .027.834.568 1.275.542.44 1.599 1.358 3.603 2.073 2.004.715 2.387.58 2.49.559.166-.035.212-.371.212-.371Z" fill="#512A10"></path><path d="M5.692 19.274c-1.054-.12-1.396.703-.773 1.383.623.679 4.72.89 4.997.06.277-.83-.557-1.242-1.468-1.307-.911-.065-2.116-.063-2.756-.136ZM26.526 19.139c1.044-.141 1.4.674.793 1.366-.606.692-4.673.988-4.963.164-.29-.824.53-1.253 1.434-1.337.903-.084 2.102-.107 2.736-.193Z" fill="#FF7373"></path><path d="M11.798 14.529c-.612-.025-.752 2.055-2.904 1.917-2.151-.138-2.144-1.696-2.724-1.834-.58-.138-1.683-.304-1.738.11-.055.414.745 3.504 4.8 3.449 4.057-.055 4.415-3.256 4.222-3.421-.193-.166-.966-.193-1.656-.221ZM18.944 14.529c-.692.058-.11 1.904 1.076 2.759 1.186.855 3.752 1.407 5.49.469 1.738-.938 2.318-3.09 1.959-3.256-.36-.166-1.766.055-1.904.248-.138.193-.248 1.683-2.456 1.655-2.428-.03-2.317-1.821-2.814-1.848-.497-.027-1.02-.055-1.351-.027Z" fill="#6A3614"></path><path d="M4.435 14.739c-.031.32.517 2.42 2.7 3.166 2.183.745 4.314.103 5.204-.693.89-.797 1.107-1.707 1.148-1.997.032-.225.083-.427-.155-.538-.277.948-.414 1.5-1.48 2.276-1.065.776-3.237 1.097-4.665.393-1.427-.703-2.555-1.976-2.752-2.607ZM21.092 17.357c1.976.724 4.1.42 5.11-.528.849-.797 1.139-1.562 1.336-2.255.093.197-.041 1.252-.766 2.183-.725.93-1.811 1.593-3.86 1.53-2.048-.061-3.59-1.22-4.034-2.327-.445-1.107-.2-1.286-.083-1.386.052.445.32 2.059 2.297 2.783Z" fill="#512A10"></path><path d="M6.82 16.884s.83.676 2.013.719c1.182.043 1.545-.218 1.545-.218s-.54.219-1.738.017c-.845-.142-1.914-.887-1.82-.518ZM21.062 17.043s.874.619 2.055.583c1.182-.036 1.527-.321 1.527-.321s-.57.217-1.733.133c-.89-.064-1.968-.757-1.85-.395Z" fill="#B2622B"></path><path d="M11.059 20.45s-.403.555-.05 1.462c.353.907 1.914 3.173 4.886 3.224 2.972.05 5.994-2.67 5.238-4.987 0 0-1.108 1.864-2.015 2.569-.907.705-2.015 1.41-2.62 1.31-.603-.101-2.87-1.108-3.071-1.31a648.413 648.413 0 0 0-2.368-2.267Z" fill="#FDDC9A"></path><path d="M11.546 19.998c-.756.189-.638.94-.302 1.645.336.705 1.679 3.089 5.17 3.022 3.493-.067 4.736-3.627 4.736-4.332 0-.705-1.444-.705-1.746-.302-.302.403-.57 3.489-3.157 3.459-2.888-.034-2.955-3.224-3.56-3.46-.604-.233-.873-.1-1.141-.032Z" fill="#6A3614"></path><path d="M16.065 2.03c-5.104 0-13.667.39-13.667 3 0 .835.878 1.44 2.234 1.88 2.888.938 7.959 1.12 11.433 1.12 3.483 0 8.573-.182 11.457-1.127 1.343-.44 2.21-1.043 2.21-1.872 0-2.61-8.563-3-13.667-3Zm0 4c-4.852 0-8.256-.406-10.138-.821-.271-.06-.51-.12-.717-.178.328-.093.736-.188 1.22-.282l.005-.001C8.228 3.265 11.15 2.156 16 2.156c5.057 0 8.015 1.123 9.788 2.616.436.086.829.174 1.132.259-.194.055-.42.11-.67.166-1.87.42-5.287.834-10.185.834Z" fill="#00BDF2"></path><path d="M28.066 3.364C25.324 2.242 19.783 2.03 16.065 2.03c-3.49 0-8.595.183-11.476 1.133-.2.076-1.152.47-.73.988.47.577 1.35.879 1.35.879.329-.093.737-.188 1.222-.282l.004-.001C8.228 3.265 11.15 2.156 16 2.156c5.057 0 8.015 1.123 9.788 2.616.436.086.829.174 1.132.259 2.259-.811 1.36-1.525 1.146-1.667Z" fill="#009DDB"></path></symbol> + <use xlink:href="#icon--emotions__kind"/> +</svg> diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg new file mode 100644 index 000000000..15f062564 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs><linearGradient id="B" x1="10" y1="1.043" x2="10" y2="22.435" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="A" x1="18" y1="2" x2="18" y2="48.452" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><clipPath id="eye-crossed-a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><linearGradient id="paint_angry_linear" x1="16" y1="4.666" x2="16" y2="24.261" gradientUnits="userSpaceOnUse"><stop stop-color="#F7B259"></stop><stop offset="1" stop-color="#EE3323"></stop></linearGradient><linearGradient id="paint_sad_linear" x1="16" y1="3.937" x2="16" y2="25.501" gradientUnits="userSpaceOnUse"><stop stop-color="#5EAEE5"></stop><stop offset=".455" stop-color="#AAB09F"></stop><stop offset=".823" stop-color="#E2B16D"></stop><stop offset="1" stop-color="#F7B259"></stop></linearGradient><rect id="a" width="36" height="20" rx="4"></rect><mask id="b" x="0" y="0" width="36" height="20" fill="#fff"><use xlink:href="#a"></use></mask><clipPath id="clip0comm"><path fill="#fff" transform="translate(.5)" d="M0 0h16v16H0z"></path></clipPath><mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="43" height="42"><path d="M24.094 45c11.598 0 21-9.402 21-21s-9.402-21-21-21-21 9.402-21 21 9.402 21 21 21Z" fill="#8AC858"></path></mask><radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 5.57802 -6.33866 0 10.278 10)"><stop stop-color="#3E671B"></stop><stop offset="1" stop-color="#77AB4F"></stop></radialGradient><linearGradient id="paint0_linear_2" x1="23.5" y1="-4.5" x2="10" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".505" stop-color="#FFC000" stop-opacity=".495"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="paint10_linear" x1="2.83" y1="19.846" x2="5.026" y2="26.633" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear_2" x1="9.659" y1="15.773" x2="18.46" y2="20.493" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear_2" x1="9.551" y1="16.552" x2="18.583" y2="20.803" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear_2" x1="7.139" y1="18.488" x2="14.598" y2="24.775" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset="1" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint5_linear" x1="41.782" y1="18.301" x2="9.337" y2="-7.723" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".222" stop-color="#FFCB7D"></stop><stop offset=".577" stop-color="#FF9333"></stop><stop offset="1" stop-color="#FFD99C"></stop></linearGradient><radialGradient id="paint3_radial_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.15404 .55622 -.33024 4.84121 14.294 14.948)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint66_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.84356 -2.56095 8.4484 6.0818 18.636 4.431)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.23117 3.3889 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.3077 4.51068 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.76699 0 0 5.75381 1.884 8.65)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.43105 -.26644 .38005 4.89394 4.493 9.557)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.9258 .69277 -.81353 2.26147 9.542 12.416)"><stop stop-color="#C13B12"></stop><stop offset=".723" stop-color="#DD5D28" stop-opacity=".414"></stop><stop offset=".994" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint9_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-155.017 6.478 1.388) scale(14.9871 12.8928)"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".299" stop-color="#F9C16E"></stop><stop offset=".528" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-54.251 6.526 -7.925) scale(2.20888 7.28696)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="paint0_linear" x1="-1.92" y1="12.792" x2="-.382" y2="17.543" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear" x1="2.861" y1="9.941" x2="9.022" y2="13.245" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear" x1="2.785" y1="10.486" x2="9.108" y2="13.462" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear" x1="1.097" y1="11.842" x2="6.318" y2="16.243" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><radialGradient id="pkb1" cx="0" cy="0" r="1" gradientTransform="matrix(11.6095 .75457 -.56841 8.74533 13.24 18.89)" gradientUnits="userSpaceOnUse"><stop stop-color="#BD350E"></stop><stop offset=".44" stop-color="#BF3710" stop-opacity=".51"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".3"></stop><stop offset=".76" stop-color="#CE4B20" stop-opacity=".15"></stop><stop offset=".87" stop-color="#DB5C2E" stop-opacity=".03"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb2" cx="0" cy="0" r="1" gradientTransform="matrix(7.21222 0 0 10.9971 4.56 17.34)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb3" cx="0" cy="0" r="1" gradientTransform="matrix(6.56903 -.50925 .72514 9.35388 9.55 19.07)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb4" cx="0" cy="0" r="1" gradientTransform="matrix(3.6871 1.32408 -1.5528 4.32403 19.22 24.54)" gradientUnits="userSpaceOnUse"><stop stop-color="#C13B12"></stop><stop offset=".72" stop-color="#DD5D28" stop-opacity=".41"></stop><stop offset=".99" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb5" cx="0" cy="0" r="1" gradientTransform="rotate(-155.06 12.96 2.95) scale(28.6853 24.6493)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".13" stop-color="#FFE491"></stop><stop offset=".3" stop-color="#F9C16E"></stop><stop offset=".53" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="pkb6" cx="0" cy="0" r="1" gradientTransform="matrix(2.47103 -3.42617 11.3089 8.15624 18.457 4.628)" gradientUnits="userSpaceOnUse"><stop stop-color="#E74C29"></stop><stop offset=".26" stop-color="#DE4926"></stop><stop offset=".67" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="pkb0" x1="3.05" x2="13.03" y1="23.44" y2="31.87" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".65" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint-preloader_linear" x1="12.083" y1="2.75" x2="14.417" y2="8" gradientUnits="userSpaceOnUse"><stop stop-color="#BDBDBF"></stop><stop offset=".526" stop-color="#BDBDBF" stop-opacity=".6"></stop><stop offset="1" stop-color="#BDBDBF" stop-opacity="0"></stop></linearGradient><circle id="loadingCircle" cx="50" cy="50" r="50"></circle><mask id="loadingCircleMask" x="0" y="0" width="100" height="100" fill="#fff"><use xlink:href="#loadingCircle"></use></mask><linearGradient id="telegram-logo-16-a" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram-logo-16-b" x1="9.003" y1="8.001" x2="10.251" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><linearGradient id="telegram_paint11_linear" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram_paint1_linear" x1="9.003" y1="8.001" x2="10.252" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><clipPath id="clip1"><path fill="#fff" transform="translate(6 6)" d="M0 0h12v12H0z"></path></clipPath></defs> + <symbol id="icon--emotions__sad" viewBox="0 0 32 32"><path d="M3.465 10.652C3.919 7.779 5.074 2.156 16 2.156c11.388 0 12.147 5.688 12.649 8.496-4.522 4.38-25.184 0-25.184 0Z" fill="#BBE1FC"></path><path d="M29.206 16.568c-.133 8.36-5.773 12.729-13.164 12.729s-13.25-5.22-13.254-12.503C2.784 4.668 8.606 2.924 15.997 2.924c7.39 0 13.4 1.569 13.209 13.644Z" fill="url(#paint_sad_linear)"></path><path d="M3.266 20.274S7.112 26.75 16 26.75s12.734-6.476 12.734-6.476-1.38 9.57-12.691 9.57c-11.312 0-12.777-9.57-12.777-9.57Z" fill="#D17715"></path><path d="M10.956 23.536s.339.818 1.333.86c.994.044 1.096-.59 1.208-1.006.112-.417.218-2.4 1.57-2.651.79-.146 2.063-.157 2.72 1.11.319.613.138 2.475 1.33 2.682 1.656.287 1.709-1.161 1.709-1.161s-2.703-4.08-3.485-4.29c-.782-.21-3.058-.262-3.243-.2-.184.062-1.076.884-1.264 1.169-.187.284-1.878 3.487-1.878 3.487Z" fill="#FCD996"></path><path d="M15.568 18.395c-1.962 0-3.621 1.434-4.121 2.647-.594 1.44-.683 2.58-.355 2.68.623.19 1.487.289 1.622.064.135-.224.233-2.906 1.78-3.533 1.547-.627 2.839-.113 3.642 1.035.834 1.193.394 2.658 1.048 2.722.653.064 1.808-.073 1.675-.965-.134-.894-1.554-4.65-5.291-4.65ZM12.045 13.958c-.53.046-.482.497-.677 1.045-.194.548-.938 1.708-2.333 1.534-1.394-.174-1.857-1.117-2.025-1.805-.168-.688-.286-.857-.84-.92-.552-.061-1.206.218-1.087.959.119.74 1.052 3.4 4.61 3.303 3.558-.098 4.096-3.302 4.04-3.687-.053-.386-.894-.5-1.688-.43Z" fill="#6A3614"></path><path d="M5.507 13.904s-.173.302.184 1.147c.357.844 1.443 2.07 2.984 2.372 1.542.3 3.073-.392 3.533-.93.46-.536.874-1.776.894-2.072.02-.296-.092-.43-.092-.43s.602.009.715.31c.113.303-.18 1.645-1.152 2.722-.971 1.076-3.305 1.467-4.811.873-1.506-.594-2.462-1.916-2.697-2.893-.232-.978.442-1.1.442-1.1Z" fill="#512A10"></path><path d="M25.015 14.333c-.53.046-.482.497-.677 1.045-.194.548-.938 1.708-2.333 1.534-1.394-.174-1.857-1.117-2.025-1.805-.168-.688-.286-.857-.84-.92-.552-.061-1.206.218-1.087.959.119.74 1.052 3.4 4.61 3.303 3.558-.098 4.096-3.302 4.04-3.687-.054-.386-.894-.5-1.688-.43Z" fill="#6A3614"></path><path d="M18.477 14.279s-.173.302.184 1.147c.357.844 1.443 2.07 2.984 2.372 1.542.3 3.073-.392 3.533-.93.46-.536.874-1.776.894-2.072.02-.297-.092-.43-.092-.43s.602.009.715.31c.113.303-.18 1.645-1.152 2.722-.971 1.076-3.305 1.467-4.811.873-1.506-.594-2.462-1.916-2.697-2.893-.235-.977.442-1.1.442-1.1Z" fill="#512A10"></path><path d="M11.054 5.965c-.564-.143-.684.947-1.561 2.014-.878 1.067-2.206 1.428-3.057 1.473-.851.045-.904.309-.891.688.013.379.037.733.639.87.602.139 3.112.244 4.92-1.264 1.81-1.508 2.123-2.324 1.835-2.732-.288-.408-1.366-.917-1.885-1.05Z" fill="#1576B7"></path><path d="M12.269 6.489s.697.32.772.725c.074.404-.341 1.19-1.356 2.07-1.015.882-2.294 1.927-4.504 1.915-1.975-.01-1.64-1.017-1.64-1.017s.303.342 1.068.433c.765.09 2.077-.162 3.026-.682.95-.52 1.819-1.361 2.26-1.936.442-.576.644-1.194.374-1.508Z" fill="#115F9F"></path><path d="M20.622 6.543c.564-.143.684.947 1.562 2.014.878 1.067 2.206 1.428 3.057 1.473.85.045.904.309.89.688-.012.379-.036.733-.638.87-.602.139-3.112.244-4.921-1.264C18.762 8.816 18.45 8 18.738 7.592c.288-.41 1.365-.918 1.884-1.05Z" fill="#1576B7"></path><path d="M19.407 7.066s-.697.32-.772.725c-.074.404.34 1.19 1.356 2.07 1.015.88 2.294 1.926 4.504 1.914 1.975-.011 1.639-1.017 1.639-1.017s-.302.342-1.067.433c-.765.09-2.077-.162-3.026-.682-.95-.52-1.82-1.361-2.261-1.936-.441-.574-.643-1.192-.373-1.507Z" fill="#115F9F"></path><path d="M10.522 17.007s1.094-.121 1.337-.691c.243-.57-.213.3-1.337.69ZM20.946 17.273s-1.12-.39-1.4-.957c-.34-.695.464.439 1.4.957Z" fill="#B2622B"></path><path d="M6.77 18.855c-.476.45-1.371 1.868-1.774 3.205-.426 1.414.114 2.738 1.674 2.738 1.604 0 2.178-1.33 1.779-2.744-.384-1.357-1.34-2.792-1.678-3.2Z" fill="#5AA8E5"></path><path d="M6.994 24.022c-1.574.685-2.128-.937-2.134-.956.039.977.634 1.732 1.81 1.732 1.605 0 2.179-1.33 1.78-2.744-.383-1.358-1.34-2.793-1.678-3.2-.001 0 2.069 4.364.222 5.168Z" fill="#2F83BC"></path></symbol> + <use xlink:href="#icon--emotions__sad"/> +</svg> diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg new file mode 100644 index 000000000..68c67cc0b --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs><linearGradient id="B" x1="10" y1="1.043" x2="10" y2="22.435" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="A" x1="18" y1="2" x2="18" y2="48.452" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><clipPath id="eye-crossed-a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><linearGradient id="paint_angry_linear" x1="16" y1="4.666" x2="16" y2="24.261" gradientUnits="userSpaceOnUse"><stop stop-color="#F7B259"></stop><stop offset="1" stop-color="#EE3323"></stop></linearGradient><linearGradient id="paint_sad_linear" x1="16" y1="3.937" x2="16" y2="25.501" gradientUnits="userSpaceOnUse"><stop stop-color="#5EAEE5"></stop><stop offset=".455" stop-color="#AAB09F"></stop><stop offset=".823" stop-color="#E2B16D"></stop><stop offset="1" stop-color="#F7B259"></stop></linearGradient><rect id="a" width="36" height="20" rx="4"></rect><mask id="b" x="0" y="0" width="36" height="20" fill="#fff"><use xlink:href="#a"></use></mask><clipPath id="clip0comm"><path fill="#fff" transform="translate(.5)" d="M0 0h16v16H0z"></path></clipPath><mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="43" height="42"><path d="M24.094 45c11.598 0 21-9.402 21-21s-9.402-21-21-21-21 9.402-21 21 9.402 21 21 21Z" fill="#8AC858"></path></mask><radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 5.57802 -6.33866 0 10.278 10)"><stop stop-color="#3E671B"></stop><stop offset="1" stop-color="#77AB4F"></stop></radialGradient><linearGradient id="paint0_linear_2" x1="23.5" y1="-4.5" x2="10" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".505" stop-color="#FFC000" stop-opacity=".495"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="paint10_linear" x1="2.83" y1="19.846" x2="5.026" y2="26.633" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear_2" x1="9.659" y1="15.773" x2="18.46" y2="20.493" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear_2" x1="9.551" y1="16.552" x2="18.583" y2="20.803" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear_2" x1="7.139" y1="18.488" x2="14.598" y2="24.775" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset="1" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint5_linear" x1="41.782" y1="18.301" x2="9.337" y2="-7.723" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".222" stop-color="#FFCB7D"></stop><stop offset=".577" stop-color="#FF9333"></stop><stop offset="1" stop-color="#FFD99C"></stop></linearGradient><radialGradient id="paint3_radial_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.15404 .55622 -.33024 4.84121 14.294 14.948)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint66_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.84356 -2.56095 8.4484 6.0818 18.636 4.431)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.23117 3.3889 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.3077 4.51068 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.76699 0 0 5.75381 1.884 8.65)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.43105 -.26644 .38005 4.89394 4.493 9.557)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.9258 .69277 -.81353 2.26147 9.542 12.416)"><stop stop-color="#C13B12"></stop><stop offset=".723" stop-color="#DD5D28" stop-opacity=".414"></stop><stop offset=".994" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint9_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-155.017 6.478 1.388) scale(14.9871 12.8928)"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".299" stop-color="#F9C16E"></stop><stop offset=".528" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-54.251 6.526 -7.925) scale(2.20888 7.28696)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="paint0_linear" x1="-1.92" y1="12.792" x2="-.382" y2="17.543" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear" x1="2.861" y1="9.941" x2="9.022" y2="13.245" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear" x1="2.785" y1="10.486" x2="9.108" y2="13.462" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear" x1="1.097" y1="11.842" x2="6.318" y2="16.243" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><radialGradient id="pkb1" cx="0" cy="0" r="1" gradientTransform="matrix(11.6095 .75457 -.56841 8.74533 13.24 18.89)" gradientUnits="userSpaceOnUse"><stop stop-color="#BD350E"></stop><stop offset=".44" stop-color="#BF3710" stop-opacity=".51"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".3"></stop><stop offset=".76" stop-color="#CE4B20" stop-opacity=".15"></stop><stop offset=".87" stop-color="#DB5C2E" stop-opacity=".03"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb2" cx="0" cy="0" r="1" gradientTransform="matrix(7.21222 0 0 10.9971 4.56 17.34)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb3" cx="0" cy="0" r="1" gradientTransform="matrix(6.56903 -.50925 .72514 9.35388 9.55 19.07)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb4" cx="0" cy="0" r="1" gradientTransform="matrix(3.6871 1.32408 -1.5528 4.32403 19.22 24.54)" gradientUnits="userSpaceOnUse"><stop stop-color="#C13B12"></stop><stop offset=".72" stop-color="#DD5D28" stop-opacity=".41"></stop><stop offset=".99" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb5" cx="0" cy="0" r="1" gradientTransform="rotate(-155.06 12.96 2.95) scale(28.6853 24.6493)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".13" stop-color="#FFE491"></stop><stop offset=".3" stop-color="#F9C16E"></stop><stop offset=".53" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="pkb6" cx="0" cy="0" r="1" gradientTransform="matrix(2.47103 -3.42617 11.3089 8.15624 18.457 4.628)" gradientUnits="userSpaceOnUse"><stop stop-color="#E74C29"></stop><stop offset=".26" stop-color="#DE4926"></stop><stop offset=".67" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="pkb0" x1="3.05" x2="13.03" y1="23.44" y2="31.87" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".65" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint-preloader_linear" x1="12.083" y1="2.75" x2="14.417" y2="8" gradientUnits="userSpaceOnUse"><stop stop-color="#BDBDBF"></stop><stop offset=".526" stop-color="#BDBDBF" stop-opacity=".6"></stop><stop offset="1" stop-color="#BDBDBF" stop-opacity="0"></stop></linearGradient><circle id="loadingCircle" cx="50" cy="50" r="50"></circle><mask id="loadingCircleMask" x="0" y="0" width="100" height="100" fill="#fff"><use xlink:href="#loadingCircle"></use></mask><linearGradient id="telegram-logo-16-a" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram-logo-16-b" x1="9.003" y1="8.001" x2="10.251" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><linearGradient id="telegram_paint11_linear" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram_paint1_linear" x1="9.003" y1="8.001" x2="10.252" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><clipPath id="clip1"><path fill="#fff" transform="translate(6 6)" d="M0 0h12v12H0z"></path></clipPath></defs> + <symbol id="icon--emotions__smile" viewBox="0 0 32 32"><path d="M3.465 10.652C3.919 7.779 5.074 2.156 16 2.156c11.388 0 12.147 5.688 12.649 8.496-4.522 4.38-25.184 0-25.184 0Z" fill="#FCD996"></path><path d="M29.206 16.568c-.133 8.36-5.773 12.729-13.164 12.729s-13.25-5.22-13.254-12.503C2.784 4.668 8.606 2.924 15.997 2.924c7.39 0 13.4 1.569 13.209 13.644Z" fill="#F7B259"></path><path d="M3.266 20.274S7.112 26.75 16 26.75s12.734-6.476 12.734-6.476-1.38 9.57-12.691 9.57c-11.312 0-12.777-9.57-12.777-9.57Z" fill="#D17715"></path><path d="M8.645 19.603s1.102 6.796 7.453 6.429c6.35-.367 7.865-4.948 7.878-7.39-8.437 1.247-15.331.96-15.331.96Z" fill="#FCD996"></path><path d="M9.154 17.666c2.24.278 9 .975 14.356.035.21-.037.62.144.565.574-.231 1.805-1.374 6.555-7.013 7.142-6.336.659-8.425-4.55-8.653-7.147-.023-.266.259-.664.745-.604Z" fill="#6A3614"></path><path d="M23.334 18.404s-7.081 1.099-14.22-.13c0 0 .109 1.195.338 1.653 0 0 5.176 1.176 12.082.265 1.978-.262 1.8-1.788 1.8-1.788Z" fill="#fff"></path><path d="M17.426 20.764c.68.113 1.368.947.735 1.68-.633.734-2.86 1.013-3.261.546-.402-.467-.841-1.924.014-2.125.855-.201.995.084 1.003.326.008.242-.27.722.19.71.462-.01.084-.58.167-.745.084-.166.543-.494 1.152-.392Z" fill="#512A10"></path><path d="M12.063 23.679s1.555 1.123 4.48 1.12c2.925-.003 4.479-1.49 4.479-1.49s-.306-1.046-1.698-1.21c-2.006-.237-2.832.712-2.832.712s-1.222-.834-2.264-.713c-1.448.169-2.165 1.58-2.165 1.58Z" fill="#C31517"></path><path d="M9.114 18.274s.01.323.055.475c0 0 3.154.577 7.513.604 4.359.027 6.62-.506 6.62-.506s.07-.285.033-.444c0 0-3.191.48-6.7.437-4.97-.061-7.521-.566-7.521-.566Z" fill="#DBC8CF"></path><path d="M25.04 15.976c-1.031-.003-1.57 1.033-.842 1.368.727.335 3.97.95 4.288.344.319-.606.334-1.157-.726-1.392-.574-.127-2.093-.318-2.72-.32ZM7.43 15.868c1.098.056 1.613 1.183.82 1.495-.791.313-4.274.776-4.58.116-.304-.66-.291-1.243.85-1.43.618-.101 2.243-.215 2.91-.181Z" fill="#FF7373"></path><path d="M10.038 3.294c.546-.102 1.367 1.57.934 2.006-.434.434-2.07 1.097-2.855 1.998-.785.9-1.13 1.445-1.724 1.42-.594-.023-.902-.785-.33-1.863.572-1.078 1.879-3.172 3.975-3.561ZM22.29 3.239c-.653-.208-1.041.687-1.304 1.264-.263.577-.205 1.203.275 1.277.48.074 1.868.578 2.765 1.325.897.746 1.072 1.362 1.374 1.216.302-.146.928-.638.857-.986-.071-.35-.592-3.024-3.966-4.096ZM8.655 10.159c-1.68.1-3.466 2.006-3.526 3.335-.06 1.329.005 1.55.647 1.583.642.032 1.222-.112 1.29-.347.07-.235-.205-2.176 1.726-2.576 1.97-.41 2.37 1.47 2.683 1.898.313.427.938.279 1.699.118.519-.11-.21-4.27-4.52-4.011ZM24.136 10.21c1.692.127 3.469 2.06 3.513 3.39.043 1.33-.027 1.55-.674 1.573-.648.023-1.231-.131-1.298-.366-.067-.236.237-2.172-1.706-2.603-1.982-.44-2.411 1.434-2.733 1.857-.322.423-.95.265-1.716.092-.522-.118.269-4.266 4.614-3.942Z" fill="#6A3614"></path><path d="M10.767 3.937s.065.704-.69 1.089C9.325 5.41 8.145 6.309 7.6 6.964c-.545.655-.675 1.245-1.137 1.367-.462.122-.677-.201-.677-.201s.062.678.665.649c.602-.03 1.14-.762 1.496-1.19.356-.426 1.142-1.025 1.669-1.31.526-.285 1.403-.76 1.484-1.031.08-.27.032-.792-.332-1.311ZM21.26 4.044s-.078.586.503.9c.58.316 1.744.657 2.556 1.477.812.82.795 1.216 1.09 1.268.294.052.799-.18.799-.468 0 0 .176.194-.057.53-.232.338-.754.675-.941.64-.187-.035-.928-1.23-1.892-1.694-.964-.464-2.022-.814-2.351-.968-.33-.154-.261-1.134.293-1.685ZM5.159 14.662s-.046.49.77.446c.818-.045 1.114-.133 1.177-.443.063-.31.152-1.991 1.336-2.343 1.184-.352 1.982.06 2.265.492.283.43.68 1.458 1.085 1.543.403.085 1.336-.031 1.467-.256.13-.225.035-.611.035-.611s-.296.423-1.07.28c-.506-.092-.675-1.397-1.723-1.88-1.048-.481-2.65-.364-3.29.607-.641.97-.42 1.697-.827 1.93-.693.399-1.225.235-1.225.235ZM19.427 13.309s.223.386.768.416c.825.046 1.019-1.158 1.526-1.6.507-.442 1.485-1.05 2.912-.42 1.426.63 1.575 1.59 1.672 2.31.096.722.225.802.593.829.367.027.66-.091.66-.091s.136.474-.5.502c-.636.028-1.506-.166-1.572-.555-.066-.39.1-1.895-1.19-2.355-1.288-.46-1.89-.04-2.423.77-.534.81-.616 1.243-1.096 1.317-.48.075-1.191-.167-1.333-.273-.327-.243-.017-.85-.017-.85Z" fill="#512A10"></path><path d="M24.839 10.973s-.97-.618-2.094-.447c-1.125.172-1.37.49-1.11.58.259.09 1.462-.726 3.204-.133ZM10.268 10.96s-.968-.564-2.035-.333c-1.067.232-1.281.564-1.027.64.255.076 1.355-.805 3.062-.307Z" fill="#B2622B"></path></symbol> + <use xlink:href="#icon--emotions__smile"/> +</svg> diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg new file mode 100644 index 000000000..7d7ff8b15 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs><linearGradient id="B" x1="10" y1="1.043" x2="10" y2="22.435" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="A" x1="18" y1="2" x2="18" y2="48.452" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><clipPath id="eye-crossed-a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath><linearGradient id="paint_angry_linear" x1="16" y1="4.666" x2="16" y2="24.261" gradientUnits="userSpaceOnUse"><stop stop-color="#F7B259"></stop><stop offset="1" stop-color="#EE3323"></stop></linearGradient><linearGradient id="paint_sad_linear" x1="16" y1="3.937" x2="16" y2="25.501" gradientUnits="userSpaceOnUse"><stop stop-color="#5EAEE5"></stop><stop offset=".455" stop-color="#AAB09F"></stop><stop offset=".823" stop-color="#E2B16D"></stop><stop offset="1" stop-color="#F7B259"></stop></linearGradient><rect id="a" width="36" height="20" rx="4"></rect><mask id="b" x="0" y="0" width="36" height="20" fill="#fff"><use xlink:href="#a"></use></mask><clipPath id="clip0comm"><path fill="#fff" transform="translate(.5)" d="M0 0h16v16H0z"></path></clipPath><mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="43" height="42"><path d="M24.094 45c11.598 0 21-9.402 21-21s-9.402-21-21-21-21 9.402-21 21 9.402 21 21 21Z" fill="#8AC858"></path></mask><radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 5.57802 -6.33866 0 10.278 10)"><stop stop-color="#3E671B"></stop><stop offset="1" stop-color="#77AB4F"></stop></radialGradient><linearGradient id="paint0_linear_2" x1="23.5" y1="-4.5" x2="10" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".505" stop-color="#FFC000" stop-opacity=".495"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"></stop></linearGradient><linearGradient id="paint10_linear" x1="2.83" y1="19.846" x2="5.026" y2="26.633" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear_2" x1="9.659" y1="15.773" x2="18.46" y2="20.493" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear_2" x1="9.551" y1="16.552" x2="18.583" y2="20.803" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear_2" x1="7.139" y1="18.488" x2="14.598" y2="24.775" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset="1" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint5_linear" x1="41.782" y1="18.301" x2="9.337" y2="-7.723" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".222" stop-color="#FFCB7D"></stop><stop offset=".577" stop-color="#FF9333"></stop><stop offset="1" stop-color="#FFD99C"></stop></linearGradient><radialGradient id="paint3_radial_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.15404 .55622 -.33024 4.84121 14.294 14.948)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint66_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.84356 -2.56095 8.4484 6.0818 18.636 4.431)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.23117 3.3889 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.70792 .38936 -.3077 4.51068 6.105 9.364)"><stop stop-color="#BD350E"></stop><stop offset=".437" stop-color="#BF3710" stop-opacity=".509"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".304"></stop><stop offset=".755" stop-color="#CE4B20" stop-opacity=".152"></stop><stop offset=".867" stop-color="#DB5C2E" stop-opacity=".027"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.76699 0 0 5.75381 1.884 8.65)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.43105 -.26644 .38005 4.89394 4.493 9.557)"><stop stop-color="#E05E21"></stop><stop offset=".565" stop-color="#DF602A" stop-opacity=".366"></stop><stop offset=".891" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.9258 .69277 -.81353 2.26147 9.542 12.416)"><stop stop-color="#C13B12"></stop><stop offset=".723" stop-color="#DD5D28" stop-opacity=".414"></stop><stop offset=".994" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="paint9_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-155.017 6.478 1.388) scale(14.9871 12.8928)"><stop stop-color="#FFE491"></stop><stop offset=".129" stop-color="#FFE491"></stop><stop offset=".299" stop-color="#F9C16E"></stop><stop offset=".528" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-54.251 6.526 -7.925) scale(2.20888 7.28696)"><stop stop-color="#E74C29"></stop><stop offset=".259" stop-color="#DE4926"></stop><stop offset=".673" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="paint0_linear" x1="-1.92" y1="12.792" x2="-.382" y2="17.543" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint1_linear" x1="2.861" y1="9.941" x2="9.022" y2="13.245" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint2_linear" x1="2.785" y1="10.486" x2="9.108" y2="13.462" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint4_linear" x1="1.097" y1="11.842" x2="6.318" y2="16.243" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".645" stop-color="#DF6132"></stop></linearGradient><radialGradient id="pkb1" cx="0" cy="0" r="1" gradientTransform="matrix(11.6095 .75457 -.56841 8.74533 13.24 18.89)" gradientUnits="userSpaceOnUse"><stop stop-color="#BD350E"></stop><stop offset=".44" stop-color="#BF3710" stop-opacity=".51"></stop><stop offset=".62" stop-color="#C43E15" stop-opacity=".3"></stop><stop offset=".76" stop-color="#CE4B20" stop-opacity=".15"></stop><stop offset=".87" stop-color="#DB5C2E" stop-opacity=".03"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb2" cx="0" cy="0" r="1" gradientTransform="matrix(7.21222 0 0 10.9971 4.56 17.34)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb3" cx="0" cy="0" r="1" gradientTransform="matrix(6.56903 -.50925 .72514 9.35388 9.55 19.07)" gradientUnits="userSpaceOnUse"><stop stop-color="#E05E21"></stop><stop offset=".56" stop-color="#DF602A" stop-opacity=".37"></stop><stop offset=".89" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb4" cx="0" cy="0" r="1" gradientTransform="matrix(3.6871 1.32408 -1.5528 4.32403 19.22 24.54)" gradientUnits="userSpaceOnUse"><stop stop-color="#C13B12"></stop><stop offset=".72" stop-color="#DD5D28" stop-opacity=".41"></stop><stop offset=".99" stop-color="#DF602A" stop-opacity=".01"></stop><stop offset="1" stop-color="#DF6132" stop-opacity=".01"></stop></radialGradient><radialGradient id="pkb5" cx="0" cy="0" r="1" gradientTransform="rotate(-155.06 12.96 2.95) scale(28.6853 24.6493)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFE491"></stop><stop offset=".13" stop-color="#FFE491"></stop><stop offset=".3" stop-color="#F9C16E"></stop><stop offset=".53" stop-color="#F19645"></stop><stop offset="1" stop-color="#EE8635"></stop></radialGradient><radialGradient id="pkb6" cx="0" cy="0" r="1" gradientTransform="matrix(2.47103 -3.42617 11.3089 8.15624 18.457 4.628)" gradientUnits="userSpaceOnUse"><stop stop-color="#E74C29"></stop><stop offset=".26" stop-color="#DE4926"></stop><stop offset=".67" stop-color="#C7421C"></stop><stop offset="1" stop-color="#B03B13"></stop></radialGradient><linearGradient id="pkb0" x1="3.05" x2="13.03" y1="23.44" y2="31.87" gradientUnits="userSpaceOnUse"><stop stop-color="#DD571D"></stop><stop offset=".65" stop-color="#DF6132"></stop></linearGradient><linearGradient id="paint-preloader_linear" x1="12.083" y1="2.75" x2="14.417" y2="8" gradientUnits="userSpaceOnUse"><stop stop-color="#BDBDBF"></stop><stop offset=".526" stop-color="#BDBDBF" stop-opacity=".6"></stop><stop offset="1" stop-color="#BDBDBF" stop-opacity="0"></stop></linearGradient><circle id="loadingCircle" cx="50" cy="50" r="50"></circle><mask id="loadingCircleMask" x="0" y="0" width="100" height="100" fill="#fff"><use xlink:href="#loadingCircle"></use></mask><linearGradient id="telegram-logo-16-a" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram-logo-16-b" x1="9.003" y1="8.001" x2="10.251" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><linearGradient id="telegram_paint11_linear" x1="10.672" y1="2.672" x2="6.672" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#37AEE2"></stop><stop offset="1" stop-color="#1E96C8"></stop></linearGradient><linearGradient id="telegram_paint1_linear" x1="9.003" y1="8.001" x2="10.252" y2="10.845" gradientUnits="userSpaceOnUse"><stop stop-color="#EFF7FC"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient><clipPath id="clip1"><path fill="#fff" transform="translate(6 6)" d="M0 0h12v12H0z"></path></clipPath></defs> + <symbol id="icon--emotions__surprised" viewBox="0 0 32 32"><path d="M3.465 10.652C3.919 7.779 5.074 2.156 16 2.156c11.388 0 12.147 5.688 12.649 8.496-4.522 4.38-25.184 0-25.184 0Z" fill="#FCD996"></path><path d="M29.206 16.568c-.133 8.36-5.773 12.729-13.164 12.729s-13.25-5.22-13.254-12.503C2.784 4.668 8.606 2.924 15.997 2.924c7.39 0 13.4 1.569 13.209 13.644Z" fill="#F7B259"></path><path d="M3.266 20.274S7.112 26.75 16 26.75s12.734-6.476 12.734-6.476-1.38 9.57-12.691 9.57c-11.312 0-12.777-9.57-12.777-9.57Z" fill="#D17715"></path><path d="M13.733 23.218c-.088 1.303 1.038 2.657 2.646 2.657.958 0 2.604-.863 2.578-2.817-.025-1.914-4.876.02-4.876.02" fill="#FCD996"></path><path d="M16.31 25.398c1.543 0 2.794-1.772 2.794-3.958s-1.251-3.958-2.794-3.958c-1.543 0-2.794 1.772-2.794 3.958s1.25 3.958 2.794 3.958Z" fill="#6A3614"></path><path d="M16.115 23.799c-.89-.03-1.714-.584-1.964-1.44-.11-.372-.111-.761.144-1.07.407-.492 1.573-.382 1.71.134.139.52-.323.906.302.907.384 0 .105-.515.18-.805.135-.51.85-.8 1.424-.776.573.024 1.085 1.13.34 2.317-.4.639-1.348.759-2.136.733Z" fill="#512A10"></path><path d="M16.428 23.266c.115-.128.53-.532 1.118-.236.587.296.484 1.783-.513 2.039-.997.256-1.853-.008-2.106-.741-.253-.733.182-1.27.629-1.301.357-.026.677.15.872.239Z" fill="#C31517"></path><path d="M11.209 6.062c.799-.36.96-.594.746-1.25-.215-.656-.59-1.69-1.656-1.191-1.067.498-3.112 1.652-4.23 4.356-.638 1.544.059 1.574.355 1.822s.744-.337 1.26-1.074c.66-.943 1.707-1.844 3.525-2.663ZM22.823 3.98c-.781-.544-1.21-.153-1.512.182-.302.335-.812 1.468-.3 1.83.512.364 1.595.915 2.79 2.246.789.879.848 1.663 1.314 1.523.466-.14.804-.88.566-1.762-.24-.882-.768-2.562-2.858-4.018ZM10.17 10.309c1.485-.075 1.74 3.952 1.496 5.583-.245 1.63-1.526 2.217-3.21 2.144-1.28-.056-2.062-1-1.932-2.493.13-1.493 1.275-5.114 3.647-5.234ZM22.629 10.472c-2.2-.01-2.611 6.277-1.204 6.996 1.407.719 3.714.548 4.217-.703.503-1.251-.703-6.284-3.013-6.293Z" fill="#6A3614"></path><path d="M5.959 9.458s.47.178.85-.295 1.28-1.935 2.766-2.897c1.485-.962 1.858-1.076 2.047-1.376.189-.3.049-.773.049-.773s.522.773.4 1.329c-.121.557-.973.7-1.711 1.116-.738.415-1.876 1.263-2.331 1.838-.455.575-.922 1.245-1.294 1.46-.372.215-.637-.181-.776-.402ZM21.234 4.268s-.298.789.449 1.309c.747.52 2.128 1.54 2.73 2.398.604.859.629 1.337.877 1.348.248.01.378-.17.378-.17s-.126.52-.532.62c-.406.1-.52-.261-.93-.887-.408-.626-1.766-1.904-2.507-2.356-.741-.452-.971-.665-.984-1.03-.013-.363.32-1.034.519-1.232ZM20.628 14.5s.14 1.983 1.446 2.447c1.305.464 2.578.144 3.073-.224.495-.367.578-1.154.578-1.154s.263 1.045-.457 1.737c-.72.692-2.64.88-3.806.204-1.167-.678-.866-2.53-.834-3.01ZM11.726 14.31s-.154 2.093-1.388 2.699c-1.234.606-2.663.593-3.26-.195-.466-.617-.515-1.591-.515-1.591s-.282 1.124.272 2.05c.554.926 1.71.93 2.686.775.975-.155 1.814-.752 2.082-1.628.268-.876.123-2.11.123-2.11Z" fill="#512A10"></path><path d="M9.577 12.349c.813-.238 1.286 2.73.065 3.322-1.221.592-1.645-.071-1.451-.891.194-.818.695-2.23 1.386-2.431ZM22.976 12.142c.656-.195 2.07 3.134.676 3.58-1.394.448-1.72-.208-1.668-1.022.05-.815.276-2.346.992-2.558Z" fill="#B2622B"></path><path d="M9.296 12.477c.206.005-.117.614-.254.863s-.412.289-.45.142c-.038-.147.287-1.015.704-1.005ZM22.5 12.357c.262-.088.002.833-.062 1.09-.064.256-.38.528-.48.265-.096-.264.174-1.23.543-1.355Z" fill="#fff"></path></symbol> + <use xlink:href="#icon--emotions__surprised"/> +</svg> diff --git a/pikabu_ru/get_profile_rating.py b/pikabu_ru/get_profile_rating.py new file mode 100644 index 000000000..645d5ba44 --- /dev/null +++ b/pikabu_ru/get_profile_rating.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests +from bs4 import BeautifulSoup + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" + + +def get_profile_rating(url: str) -> int: + rs = session.get(url) + rs.raise_for_status() + + root = BeautifulSoup(rs.content, "html.parser") + profile_digital_el = root.select_one(".profile__digital") + if not profile_digital_el: + raise Exception('Element ".profile__digital" not found!') + + rating_str = profile_digital_el["aria-label"] + + # В рейтинге могут быть не цифровые символы, типа пробелов + return int("".join(c for c in rating_str if c.isdigit())) + + +if __name__ == "__main__": + url = "https://pikabu.ru/@RytsarSvezhego" + print(get_profile_rating(url)) + # 2016 + + url = "https://pikabu.ru/@tibidohtel" + print(get_profile_rating(url)) + # 104563 diff --git a/pikabu_ru_page_interview_fullstack__question_19/154800137443598227.png b/pikabu_ru/page_interview_fullstack__question_19/154800137443598227.png similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/154800137443598227.png rename to pikabu_ru/page_interview_fullstack__question_19/154800137443598227.png diff --git a/pikabu_ru_page_interview_fullstack__question_19/README.md b/pikabu_ru/page_interview_fullstack__question_19/README.md similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/README.md rename to pikabu_ru/page_interview_fullstack__question_19/README.md diff --git a/pikabu_ru/page_interview_fullstack__question_19/main.py b/pikabu_ru/page_interview_fullstack__question_19/main.py new file mode 100644 index 000000000..eb9d6407d --- /dev/null +++ b/pikabu_ru/page_interview_fullstack__question_19/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://pikabu.ru/page/interview/fullstack/ +# 19. Секретный агент Пикабу передал зашифрованное изображение. Вам необходимо расшифровать изображение и вывести его +# на страницу средствами JS (без сторонних библиотек). Алгоритм дешифрования известен: +# * пиксели перебираются слева направо для каждой строки; +# * для каждого пикселя вычисляется параметр s += x + y * 80 (изначально s = 0 и для каждого следующего пикселя +# значение увеличивается на x + y * 80), где x - колонка пикселя, y - строка; +# * для канала красного и синего цвета необходимо добавить параметр s; +# * для канала зеленого цвета необходимо отнять параметр s следующим образом: green = (green - s) & 0xff; + + +# pip install Pillow +from PIL import Image + + +image = Image.open("154800137443598227.png") +width, height = image.size +pixel = image.load() + +s = 0 + +for y in range(height): + for x in range(width): + s += x + y * 80 + + # RGBa, alpha-канал нас не интересует, только RGB + r, g, b, _ = pixel[x, y] + + r = (r + s) & 0xFF + g = (g - s) & 0xFF + b = (b + s) & 0xFF + + pixel[x, y] = r, g, b + + +image.save("result.png") diff --git a/pikabu_ru_page_interview_fullstack__question_19/result.png b/pikabu_ru/page_interview_fullstack__question_19/result.png similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/result.png rename to pikabu_ru/page_interview_fullstack__question_19/result.png diff --git a/pikabu_ru/profile_rating__tracking/db.py b/pikabu_ru/profile_rating__tracking/db.py new file mode 100644 index 000000000..d6f5668d0 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/db.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import shutil +import sys + +from pathlib import Path +from typing import Iterable, Type, TypeVar + +# pip install peewee +from peewee import ( + Model, + SqliteDatabase, + TextField, + DateTimeField, + IntegerField, + CharField, + ForeignKeyField, +) + +DIR = Path(__file__).resolve().parent + +sys.path.append(str(DIR.parent.parent)) +from shorten import shorten + + +# Absolute file name +DB_FILE_NAME = str(DIR / "db.sqlite") +DIR_BACKUP = DIR / "backup" + + +def db_create_backup(backup_dir=DIR_BACKUP) -> None: + backup_dir.mkdir(parents=True, exist_ok=True) + + file_name = str(dt.datetime.today().date()) + ".sqlite" + file_name = backup_dir / file_name + + shutil.copy(DB_FILE_NAME, file_name) + + +# Ensure foreign-key constraints are enforced. +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) + + +ChildModel = TypeVar("ChildModel", bound="BaseModel") + + +class BaseModel(Model): + """ + Базовая модель классов-таблиц + """ + + class Meta: + database = db + + def get_new(self) -> ChildModel: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> ChildModel: + return cls.select().first() + + @classmethod + def get_last(cls) -> ChildModel: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class ProfileRating(BaseModel): + url = TextField() + date = DateTimeField(default=dt.datetime.now) + value = IntegerField() + + @classmethod + def append(cls, url: str, value: int) -> "ProfileRating": + return cls.get_or_create(url=url, value=value)[0] + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + print() + + print(ProfileRating.get_last()) diff --git a/pikabu_ru/profile_rating__tracking/main.py b/pikabu_ru/profile_rating__tracking/main.py new file mode 100644 index 000000000..91b2d3ffb --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/main.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import sys +import traceback + +from pathlib import Path + +# pip install simple-wait +from simple_wait import wait + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from get_profile_rating import get_profile_rating + +from db import ProfileRating, db_create_backup + + +URL = "https://pikabu.ru/@RytsarSvezhego" + + +while True: + print(f"Started at {dt.datetime.now():%d/%m/%Y %H:%M:%S}\n") + + db_create_backup() + + try: + value = get_profile_rating(URL) + print(f"URL: {URL}\n {value}\n") + + ProfileRating.append(URL, value) + + wait(weeks=1) + + except Exception as e: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(tb) + + print("Wait 15 minutes") + wait(minutes=15) + + print() diff --git a/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js new file mode 100644 index 000000000..43203684c --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery")):"function"==typeof define&&define.amd?define(["exports","jquery"],e):e((t=t||self).bootstrap={},t.jQuery)}(this,function(t,p){"use strict";function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function s(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}function l(o){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{},e=Object.keys(r);"function"==typeof Object.getOwnPropertySymbols&&(e=e.concat(Object.getOwnPropertySymbols(r).filter(function(t){return Object.getOwnPropertyDescriptor(r,t).enumerable}))),e.forEach(function(t){var e,n,i;e=o,i=r[n=t],n in e?Object.defineProperty(e,n,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[n]=i})}return o}p=p&&p.hasOwnProperty("default")?p.default:p;var e="transitionend";function n(t){var e=this,n=!1;return p(this).one(m.TRANSITION_END,function(){n=!0}),setTimeout(function(){n||m.triggerTransitionEnd(e)},t),this}var m={TRANSITION_END:"bsTransitionEnd",getUID:function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(t){if(!t)return 0;var e=p(t).css("transition-duration"),n=p(t).css("transition-delay"),i=parseFloat(e),o=parseFloat(n);return i||o?(e=e.split(",")[0],n=n.split(",")[0],1e3*(parseFloat(e)+parseFloat(n))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){p(t).trigger(e)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)){var o=n[i],r=e[i],s=r&&m.isElement(r)?"element":(a=r,{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase());if(!new RegExp(o).test(s))throw new Error(t.toUpperCase()+': Option "'+i+'" provided type "'+s+'" but expected type "'+o+'".')}var a},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"!=typeof t.getRootNode)return t instanceof ShadowRoot?t:t.parentNode?m.findShadowRoot(t.parentNode):null;var e=t.getRootNode();return e instanceof ShadowRoot?e:null}};p.fn.emulateTransitionEnd=n,p.event.special[m.TRANSITION_END]={bindType:e,delegateType:e,handle:function(t){if(p(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var o="alert",r="bs.alert",a="."+r,c=p.fn[o],h={CLOSE:"close"+a,CLOSED:"closed"+a,CLICK_DATA_API:"click"+a+".data-api"},u="alert",f="fade",d="show",g=function(){function i(t){this._element=t}var t=i.prototype;return t.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},t.dispose=function(){p.removeData(this._element,r),this._element=null},t._getRootElement=function(t){var e=m.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=p(t).closest("."+u)[0]),n},t._triggerCloseEvent=function(t){var e=p.Event(h.CLOSE);return p(t).trigger(e),e},t._removeElement=function(e){var n=this;if(p(e).removeClass(d),p(e).hasClass(f)){var t=m.getTransitionDurationFromElement(e);p(e).one(m.TRANSITION_END,function(t){return n._destroyElement(e,t)}).emulateTransitionEnd(t)}else this._destroyElement(e)},t._destroyElement=function(t){p(t).detach().trigger(h.CLOSED).remove()},i._jQueryInterface=function(n){return this.each(function(){var t=p(this),e=t.data(r);e||(e=new i(this),t.data(r,e)),"close"===n&&e[n](this)})},i._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),i}();p(document).on(h.CLICK_DATA_API,'[data-dismiss="alert"]',g._handleDismiss(new g)),p.fn[o]=g._jQueryInterface,p.fn[o].Constructor=g,p.fn[o].noConflict=function(){return p.fn[o]=c,g._jQueryInterface};var _="button",v="bs.button",y="."+v,E=".data-api",b=p.fn[_],w="active",C="btn",T="focus",S='[data-toggle^="button"]',D='[data-toggle="buttons"]',I='input:not([type="hidden"])',A=".active",O=".btn",N={CLICK_DATA_API:"click"+y+E,FOCUS_BLUR_DATA_API:"focus"+y+E+" blur"+y+E},k=function(){function n(t){this._element=t}var t=n.prototype;return t.toggle=function(){var t=!0,e=!0,n=p(this._element).closest(D)[0];if(n){var i=this._element.querySelector(I);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(w))t=!1;else{var o=n.querySelector(A);o&&p(o).removeClass(w)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!this._element.classList.contains(w),p(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(w)),t&&p(this._element).toggleClass(w)},t.dispose=function(){p.removeData(this._element,v),this._element=null},n._jQueryInterface=function(e){return this.each(function(){var t=p(this).data(v);t||(t=new n(this),p(this).data(v,t)),"toggle"===e&&t[e]()})},s(n,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),n}();p(document).on(N.CLICK_DATA_API,S,function(t){t.preventDefault();var e=t.target;p(e).hasClass(C)||(e=p(e).closest(O)),k._jQueryInterface.call(p(e),"toggle")}).on(N.FOCUS_BLUR_DATA_API,S,function(t){var e=p(t.target).closest(O)[0];p(e).toggleClass(T,/^focus(in)?$/.test(t.type))}),p.fn[_]=k._jQueryInterface,p.fn[_].Constructor=k,p.fn[_].noConflict=function(){return p.fn[_]=b,k._jQueryInterface};var L="carousel",x="bs.carousel",P="."+x,H=".data-api",j=p.fn[L],R={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},F={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},M="next",W="prev",U="left",B="right",q={SLIDE:"slide"+P,SLID:"slid"+P,KEYDOWN:"keydown"+P,MOUSEENTER:"mouseenter"+P,MOUSELEAVE:"mouseleave"+P,TOUCHSTART:"touchstart"+P,TOUCHMOVE:"touchmove"+P,TOUCHEND:"touchend"+P,POINTERDOWN:"pointerdown"+P,POINTERUP:"pointerup"+P,DRAG_START:"dragstart"+P,LOAD_DATA_API:"load"+P+H,CLICK_DATA_API:"click"+P+H},K="carousel",Q="active",V="slide",Y="carousel-item-right",z="carousel-item-left",X="carousel-item-next",G="carousel-item-prev",$="pointer-event",J=".active",Z=".active.carousel-item",tt=".carousel-item",et=".carousel-item img",nt=".carousel-item-next, .carousel-item-prev",it=".carousel-indicators",ot="[data-slide], [data-slide-to]",rt='[data-ride="carousel"]',st={TOUCH:"touch",PEN:"pen"},at=function(){function r(t,e){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._element=t,this._indicatorsElement=this._element.querySelector(it),this._touchSupported="ontouchstart"in document.documentElement||0<navigator.maxTouchPoints,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var t=r.prototype;return t.next=function(){this._isSliding||this._slide(M)},t.nextWhenVisible=function(){!document.hidden&&p(this._element).is(":visible")&&"hidden"!==p(this._element).css("visibility")&&this.next()},t.prev=function(){this._isSliding||this._slide(W)},t.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(nt)&&(m.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},t.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},t.to=function(t){var e=this;this._activeElement=this._element.querySelector(Z);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)p(this._element).one(q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n<t?M:W;this._slide(i,this._items[t])}},t.dispose=function(){p(this._element).off(P),p.removeData(this._element,x),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},t._getConfig=function(t){return t=l({},R,t),m.typeCheckConfig(L,t,F),t},t._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;0<e&&this.prev(),e<0&&this.next()}},t._addEventListeners=function(){var e=this;this._config.keyboard&&p(this._element).on(q.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&p(this._element).on(q.MOUSEENTER,function(t){return e.pause(t)}).on(q.MOUSELEAVE,function(t){return e.cycle(t)}),this._config.touch&&this._addTouchEventListeners()},t._addTouchEventListeners=function(){var n=this;if(this._touchSupported){var e=function(t){n._pointerEvent&&st[t.originalEvent.pointerType.toUpperCase()]?n.touchStartX=t.originalEvent.clientX:n._pointerEvent||(n.touchStartX=t.originalEvent.touches[0].clientX)},i=function(t){n._pointerEvent&&st[t.originalEvent.pointerType.toUpperCase()]&&(n.touchDeltaX=t.originalEvent.clientX-n.touchStartX),n._handleSwipe(),"hover"===n._config.pause&&(n.pause(),n.touchTimeout&&clearTimeout(n.touchTimeout),n.touchTimeout=setTimeout(function(t){return n.cycle(t)},500+n._config.interval))};p(this._element.querySelectorAll(et)).on(q.DRAG_START,function(t){return t.preventDefault()}),this._pointerEvent?(p(this._element).on(q.POINTERDOWN,function(t){return e(t)}),p(this._element).on(q.POINTERUP,function(t){return i(t)}),this._element.classList.add($)):(p(this._element).on(q.TOUCHSTART,function(t){return e(t)}),p(this._element).on(q.TOUCHMOVE,function(t){var e;(e=t).originalEvent.touches&&1<e.originalEvent.touches.length?n.touchDeltaX=0:n.touchDeltaX=e.originalEvent.touches[0].clientX-n.touchStartX}),p(this._element).on(q.TOUCHEND,function(t){return i(t)}))}},t._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},t._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(tt)):[],this._items.indexOf(t)},t._getItemByDirection=function(t,e){var n=t===M,i=t===W,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+(t===W?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},t._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(Z)),o=p.Event(q.SLIDE,{relatedTarget:t,direction:e,from:i,to:n});return p(this._element).trigger(o),o},t._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(J));p(e).removeClass(Q);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&p(n).addClass(Q)}},t._slide=function(t,e){var n,i,o,r=this,s=this._element.querySelector(Z),a=this._getItemIndex(s),l=e||s&&this._getItemByDirection(t,s),c=this._getItemIndex(l),h=Boolean(this._interval);if(o=t===M?(n=z,i=X,U):(n=Y,i=G,B),l&&p(l).hasClass(Q))this._isSliding=!1;else if(!this._triggerSlideEvent(l,o).isDefaultPrevented()&&s&&l){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(l);var u=p.Event(q.SLID,{relatedTarget:l,direction:o,from:a,to:c});if(p(this._element).hasClass(V)){p(l).addClass(i),m.reflow(l),p(s).addClass(n),p(l).addClass(n);var f=parseInt(l.getAttribute("data-interval"),10);this._config.interval=f?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,f):this._config.defaultInterval||this._config.interval;var d=m.getTransitionDurationFromElement(s);p(s).one(m.TRANSITION_END,function(){p(l).removeClass(n+" "+i).addClass(Q),p(s).removeClass(Q+" "+i+" "+n),r._isSliding=!1,setTimeout(function(){return p(r._element).trigger(u)},0)}).emulateTransitionEnd(d)}else p(s).removeClass(Q),p(l).addClass(Q),this._isSliding=!1,p(this._element).trigger(u);h&&this.cycle()}},r._jQueryInterface=function(i){return this.each(function(){var t=p(this).data(x),e=l({},R,p(this).data());"object"==typeof i&&(e=l({},e,i));var n="string"==typeof i?i:e.slide;if(t||(t=new r(this,e),p(this).data(x,t)),"number"==typeof i)t.to(i);else if("string"==typeof n){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}else e.interval&&e.ride&&(t.pause(),t.cycle())})},r._dataApiClickHandler=function(t){var e=m.getSelectorFromElement(this);if(e){var n=p(e)[0];if(n&&p(n).hasClass(K)){var i=l({},p(n).data(),p(this).data()),o=this.getAttribute("data-slide-to");o&&(i.interval=!1),r._jQueryInterface.call(p(n),i),o&&p(n).data(x).to(o),t.preventDefault()}}},s(r,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return R}}]),r}();p(document).on(q.CLICK_DATA_API,ot,at._dataApiClickHandler),p(window).on(q.LOAD_DATA_API,function(){for(var t=[].slice.call(document.querySelectorAll(rt)),e=0,n=t.length;e<n;e++){var i=p(t[e]);at._jQueryInterface.call(i,i.data())}}),p.fn[L]=at._jQueryInterface,p.fn[L].Constructor=at,p.fn[L].noConflict=function(){return p.fn[L]=j,at._jQueryInterface};var lt="collapse",ct="bs.collapse",ht="."+ct,ut=p.fn[lt],ft={toggle:!0,parent:""},dt={toggle:"boolean",parent:"(string|element)"},pt={SHOW:"show"+ht,SHOWN:"shown"+ht,HIDE:"hide"+ht,HIDDEN:"hidden"+ht,CLICK_DATA_API:"click"+ht+".data-api"},mt="show",gt="collapse",_t="collapsing",vt="collapsed",yt="width",Et="height",bt=".show, .collapsing",wt='[data-toggle="collapse"]',Ct=function(){function a(e,t){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(t),this._triggerArray=[].slice.call(document.querySelectorAll('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var n=[].slice.call(document.querySelectorAll(wt)),i=0,o=n.length;i<o;i++){var r=n[i],s=m.getSelectorFromElement(r),a=[].slice.call(document.querySelectorAll(s)).filter(function(t){return t===e});null!==s&&0<a.length&&(this._selector=s,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var t=a.prototype;return t.toggle=function(){p(this._element).hasClass(mt)?this.hide():this.show()},t.show=function(){var t,e,n=this;if(!this._isTransitioning&&!p(this._element).hasClass(mt)&&(this._parent&&0===(t=[].slice.call(this._parent.querySelectorAll(bt)).filter(function(t){return"string"==typeof n._config.parent?t.getAttribute("data-parent")===n._config.parent:t.classList.contains(gt)})).length&&(t=null),!(t&&(e=p(t).not(this._selector).data(ct))&&e._isTransitioning))){var i=p.Event(pt.SHOW);if(p(this._element).trigger(i),!i.isDefaultPrevented()){t&&(a._jQueryInterface.call(p(t).not(this._selector),"hide"),e||p(t).data(ct,null));var o=this._getDimension();p(this._element).removeClass(gt).addClass(_t),this._element.style[o]=0,this._triggerArray.length&&p(this._triggerArray).removeClass(vt).attr("aria-expanded",!0),this.setTransitioning(!0);var r="scroll"+(o[0].toUpperCase()+o.slice(1)),s=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(){p(n._element).removeClass(_t).addClass(gt).addClass(mt),n._element.style[o]="",n.setTransitioning(!1),p(n._element).trigger(pt.SHOWN)}).emulateTransitionEnd(s),this._element.style[o]=this._element[r]+"px"}}},t.hide=function(){var t=this;if(!this._isTransitioning&&p(this._element).hasClass(mt)){var e=p.Event(pt.HIDE);if(p(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",m.reflow(this._element),p(this._element).addClass(_t).removeClass(gt).removeClass(mt);var i=this._triggerArray.length;if(0<i)for(var o=0;o<i;o++){var r=this._triggerArray[o],s=m.getSelectorFromElement(r);if(null!==s)p([].slice.call(document.querySelectorAll(s))).hasClass(mt)||p(r).addClass(vt).attr("aria-expanded",!1)}this.setTransitioning(!0);this._element.style[n]="";var a=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(){t.setTransitioning(!1),p(t._element).removeClass(_t).addClass(gt).trigger(pt.HIDDEN)}).emulateTransitionEnd(a)}}},t.setTransitioning=function(t){this._isTransitioning=t},t.dispose=function(){p.removeData(this._element,ct),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},t._getConfig=function(t){return(t=l({},ft,t)).toggle=Boolean(t.toggle),m.typeCheckConfig(lt,t,dt),t},t._getDimension=function(){return p(this._element).hasClass(yt)?yt:Et},t._getParent=function(){var t,n=this;m.isElement(this._config.parent)?(t=this._config.parent,"undefined"!=typeof this._config.parent.jquery&&(t=this._config.parent[0])):t=document.querySelector(this._config.parent);var e='[data-toggle="collapse"][data-parent="'+this._config.parent+'"]',i=[].slice.call(t.querySelectorAll(e));return p(i).each(function(t,e){n._addAriaAndCollapsedClass(a._getTargetFromElement(e),[e])}),t},t._addAriaAndCollapsedClass=function(t,e){var n=p(t).hasClass(mt);e.length&&p(e).toggleClass(vt,!n).attr("aria-expanded",n)},a._getTargetFromElement=function(t){var e=m.getSelectorFromElement(t);return e?document.querySelector(e):null},a._jQueryInterface=function(i){return this.each(function(){var t=p(this),e=t.data(ct),n=l({},ft,t.data(),"object"==typeof i&&i?i:{});if(!e&&n.toggle&&/show|hide/.test(i)&&(n.toggle=!1),e||(e=new a(this,n),t.data(ct,e)),"string"==typeof i){if("undefined"==typeof e[i])throw new TypeError('No method named "'+i+'"');e[i]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return ft}}]),a}();p(document).on(pt.CLICK_DATA_API,wt,function(t){"A"===t.currentTarget.tagName&&t.preventDefault();var n=p(this),e=m.getSelectorFromElement(this),i=[].slice.call(document.querySelectorAll(e));p(i).each(function(){var t=p(this),e=t.data(ct)?"toggle":n.data();Ct._jQueryInterface.call(t,e)})}),p.fn[lt]=Ct._jQueryInterface,p.fn[lt].Constructor=Ct,p.fn[lt].noConflict=function(){return p.fn[lt]=ut,Ct._jQueryInterface};for(var Tt="undefined"!=typeof window&&"undefined"!=typeof document,St=["Edge","Trident","Firefox"],Dt=0,It=0;It<St.length;It+=1)if(Tt&&0<=navigator.userAgent.indexOf(St[It])){Dt=1;break}var At=Tt&&window.Promise?function(t){var e=!1;return function(){e||(e=!0,window.Promise.resolve().then(function(){e=!1,t()}))}}:function(t){var e=!1;return function(){e||(e=!0,setTimeout(function(){e=!1,t()},Dt))}};function Ot(t){return t&&"[object Function]"==={}.toString.call(t)}function Nt(t,e){if(1!==t.nodeType)return[];var n=t.ownerDocument.defaultView.getComputedStyle(t,null);return e?n[e]:n}function kt(t){return"HTML"===t.nodeName?t:t.parentNode||t.host}function Lt(t){if(!t)return document.body;switch(t.nodeName){case"HTML":case"BODY":return t.ownerDocument.body;case"#document":return t.body}var e=Nt(t),n=e.overflow,i=e.overflowX,o=e.overflowY;return/(auto|scroll|overlay)/.test(n+o+i)?t:Lt(kt(t))}var xt=Tt&&!(!window.MSInputMethodContext||!document.documentMode),Pt=Tt&&/MSIE 10/.test(navigator.userAgent);function Ht(t){return 11===t?xt:10===t?Pt:xt||Pt}function jt(t){if(!t)return document.documentElement;for(var e=Ht(10)?document.body:null,n=t.offsetParent||null;n===e&&t.nextElementSibling;)n=(t=t.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&"BODY"!==i&&"HTML"!==i?-1!==["TH","TD","TABLE"].indexOf(n.nodeName)&&"static"===Nt(n,"position")?jt(n):n:t?t.ownerDocument.documentElement:document.documentElement}function Rt(t){return null!==t.parentNode?Rt(t.parentNode):t}function Ft(t,e){if(!(t&&t.nodeType&&e&&e.nodeType))return document.documentElement;var n=t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING,i=n?t:e,o=n?e:t,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var s,a,l=r.commonAncestorContainer;if(t!==l&&e!==l||i.contains(o))return"BODY"===(a=(s=l).nodeName)||"HTML"!==a&&jt(s.firstElementChild)!==s?jt(l):l;var c=Rt(t);return c.host?Ft(c.host,e):Ft(t,Rt(e).host)}function Mt(t){var e="top"===(1<arguments.length&&void 0!==arguments[1]?arguments[1]:"top")?"scrollTop":"scrollLeft",n=t.nodeName;if("BODY"!==n&&"HTML"!==n)return t[e];var i=t.ownerDocument.documentElement;return(t.ownerDocument.scrollingElement||i)[e]}function Wt(t,e){var n="x"===e?"Left":"Top",i="Left"===n?"Right":"Bottom";return parseFloat(t["border"+n+"Width"],10)+parseFloat(t["border"+i+"Width"],10)}function Ut(t,e,n,i){return Math.max(e["offset"+t],e["scroll"+t],n["client"+t],n["offset"+t],n["scroll"+t],Ht(10)?parseInt(n["offset"+t])+parseInt(i["margin"+("Height"===t?"Top":"Left")])+parseInt(i["margin"+("Height"===t?"Bottom":"Right")]):0)}function Bt(t){var e=t.body,n=t.documentElement,i=Ht(10)&&getComputedStyle(n);return{height:Ut("Height",e,n,i),width:Ut("Width",e,n,i)}}var qt=function(){function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}return function(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}}(),Kt=function(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t},Qt=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t};function Vt(t){return Qt({},t,{right:t.left+t.width,bottom:t.top+t.height})}function Yt(t){var e={};try{if(Ht(10)){e=t.getBoundingClientRect();var n=Mt(t,"top"),i=Mt(t,"left");e.top+=n,e.left+=i,e.bottom+=n,e.right+=i}else e=t.getBoundingClientRect()}catch(t){}var o={left:e.left,top:e.top,width:e.right-e.left,height:e.bottom-e.top},r="HTML"===t.nodeName?Bt(t.ownerDocument):{},s=r.width||t.clientWidth||o.right-o.left,a=r.height||t.clientHeight||o.bottom-o.top,l=t.offsetWidth-s,c=t.offsetHeight-a;if(l||c){var h=Nt(t);l-=Wt(h,"x"),c-=Wt(h,"y"),o.width-=l,o.height-=c}return Vt(o)}function zt(t,e){var n=2<arguments.length&&void 0!==arguments[2]&&arguments[2],i=Ht(10),o="HTML"===e.nodeName,r=Yt(t),s=Yt(e),a=Lt(t),l=Nt(e),c=parseFloat(l.borderTopWidth,10),h=parseFloat(l.borderLeftWidth,10);n&&o&&(s.top=Math.max(s.top,0),s.left=Math.max(s.left,0));var u=Vt({top:r.top-s.top-c,left:r.left-s.left-h,width:r.width,height:r.height});if(u.marginTop=0,u.marginLeft=0,!i&&o){var f=parseFloat(l.marginTop,10),d=parseFloat(l.marginLeft,10);u.top-=c-f,u.bottom-=c-f,u.left-=h-d,u.right-=h-d,u.marginTop=f,u.marginLeft=d}return(i&&!n?e.contains(a):e===a&&"BODY"!==a.nodeName)&&(u=function(t,e){var n=2<arguments.length&&void 0!==arguments[2]&&arguments[2],i=Mt(e,"top"),o=Mt(e,"left"),r=n?-1:1;return t.top+=i*r,t.bottom+=i*r,t.left+=o*r,t.right+=o*r,t}(u,e)),u}function Xt(t){if(!t||!t.parentElement||Ht())return document.documentElement;for(var e=t.parentElement;e&&"none"===Nt(e,"transform");)e=e.parentElement;return e||document.documentElement}function Gt(t,e,n,i){var o=4<arguments.length&&void 0!==arguments[4]&&arguments[4],r={top:0,left:0},s=o?Xt(t):Ft(t,e);if("viewport"===i)r=function(t){var e=1<arguments.length&&void 0!==arguments[1]&&arguments[1],n=t.ownerDocument.documentElement,i=zt(t,n),o=Math.max(n.clientWidth,window.innerWidth||0),r=Math.max(n.clientHeight,window.innerHeight||0),s=e?0:Mt(n),a=e?0:Mt(n,"left");return Vt({top:s-i.top+i.marginTop,left:a-i.left+i.marginLeft,width:o,height:r})}(s,o);else{var a=void 0;"scrollParent"===i?"BODY"===(a=Lt(kt(e))).nodeName&&(a=t.ownerDocument.documentElement):a="window"===i?t.ownerDocument.documentElement:i;var l=zt(a,s,o);if("HTML"!==a.nodeName||function t(e){var n=e.nodeName;if("BODY"===n||"HTML"===n)return!1;if("fixed"===Nt(e,"position"))return!0;var i=kt(e);return!!i&&t(i)}(s))r=l;else{var c=Bt(t.ownerDocument),h=c.height,u=c.width;r.top+=l.top-l.marginTop,r.bottom=h+l.top,r.left+=l.left-l.marginLeft,r.right=u+l.left}}var f="number"==typeof(n=n||0);return r.left+=f?n:n.left||0,r.top+=f?n:n.top||0,r.right-=f?n:n.right||0,r.bottom-=f?n:n.bottom||0,r}function $t(t,e,i,n,o){var r=5<arguments.length&&void 0!==arguments[5]?arguments[5]:0;if(-1===t.indexOf("auto"))return t;var s=Gt(i,n,r,o),a={top:{width:s.width,height:e.top-s.top},right:{width:s.right-e.right,height:s.height},bottom:{width:s.width,height:s.bottom-e.bottom},left:{width:e.left-s.left,height:s.height}},l=Object.keys(a).map(function(t){return Qt({key:t},a[t],{area:(e=a[t],e.width*e.height)});var e}).sort(function(t,e){return e.area-t.area}),c=l.filter(function(t){var e=t.width,n=t.height;return e>=i.clientWidth&&n>=i.clientHeight}),h=0<c.length?c[0].key:l[0].key,u=t.split("-")[1];return h+(u?"-"+u:"")}function Jt(t,e,n){var i=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return zt(n,i?Xt(e):Ft(e,n),i)}function Zt(t){var e=t.ownerDocument.defaultView.getComputedStyle(t),n=parseFloat(e.marginTop||0)+parseFloat(e.marginBottom||0),i=parseFloat(e.marginLeft||0)+parseFloat(e.marginRight||0);return{width:t.offsetWidth+i,height:t.offsetHeight+n}}function te(t){var e={left:"right",right:"left",bottom:"top",top:"bottom"};return t.replace(/left|right|bottom|top/g,function(t){return e[t]})}function ee(t,e,n){n=n.split("-")[0];var i=Zt(t),o={width:i.width,height:i.height},r=-1!==["right","left"].indexOf(n),s=r?"top":"left",a=r?"left":"top",l=r?"height":"width",c=r?"width":"height";return o[s]=e[s]+e[l]/2-i[l]/2,o[a]=n===a?e[a]-i[c]:e[te(a)],o}function ne(t,e){return Array.prototype.find?t.find(e):t.filter(e)[0]}function ie(t,n,e){return(void 0===e?t:t.slice(0,function(t,e,n){if(Array.prototype.findIndex)return t.findIndex(function(t){return t[e]===n});var i=ne(t,function(t){return t[e]===n});return t.indexOf(i)}(t,"name",e))).forEach(function(t){t.function&&console.warn("`modifier.function` is deprecated, use `modifier.fn`!");var e=t.function||t.fn;t.enabled&&Ot(e)&&(n.offsets.popper=Vt(n.offsets.popper),n.offsets.reference=Vt(n.offsets.reference),n=e(n,t))}),n}function oe(t,n){return t.some(function(t){var e=t.name;return t.enabled&&e===n})}function re(t){for(var e=[!1,"ms","Webkit","Moz","O"],n=t.charAt(0).toUpperCase()+t.slice(1),i=0;i<e.length;i++){var o=e[i],r=o?""+o+n:t;if("undefined"!=typeof document.body.style[r])return r}return null}function se(t){var e=t.ownerDocument;return e?e.defaultView:window}function ae(t,e,n,i){n.updateBound=i,se(t).addEventListener("resize",n.updateBound,{passive:!0});var o=Lt(t);return function t(e,n,i,o){var r="BODY"===e.nodeName,s=r?e.ownerDocument.defaultView:e;s.addEventListener(n,i,{passive:!0}),r||t(Lt(s.parentNode),n,i,o),o.push(s)}(o,"scroll",n.updateBound,n.scrollParents),n.scrollElement=o,n.eventsEnabled=!0,n}function le(){var t,e;this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=(t=this.reference,e=this.state,se(t).removeEventListener("resize",e.updateBound),e.scrollParents.forEach(function(t){t.removeEventListener("scroll",e.updateBound)}),e.updateBound=null,e.scrollParents=[],e.scrollElement=null,e.eventsEnabled=!1,e))}function ce(t){return""!==t&&!isNaN(parseFloat(t))&&isFinite(t)}function he(n,i){Object.keys(i).forEach(function(t){var e="";-1!==["width","height","top","right","bottom","left"].indexOf(t)&&ce(i[t])&&(e="px"),n.style[t]=i[t]+e})}var ue=Tt&&/Firefox/i.test(navigator.userAgent);function fe(t,e,n){var i=ne(t,function(t){return t.name===e}),o=!!i&&t.some(function(t){return t.name===n&&t.enabled&&t.order<i.order});if(!o){var r="`"+e+"`",s="`"+n+"`";console.warn(s+" modifier is required by "+r+" modifier in order to work, be sure to include it before "+r+"!")}return o}var de=["auto-start","auto","auto-end","top-start","top","top-end","right-start","right","right-end","bottom-end","bottom","bottom-start","left-end","left","left-start"],pe=de.slice(3);function me(t){var e=1<arguments.length&&void 0!==arguments[1]&&arguments[1],n=pe.indexOf(t),i=pe.slice(n+1).concat(pe.slice(0,n));return e?i.reverse():i}var ge="flip",_e="clockwise",ve="counterclockwise";function ye(t,o,r,e){var s=[0,0],a=-1!==["right","left"].indexOf(e),n=t.split(/(\+|\-)/).map(function(t){return t.trim()}),i=n.indexOf(ne(n,function(t){return-1!==t.search(/,|\s/)}));n[i]&&-1===n[i].indexOf(",")&&console.warn("Offsets separated by white space(s) are deprecated, use a comma (,) instead.");var l=/\s*,\s*|\s+/,c=-1!==i?[n.slice(0,i).concat([n[i].split(l)[0]]),[n[i].split(l)[1]].concat(n.slice(i+1))]:[n];return(c=c.map(function(t,e){var n=(1===e?!a:a)?"height":"width",i=!1;return t.reduce(function(t,e){return""===t[t.length-1]&&-1!==["+","-"].indexOf(e)?(t[t.length-1]=e,i=!0,t):i?(t[t.length-1]+=e,i=!1,t):t.concat(e)},[]).map(function(t){return function(t,e,n,i){var o=t.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),r=+o[1],s=o[2];if(!r)return t;if(0!==s.indexOf("%"))return"vh"!==s&&"vw"!==s?r:("vh"===s?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0))/100*r;var a=void 0;switch(s){case"%p":a=n;break;case"%":case"%r":default:a=i}return Vt(a)[e]/100*r}(t,n,o,r)})})).forEach(function(n,i){n.forEach(function(t,e){ce(t)&&(s[i]+=t*("-"===n[e-1]?-1:1))})}),s}var Ee={placement:"bottom",positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(t){var e=t.placement,n=e.split("-")[0],i=e.split("-")[1];if(i){var o=t.offsets,r=o.reference,s=o.popper,a=-1!==["bottom","top"].indexOf(n),l=a?"left":"top",c=a?"width":"height",h={start:Kt({},l,r[l]),end:Kt({},l,r[l]+r[c]-s[c])};t.offsets.popper=Qt({},s,h[i])}return t}},offset:{order:200,enabled:!0,fn:function(t,e){var n=e.offset,i=t.placement,o=t.offsets,r=o.popper,s=o.reference,a=i.split("-")[0],l=void 0;return l=ce(+n)?[+n,0]:ye(n,r,s,a),"left"===a?(r.top+=l[0],r.left-=l[1]):"right"===a?(r.top+=l[0],r.left+=l[1]):"top"===a?(r.left+=l[0],r.top-=l[1]):"bottom"===a&&(r.left+=l[0],r.top+=l[1]),t.popper=r,t},offset:0},preventOverflow:{order:300,enabled:!0,fn:function(t,i){var e=i.boundariesElement||jt(t.instance.popper);t.instance.reference===e&&(e=jt(e));var n=re("transform"),o=t.instance.popper.style,r=o.top,s=o.left,a=o[n];o.top="",o.left="",o[n]="";var l=Gt(t.instance.popper,t.instance.reference,i.padding,e,t.positionFixed);o.top=r,o.left=s,o[n]=a,i.boundaries=l;var c=i.priority,h=t.offsets.popper,u={primary:function(t){var e=h[t];return h[t]<l[t]&&!i.escapeWithReference&&(e=Math.max(h[t],l[t])),Kt({},t,e)},secondary:function(t){var e="right"===t?"left":"top",n=h[e];return h[t]>l[t]&&!i.escapeWithReference&&(n=Math.min(h[e],l[t]-("right"===t?h.width:h.height))),Kt({},e,n)}};return c.forEach(function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";h=Qt({},h,u[e](t))}),t.offsets.popper=h,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]<r(i[l])&&(t.offsets.popper[l]=r(i[l])-n[c]),n[l]>r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!fe(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",h=l?"Top":"Left",u=h.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=Zt(i)[c];a[d]-p<s[u]&&(t.offsets.popper[u]-=s[u]-(a[d]-p)),a[u]+p>s[d]&&(t.offsets.popper[u]+=a[u]+p-s[d]),t.offsets.popper=Vt(t.offsets.popper);var m=a[u]+a[c]/2-p/2,g=Nt(t.instance.popper),_=parseFloat(g["margin"+h],10),v=parseFloat(g["border"+h+"Width"],10),y=m-t.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),t.arrowElement=i,t.offsets.arrow=(Kt(n={},u,Math.round(y)),Kt(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(p,m){if(oe(p.instance.modifiers,"inner"))return p;if(p.flipped&&p.placement===p.originalPlacement)return p;var g=Gt(p.instance.popper,p.instance.reference,m.padding,m.boundariesElement,p.positionFixed),_=p.placement.split("-")[0],v=te(_),y=p.placement.split("-")[1]||"",E=[];switch(m.behavior){case ge:E=[_,v];break;case _e:E=me(_);break;case ve:E=me(_,!0);break;default:E=m.behavior}return E.forEach(function(t,e){if(_!==t||E.length===e+1)return p;_=p.placement.split("-")[0],v=te(_);var n,i=p.offsets.popper,o=p.offsets.reference,r=Math.floor,s="left"===_&&r(i.right)>r(o.left)||"right"===_&&r(i.left)<r(o.right)||"top"===_&&r(i.bottom)>r(o.top)||"bottom"===_&&r(i.top)<r(o.bottom),a=r(i.left)<r(g.left),l=r(i.right)>r(g.right),c=r(i.top)<r(g.top),h=r(i.bottom)>r(g.bottom),u="left"===_&&a||"right"===_&&l||"top"===_&&c||"bottom"===_&&h,f=-1!==["top","bottom"].indexOf(_),d=!!m.flipVariations&&(f&&"start"===y&&a||f&&"end"===y&&l||!f&&"start"===y&&c||!f&&"end"===y&&h);(s||u||d)&&(p.flipped=!0,(s||u)&&(_=E[e+1]),d&&(y="end"===(n=y)?"start":"start"===n?"end":n),p.placement=_+(y?"-"+y:""),p.offsets.popper=Qt({},p.offsets.popper,ee(p.instance.popper,p.offsets.reference,p.placement)),p=ie(p.instance.modifiers,p,"flip"))}),p},behavior:"flip",padding:5,boundariesElement:"viewport"},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=te(e),t.offsets.popper=Vt(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!fe(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=ne(t.instance.modifiers,function(t){return"preventOverflow"===t.name}).boundaries;if(e.bottom<n.top||e.left>n.right||e.top>n.bottom||e.right<n.left){if(!0===t.hide)return t;t.hide=!0,t.attributes["x-out-of-boundaries"]=""}else{if(!1===t.hide)return t;t.hide=!1,t.attributes["x-out-of-boundaries"]=!1}return t}},computeStyle:{order:850,enabled:!0,fn:function(t,e){var n=e.x,i=e.y,o=t.offsets.popper,r=ne(t.instance.modifiers,function(t){return"applyStyle"===t.name}).gpuAcceleration;void 0!==r&&console.warn("WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!");var s,a,l,c,h,u,f,d,p,m,g,_,v,y,E=void 0!==r?r:e.gpuAcceleration,b=jt(t.instance.popper),w=Yt(b),C={position:o.position},T=(s=t,a=window.devicePixelRatio<2||!ue,l=s.offsets,c=l.popper,h=l.reference,u=Math.round,f=Math.floor,d=function(t){return t},p=u(h.width),m=u(c.width),g=-1!==["left","right"].indexOf(s.placement),_=-1!==s.placement.indexOf("-"),y=a?u:d,{left:(v=a?g||_||p%2==m%2?u:f:d)(p%2==1&&m%2==1&&!_&&a?c.left-1:c.left),top:y(c.top),bottom:y(c.bottom),right:v(c.right)}),S="bottom"===n?"top":"bottom",D="right"===i?"left":"right",I=re("transform"),A=void 0,O=void 0;if(O="bottom"===S?"HTML"===b.nodeName?-b.clientHeight+T.bottom:-w.height+T.bottom:T.top,A="right"===D?"HTML"===b.nodeName?-b.clientWidth+T.right:-w.width+T.right:T.left,E&&I)C[I]="translate3d("+A+"px, "+O+"px, 0)",C[S]=0,C[D]=0,C.willChange="transform";else{var N="bottom"===S?-1:1,k="right"===D?-1:1;C[S]=O*N,C[D]=A*k,C.willChange=S+", "+D}var L={"x-placement":t.placement};return t.attributes=Qt({},L,t.attributes),t.styles=Qt({},C,t.styles),t.arrowStyles=Qt({},t.offsets.arrow,t.arrowStyles),t},gpuAcceleration:!0,x:"bottom",y:"right"},applyStyle:{order:900,enabled:!0,fn:function(t){var e,n;return he(t.instance.popper,t.styles),e=t.instance.popper,n=t.attributes,Object.keys(n).forEach(function(t){!1!==n[t]?e.setAttribute(t,n[t]):e.removeAttribute(t)}),t.arrowElement&&Object.keys(t.arrowStyles).length&&he(t.arrowElement,t.arrowStyles),t},onLoad:function(t,e,n,i,o){var r=Jt(o,e,t,n.positionFixed),s=$t(n.placement,r,e,t,n.modifiers.flip.boundariesElement,n.modifiers.flip.padding);return e.setAttribute("x-placement",s),he(e,{position:n.positionFixed?"fixed":"absolute"}),n},gpuAcceleration:void 0}}},be=function(){function r(t,e){var n=this,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,r),this.scheduleUpdate=function(){return requestAnimationFrame(n.update)},this.update=At(this.update.bind(this)),this.options=Qt({},r.Defaults,i),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=t&&t.jquery?t[0]:t,this.popper=e&&e.jquery?e[0]:e,this.options.modifiers={},Object.keys(Qt({},r.Defaults.modifiers,i.modifiers)).forEach(function(t){n.options.modifiers[t]=Qt({},r.Defaults.modifiers[t]||{},i.modifiers?i.modifiers[t]:{})}),this.modifiers=Object.keys(this.options.modifiers).map(function(t){return Qt({name:t},n.options.modifiers[t])}).sort(function(t,e){return t.order-e.order}),this.modifiers.forEach(function(t){t.enabled&&Ot(t.onLoad)&&t.onLoad(n.reference,n.popper,n.options,t,n.state)}),this.update();var o=this.options.eventsEnabled;o&&this.enableEventListeners(),this.state.eventsEnabled=o}return qt(r,[{key:"update",value:function(){return function(){if(!this.state.isDestroyed){var t={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};t.offsets.reference=Jt(this.state,this.popper,this.reference,this.options.positionFixed),t.placement=$t(this.options.placement,t.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),t.originalPlacement=t.placement,t.positionFixed=this.options.positionFixed,t.offsets.popper=ee(this.popper,t.offsets.reference,t.placement),t.offsets.popper.position=this.options.positionFixed?"fixed":"absolute",t=ie(this.modifiers,t),this.state.isCreated?this.options.onUpdate(t):(this.state.isCreated=!0,this.options.onCreate(t))}}.call(this)}},{key:"destroy",value:function(){return function(){return this.state.isDestroyed=!0,oe(this.modifiers,"applyStyle")&&(this.popper.removeAttribute("x-placement"),this.popper.style.position="",this.popper.style.top="",this.popper.style.left="",this.popper.style.right="",this.popper.style.bottom="",this.popper.style.willChange="",this.popper.style[re("transform")]=""),this.disableEventListeners(),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}.call(this)}},{key:"enableEventListeners",value:function(){return function(){this.state.eventsEnabled||(this.state=ae(this.reference,this.options,this.state,this.scheduleUpdate))}.call(this)}},{key:"disableEventListeners",value:function(){return le.call(this)}}]),r}();be.Utils=("undefined"!=typeof window?window:global).PopperUtils,be.placements=de,be.Defaults=Ee;var we="dropdown",Ce="bs.dropdown",Te="."+Ce,Se=".data-api",De=p.fn[we],Ie=new RegExp("38|40|27"),Ae={HIDE:"hide"+Te,HIDDEN:"hidden"+Te,SHOW:"show"+Te,SHOWN:"shown"+Te,CLICK:"click"+Te,CLICK_DATA_API:"click"+Te+Se,KEYDOWN_DATA_API:"keydown"+Te+Se,KEYUP_DATA_API:"keyup"+Te+Se},Oe="disabled",Ne="show",ke="dropup",Le="dropright",xe="dropleft",Pe="dropdown-menu-right",He="position-static",je='[data-toggle="dropdown"]',Re=".dropdown form",Fe=".dropdown-menu",Me=".navbar-nav",We=".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",Ue="top-start",Be="top-end",qe="bottom-start",Ke="bottom-end",Qe="right-start",Ve="left-start",Ye={offset:0,flip:!0,boundary:"scrollParent",reference:"toggle",display:"dynamic"},ze={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)",reference:"(string|element)",display:"string"},Xe=function(){function c(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var t=c.prototype;return t.toggle=function(){if(!this._element.disabled&&!p(this._element).hasClass(Oe)){var t=c._getParentFromElement(this._element),e=p(this._menu).hasClass(Ne);if(c._clearMenus(),!e){var n={relatedTarget:this._element},i=p.Event(Ae.SHOW,n);if(p(t).trigger(i),!i.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof be)throw new TypeError("Bootstrap's dropdowns require Popper.js (https://popper.js.org/)");var o=this._element;"parent"===this._config.reference?o=t:m.isElement(this._config.reference)&&(o=this._config.reference,"undefined"!=typeof this._config.reference.jquery&&(o=this._config.reference[0])),"scrollParent"!==this._config.boundary&&p(t).addClass(He),this._popper=new be(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===p(t).closest(Me).length&&p(document.body).children().on("mouseover",null,p.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),p(this._menu).toggleClass(Ne),p(t).toggleClass(Ne).trigger(p.Event(Ae.SHOWN,n))}}}},t.show=function(){if(!(this._element.disabled||p(this._element).hasClass(Oe)||p(this._menu).hasClass(Ne))){var t={relatedTarget:this._element},e=p.Event(Ae.SHOW,t),n=c._getParentFromElement(this._element);p(n).trigger(e),e.isDefaultPrevented()||(p(this._menu).toggleClass(Ne),p(n).toggleClass(Ne).trigger(p.Event(Ae.SHOWN,t)))}},t.hide=function(){if(!this._element.disabled&&!p(this._element).hasClass(Oe)&&p(this._menu).hasClass(Ne)){var t={relatedTarget:this._element},e=p.Event(Ae.HIDE,t),n=c._getParentFromElement(this._element);p(n).trigger(e),e.isDefaultPrevented()||(p(this._menu).toggleClass(Ne),p(n).toggleClass(Ne).trigger(p.Event(Ae.HIDDEN,t)))}},t.dispose=function(){p.removeData(this._element,Ce),p(this._element).off(Te),this._element=null,(this._menu=null)!==this._popper&&(this._popper.destroy(),this._popper=null)},t.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},t._addEventListeners=function(){var e=this;p(this._element).on(Ae.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},t._getConfig=function(t){return t=l({},this.constructor.Default,p(this._element).data(),t),m.typeCheckConfig(we,t,this.constructor.DefaultType),t},t._getMenuElement=function(){if(!this._menu){var t=c._getParentFromElement(this._element);t&&(this._menu=t.querySelector(Fe))}return this._menu},t._getPlacement=function(){var t=p(this._element.parentNode),e=qe;return t.hasClass(ke)?(e=Ue,p(this._menu).hasClass(Pe)&&(e=Be)):t.hasClass(Le)?e=Qe:t.hasClass(xe)?e=Ve:p(this._menu).hasClass(Pe)&&(e=Ke),e},t._detectNavbar=function(){return 0<p(this._element).closest(".navbar").length},t._getOffset=function(){var e=this,t={};return"function"==typeof this._config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e._config.offset(t.offsets,e._element)||{}),t}:t.offset=this._config.offset,t},t._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),t},c._jQueryInterface=function(e){return this.each(function(){var t=p(this).data(Ce);if(t||(t=new c(this,"object"==typeof e?e:null),p(this).data(Ce,t)),"string"==typeof e){if("undefined"==typeof t[e])throw new TypeError('No method named "'+e+'"');t[e]()}})},c._clearMenus=function(t){if(!t||3!==t.which&&("keyup"!==t.type||9===t.which))for(var e=[].slice.call(document.querySelectorAll(je)),n=0,i=e.length;n<i;n++){var o=c._getParentFromElement(e[n]),r=p(e[n]).data(Ce),s={relatedTarget:e[n]};if(t&&"click"===t.type&&(s.clickEvent=t),r){var a=r._menu;if(p(o).hasClass(Ne)&&!(t&&("click"===t.type&&/input|textarea/i.test(t.target.tagName)||"keyup"===t.type&&9===t.which)&&p.contains(o,t.target))){var l=p.Event(Ae.HIDE,s);p(o).trigger(l),l.isDefaultPrevented()||("ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),e[n].setAttribute("aria-expanded","false"),p(a).removeClass(Ne),p(o).removeClass(Ne).trigger(p.Event(Ae.HIDDEN,s)))}}}},c._getParentFromElement=function(t){var e,n=m.getSelectorFromElement(t);return n&&(e=document.querySelector(n)),e||t.parentNode},c._dataApiKeydownHandler=function(t){if((/input|textarea/i.test(t.target.tagName)?!(32===t.which||27!==t.which&&(40!==t.which&&38!==t.which||p(t.target).closest(Fe).length)):Ie.test(t.which))&&(t.preventDefault(),t.stopPropagation(),!this.disabled&&!p(this).hasClass(Oe))){var e=c._getParentFromElement(this),n=p(e).hasClass(Ne);if(n&&(!n||27!==t.which&&32!==t.which)){var i=[].slice.call(e.querySelectorAll(We));if(0!==i.length){var o=i.indexOf(t.target);38===t.which&&0<o&&o--,40===t.which&&o<i.length-1&&o++,o<0&&(o=0),i[o].focus()}}else{if(27===t.which){var r=e.querySelector(je);p(r).trigger("focus")}p(this).trigger("click")}}},s(c,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Ye}},{key:"DefaultType",get:function(){return ze}}]),c}();p(document).on(Ae.KEYDOWN_DATA_API,je,Xe._dataApiKeydownHandler).on(Ae.KEYDOWN_DATA_API,Fe,Xe._dataApiKeydownHandler).on(Ae.CLICK_DATA_API+" "+Ae.KEYUP_DATA_API,Xe._clearMenus).on(Ae.CLICK_DATA_API,je,function(t){t.preventDefault(),t.stopPropagation(),Xe._jQueryInterface.call(p(this),"toggle")}).on(Ae.CLICK_DATA_API,Re,function(t){t.stopPropagation()}),p.fn[we]=Xe._jQueryInterface,p.fn[we].Constructor=Xe,p.fn[we].noConflict=function(){return p.fn[we]=De,Xe._jQueryInterface};var Ge="modal",$e="bs.modal",Je="."+$e,Ze=p.fn[Ge],tn={backdrop:!0,keyboard:!0,focus:!0,show:!0},en={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean",show:"boolean"},nn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,FOCUSIN:"focusin"+Je,RESIZE:"resize"+Je,CLICK_DISMISS:"click.dismiss"+Je,KEYDOWN_DISMISS:"keydown.dismiss"+Je,MOUSEUP_DISMISS:"mouseup.dismiss"+Je,MOUSEDOWN_DISMISS:"mousedown.dismiss"+Je,CLICK_DATA_API:"click"+Je+".data-api"},on="modal-dialog-scrollable",rn="modal-scrollbar-measure",sn="modal-backdrop",an="modal-open",ln="fade",cn="show",hn=".modal-dialog",un=".modal-body",fn='[data-toggle="modal"]',dn='[data-dismiss="modal"]',pn=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",mn=".sticky-top",gn=function(){function o(t,e){this._config=this._getConfig(e),this._element=t,this._dialog=t.querySelector(hn),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}var t=o.prototype;return t.toggle=function(t){return this._isShown?this.hide():this.show(t)},t.show=function(t){var e=this;if(!this._isShown&&!this._isTransitioning){p(this._element).hasClass(ln)&&(this._isTransitioning=!0);var n=p.Event(nn.SHOW,{relatedTarget:t});p(this._element).trigger(n),this._isShown||n.isDefaultPrevented()||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),p(this._element).on(nn.CLICK_DISMISS,dn,function(t){return e.hide(t)}),p(this._dialog).on(nn.MOUSEDOWN_DISMISS,function(){p(e._element).one(nn.MOUSEUP_DISMISS,function(t){p(t.target).is(e._element)&&(e._ignoreBackdropClick=!0)})}),this._showBackdrop(function(){return e._showElement(t)}))}},t.hide=function(t){var e=this;if(t&&t.preventDefault(),this._isShown&&!this._isTransitioning){var n=p.Event(nn.HIDE);if(p(this._element).trigger(n),this._isShown&&!n.isDefaultPrevented()){this._isShown=!1;var i=p(this._element).hasClass(ln);if(i&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),p(document).off(nn.FOCUSIN),p(this._element).removeClass(cn),p(this._element).off(nn.CLICK_DISMISS),p(this._dialog).off(nn.MOUSEDOWN_DISMISS),i){var o=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(t){return e._hideModal(t)}).emulateTransitionEnd(o)}else this._hideModal()}}},t.dispose=function(){[window,this._element,this._dialog].forEach(function(t){return p(t).off(Je)}),p(document).off(nn.FOCUSIN),p.removeData(this._element,$e),this._config=null,this._element=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null},t.handleUpdate=function(){this._adjustDialog()},t._getConfig=function(t){return t=l({},tn,t),m.typeCheckConfig(Ge,t,en),t},t._showElement=function(t){var e=this,n=p(this._element).hasClass(ln);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),p(this._dialog).hasClass(on)?this._dialog.querySelector(un).scrollTop=0:this._element.scrollTop=0,n&&m.reflow(this._element),p(this._element).addClass(cn),this._config.focus&&this._enforceFocus();var i=p.Event(nn.SHOWN,{relatedTarget:t}),o=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,p(e._element).trigger(i)};if(n){var r=m.getTransitionDurationFromElement(this._dialog);p(this._dialog).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o()},t._enforceFocus=function(){var e=this;p(document).off(nn.FOCUSIN).on(nn.FOCUSIN,function(t){document!==t.target&&e._element!==t.target&&0===p(e._element).has(t.target).length&&e._element.focus()})},t._setEscapeEvent=function(){var e=this;this._isShown&&this._config.keyboard?p(this._element).on(nn.KEYDOWN_DISMISS,function(t){27===t.which&&(t.preventDefault(),e.hide())}):this._isShown||p(this._element).off(nn.KEYDOWN_DISMISS)},t._setResizeEvent=function(){var e=this;this._isShown?p(window).on(nn.RESIZE,function(t){return e.handleUpdate(t)}):p(window).off(nn.RESIZE)},t._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._isTransitioning=!1,this._showBackdrop(function(){p(document.body).removeClass(an),t._resetAdjustments(),t._resetScrollbar(),p(t._element).trigger(nn.HIDDEN)})},t._removeBackdrop=function(){this._backdrop&&(p(this._backdrop).remove(),this._backdrop=null)},t._showBackdrop=function(t){var e=this,n=p(this._element).hasClass(ln)?ln:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className=sn,n&&this._backdrop.classList.add(n),p(this._backdrop).appendTo(document.body),p(this._element).on(nn.CLICK_DISMISS,function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._element.focus():e.hide())}),n&&m.reflow(this._backdrop),p(this._backdrop).addClass(cn),!t)return;if(!n)return void t();var i=m.getTransitionDurationFromElement(this._backdrop);p(this._backdrop).one(m.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){p(this._backdrop).removeClass(cn);var o=function(){e._removeBackdrop(),t&&t()};if(p(this._element).hasClass(ln)){var r=m.getTransitionDurationFromElement(this._backdrop);p(this._backdrop).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o()}else t&&t()},t._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()},t._setScrollbar=function(){var o=this;if(this._isBodyOverflowing){var t=[].slice.call(document.querySelectorAll(pn)),e=[].slice.call(document.querySelectorAll(mn));p(t).each(function(t,e){var n=e.style.paddingRight,i=p(e).css("padding-right");p(e).data("padding-right",n).css("padding-right",parseFloat(i)+o._scrollbarWidth+"px")}),p(e).each(function(t,e){var n=e.style.marginRight,i=p(e).css("margin-right");p(e).data("margin-right",n).css("margin-right",parseFloat(i)-o._scrollbarWidth+"px")});var n=document.body.style.paddingRight,i=p(document.body).css("padding-right");p(document.body).data("padding-right",n).css("padding-right",parseFloat(i)+this._scrollbarWidth+"px")}p(document.body).addClass(an)},t._resetScrollbar=function(){var t=[].slice.call(document.querySelectorAll(pn));p(t).each(function(t,e){var n=p(e).data("padding-right");p(e).removeData("padding-right"),e.style.paddingRight=n||""});var e=[].slice.call(document.querySelectorAll(""+mn));p(e).each(function(t,e){var n=p(e).data("margin-right");"undefined"!=typeof n&&p(e).css("margin-right",n).removeData("margin-right")});var n=p(document.body).data("padding-right");p(document.body).removeData("padding-right"),document.body.style.paddingRight=n||""},t._getScrollbarWidth=function(){var t=document.createElement("div");t.className=rn,document.body.appendChild(t);var e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e},o._jQueryInterface=function(n,i){return this.each(function(){var t=p(this).data($e),e=l({},tn,p(this).data(),"object"==typeof n&&n?n:{});if(t||(t=new o(this,e),p(this).data($e,t)),"string"==typeof n){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n](i)}else e.show&&t.show(i)})},s(o,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return tn}}]),o}();p(document).on(nn.CLICK_DATA_API,fn,function(t){var e,n=this,i=m.getSelectorFromElement(this);i&&(e=document.querySelector(i));var o=p(e).data($e)?"toggle":l({},p(e).data(),p(this).data());"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault();var r=p(e).one(nn.SHOW,function(t){t.isDefaultPrevented()||r.one(nn.HIDDEN,function(){p(n).is(":visible")&&n.focus()})});gn._jQueryInterface.call(p(e),o,this)}),p.fn[Ge]=gn._jQueryInterface,p.fn[Ge].Constructor=gn,p.fn[Ge].noConflict=function(){return p.fn[Ge]=Ze,gn._jQueryInterface};var _n=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},yn=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,En=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function bn(t,s,e){if(0===t.length)return t;if(e&&"function"==typeof e)return e(t);for(var n=(new window.DOMParser).parseFromString(t,"text/html"),a=Object.keys(s),l=[].slice.call(n.body.querySelectorAll("*")),i=function(t,e){var n=l[t],i=n.nodeName.toLowerCase();if(-1===a.indexOf(n.nodeName.toLowerCase()))return n.parentNode.removeChild(n),"continue";var o=[].slice.call(n.attributes),r=[].concat(s["*"]||[],s[i]||[]);o.forEach(function(t){(function(t,e){var n=t.nodeName.toLowerCase();if(-1!==e.indexOf(n))return-1===_n.indexOf(n)||Boolean(t.nodeValue.match(yn)||t.nodeValue.match(En));for(var i=e.filter(function(t){return t instanceof RegExp}),o=0,r=i.length;o<r;o++)if(n.match(i[o]))return!0;return!1})(t,r)||n.removeAttribute(t.nodeName)})},o=0,r=l.length;o<r;o++)i(o);return n.body.innerHTML}var wn="tooltip",Cn="bs.tooltip",Tn="."+Cn,Sn=p.fn[wn],Dn="bs-tooltip",In=new RegExp("(^|\\s)"+Dn+"\\S+","g"),An=["sanitize","whiteList","sanitizeFn"],On={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object"},Nn={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"},kn={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:vn},Ln="show",xn="out",Pn={HIDE:"hide"+Tn,HIDDEN:"hidden"+Tn,SHOW:"show"+Tn,SHOWN:"shown"+Tn,INSERTED:"inserted"+Tn,CLICK:"click"+Tn,FOCUSIN:"focusin"+Tn,FOCUSOUT:"focusout"+Tn,MOUSEENTER:"mouseenter"+Tn,MOUSELEAVE:"mouseleave"+Tn},Hn="fade",jn="show",Rn=".tooltip-inner",Fn=".arrow",Mn="hover",Wn="focus",Un="click",Bn="manual",qn=function(){function i(t,e){if("undefined"==typeof be)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(jn))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===p(this.element).css("display"))throw new Error("Please use show on visible elements");var t=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(t);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&p(o).addClass(Hn);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new be(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Fn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),p(o).addClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().on("mouseover",null,p.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,p(e.element).trigger(e.constructor.Event.SHOWN),t===xn&&e._leave(null,e)};if(p(this.tip).hasClass(Hn)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=p.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Ln&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p(this.element).trigger(i),!i.isDefaultPrevented()){if(p(n).removeClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),this._activeTrigger[Un]=!1,this._activeTrigger[Wn]=!1,this._activeTrigger[Mn]=!1,p(this.tip).hasClass(Hn)){var r=m.getTransitionDurationFromElement(n);p(n).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){p(this.getTipElement()).addClass(Dn+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(p(t.querySelectorAll(Rn)),this.getTitle()),p(t).removeClass(Hn+" "+jn)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=bn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p(e).parent().is(t)||t.empty().append(e):t.text(p(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},t._getAttachment=function(t){return Nn[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Bn){var e=t===Mn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Mn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),p(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Wn:Mn]=!0),p(e.getTipElement()).hasClass(jn)||e._hoverState===Ln?e._hoverState=Ln:(clearTimeout(e._timeout),e._hoverState=Ln,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ln&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Wn:Mn]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=xn,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===xn&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=p(this.element).data();return Object.keys(e).forEach(function(t){-1!==An.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),m.typeCheckConfig(wn,t,this.constructor.DefaultType),t.sanitize&&(t.template=bn(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(In);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p(t).removeClass(Hn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(Cn),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(Cn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return kn}},{key:"NAME",get:function(){return wn}},{key:"DATA_KEY",get:function(){return Cn}},{key:"Event",get:function(){return Pn}},{key:"EVENT_KEY",get:function(){return Tn}},{key:"DefaultType",get:function(){return On}}]),i}();p.fn[wn]=qn._jQueryInterface,p.fn[wn].Constructor=qn,p.fn[wn].noConflict=function(){return p.fn[wn]=Sn,qn._jQueryInterface};var Kn="popover",Qn="bs.popover",Vn="."+Qn,Yn=p.fn[Kn],zn="bs-popover",Xn=new RegExp("(^|\\s)"+zn+"\\S+","g"),Gn=l({},qn.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),$n=l({},qn.DefaultType,{content:"(string|element|function)"}),Jn="fade",Zn="show",ti=".popover-header",ei=".popover-body",ni={HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn,INSERTED:"inserted"+Vn,CLICK:"click"+Vn,FOCUSIN:"focusin"+Vn,FOCUSOUT:"focusout"+Vn,MOUSEENTER:"mouseenter"+Vn,MOUSELEAVE:"mouseleave"+Vn},ii=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){p(this.getTipElement()).addClass(zn+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},o.setContent=function(){var t=p(this.getTipElement());this.setElementContent(t.find(ti),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ei),e),t.removeClass(Jn+" "+Zn)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(Xn);null!==e&&0<e.length&&t.removeClass(e.join(""))},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(Qn),e="object"==typeof n?n:null;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(Qn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Gn}},{key:"NAME",get:function(){return Kn}},{key:"DATA_KEY",get:function(){return Qn}},{key:"Event",get:function(){return ni}},{key:"EVENT_KEY",get:function(){return Vn}},{key:"DefaultType",get:function(){return $n}}]),i}(qn);p.fn[Kn]=ii._jQueryInterface,p.fn[Kn].Constructor=ii,p.fn[Kn].noConflict=function(){return p.fn[Kn]=Yn,ii._jQueryInterface};var oi="scrollspy",ri="bs.scrollspy",si="."+ri,ai=p.fn[oi],li={offset:10,method:"auto",target:""},ci={offset:"number",method:"string",target:"(string|element)"},hi={ACTIVATE:"activate"+si,SCROLL:"scroll"+si,LOAD_DATA_API:"load"+si+".data-api"},ui="dropdown-item",fi="active",di='[data-spy="scroll"]',pi=".nav, .list-group",mi=".nav-link",gi=".nav-item",_i=".list-group-item",vi=".dropdown",yi=".dropdown-item",Ei=".dropdown-toggle",bi="offset",wi="position",Ci=function(){function n(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" "+mi+","+this._config.target+" "+_i+","+this._config.target+" "+yi,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,p(this._scrollElement).on(hi.SCROLL,function(t){return n._process(t)}),this.refresh(),this._process()}var t=n.prototype;return t.refresh=function(){var e=this,t=this._scrollElement===this._scrollElement.window?bi:wi,o="auto"===this._config.method?t:this._config.method,r=o===wi?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map(function(t){var e,n=m.getSelectorFromElement(t);if(n&&(e=document.querySelector(n)),e){var i=e.getBoundingClientRect();if(i.width||i.height)return[p(e)[o]().top+r,n]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},t.dispose=function(){p.removeData(this._element,ri),p(this._scrollElement).off(si),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},t._getConfig=function(t){if("string"!=typeof(t=l({},li,"object"==typeof t&&t?t:{})).target){var e=p(t.target).attr("id");e||(e=m.getUID(oi),p(t.target).attr("id",e)),t.target="#"+e}return m.typeCheckConfig(oi,t,ci),t},t._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},t._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},t._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},t._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),n<=t){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t<this._offsets[0]&&0<this._offsets[0])return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t<this._offsets[o+1])&&this._activate(this._targets[o])}}},t._activate=function(e){this._activeTarget=e,this._clear();var t=this._selector.split(",").map(function(t){return t+'[data-target="'+e+'"],'+t+'[href="'+e+'"]'}),n=p([].slice.call(document.querySelectorAll(t.join(","))));n.hasClass(ui)?(n.closest(vi).find(Ei).addClass(fi),n.addClass(fi)):(n.addClass(fi),n.parents(pi).prev(mi+", "+_i).addClass(fi),n.parents(pi).prev(gi).children(mi).addClass(fi)),p(this._scrollElement).trigger(hi.ACTIVATE,{relatedTarget:e})},t._clear=function(){[].slice.call(document.querySelectorAll(this._selector)).filter(function(t){return t.classList.contains(fi)}).forEach(function(t){return t.classList.remove(fi)})},n._jQueryInterface=function(e){return this.each(function(){var t=p(this).data(ri);if(t||(t=new n(this,"object"==typeof e&&e),p(this).data(ri,t)),"string"==typeof e){if("undefined"==typeof t[e])throw new TypeError('No method named "'+e+'"');t[e]()}})},s(n,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return li}}]),n}();p(window).on(hi.LOAD_DATA_API,function(){for(var t=[].slice.call(document.querySelectorAll(di)),e=t.length;e--;){var n=p(t[e]);Ci._jQueryInterface.call(n,n.data())}}),p.fn[oi]=Ci._jQueryInterface,p.fn[oi].Constructor=Ci,p.fn[oi].noConflict=function(){return p.fn[oi]=ai,Ci._jQueryInterface};var Ti="bs.tab",Si="."+Ti,Di=p.fn.tab,Ii={HIDE:"hide"+Si,HIDDEN:"hidden"+Si,SHOW:"show"+Si,SHOWN:"shown"+Si,CLICK_DATA_API:"click"+Si+".data-api"},Ai="dropdown-menu",Oi="active",Ni="disabled",ki="fade",Li="show",xi=".dropdown",Pi=".nav, .list-group",Hi=".active",ji="> li > .active",Ri='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',Fi=".dropdown-toggle",Mi="> .dropdown-menu .active",Wi=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&p(this._element).hasClass(Oi)||p(this._element).hasClass(Ni))){var t,i,e=p(this._element).closest(Pi)[0],o=m.getSelectorFromElement(this._element);if(e){var r="UL"===e.nodeName||"OL"===e.nodeName?ji:Hi;i=(i=p.makeArray(p(e).find(r)))[i.length-1]}var s=p.Event(Ii.HIDE,{relatedTarget:this._element}),a=p.Event(Ii.SHOW,{relatedTarget:i});if(i&&p(i).trigger(s),p(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){o&&(t=document.querySelector(o)),this._activate(this._element,e);var l=function(){var t=p.Event(Ii.HIDDEN,{relatedTarget:n._element}),e=p.Event(Ii.SHOWN,{relatedTarget:i});p(i).trigger(t),p(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){p.removeData(this._element,Ti),this._element=null},t._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?p(e).children(Hi):p(e).find(ji))[0],r=n&&o&&p(o).hasClass(ki),s=function(){return i._transitionComplete(t,o,n)};if(o&&r){var a=m.getTransitionDurationFromElement(o);p(o).removeClass(Li).one(m.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){p(e).removeClass(Oi);var i=p(e.parentNode).find(Mi)[0];i&&p(i).removeClass(Oi),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(p(t).addClass(Oi),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),m.reflow(t),t.classList.contains(ki)&&t.classList.add(Li),t.parentNode&&p(t.parentNode).hasClass(Ai)){var o=p(t).closest(xi)[0];if(o){var r=[].slice.call(o.querySelectorAll(Fi));p(r).addClass(Oi)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=p(this),e=t.data(Ti);if(e||(e=new i(this),t.data(Ti,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),i}();p(document).on(Ii.CLICK_DATA_API,Ri,function(t){t.preventDefault(),Wi._jQueryInterface.call(p(this),"show")}),p.fn.tab=Wi._jQueryInterface,p.fn.tab.Constructor=Wi,p.fn.tab.noConflict=function(){return p.fn.tab=Di,Wi._jQueryInterface};var Ui="toast",Bi="bs.toast",qi="."+Bi,Ki=p.fn[Ui],Qi={CLICK_DISMISS:"click.dismiss"+qi,HIDE:"hide"+qi,HIDDEN:"hidden"+qi,SHOW:"show"+qi,SHOWN:"shown"+qi},Vi="fade",Yi="hide",zi="show",Xi="showing",Gi={animation:"boolean",autohide:"boolean",delay:"number"},$i={animation:!0,autohide:!0,delay:500},Ji='[data-dismiss="toast"]',Zi=function(){function i(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var t=i.prototype;return t.show=function(){var t=this;p(this._element).trigger(Qi.SHOW),this._config.animation&&this._element.classList.add(Vi);var e=function(){t._element.classList.remove(Xi),t._element.classList.add(zi),p(t._element).trigger(Qi.SHOWN),t._config.autohide&&t.hide()};if(this._element.classList.remove(Yi),this._element.classList.add(Xi),this._config.animation){var n=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},t.hide=function(t){var e=this;this._element.classList.contains(zi)&&(p(this._element).trigger(Qi.HIDE),t?this._close():this._timeout=setTimeout(function(){e._close()},this._config.delay))},t.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains(zi)&&this._element.classList.remove(zi),p(this._element).off(Qi.CLICK_DISMISS),p.removeData(this._element,Bi),this._element=null,this._config=null},t._getConfig=function(t){return t=l({},$i,p(this._element).data(),"object"==typeof t&&t?t:{}),m.typeCheckConfig(Ui,t,this.constructor.DefaultType),t},t._setListeners=function(){var t=this;p(this._element).on(Qi.CLICK_DISMISS,Ji,function(){return t.hide(!0)})},t._close=function(){var t=this,e=function(){t._element.classList.add(Yi),p(t._element).trigger(Qi.HIDDEN)};if(this._element.classList.remove(zi),this._config.animation){var n=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},i._jQueryInterface=function(n){return this.each(function(){var t=p(this),e=t.data(Bi);if(e||(e=new i(this,"object"==typeof n&&n),t.data(Bi,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n](this)}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"DefaultType",get:function(){return Gi}},{key:"Default",get:function(){return $i}}]),i}();p.fn[Ui]=Zi._jQueryInterface,p.fn[Ui].Constructor=Zi,p.fn[Ui].noConflict=function(){return p.fn[Ui]=Ki,Zi._jQueryInterface},function(){if("undefined"==typeof p)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=p.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||4<=t[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),t.Util=m,t.Alert=g,t.Button=k,t.Carousel=at,t.Collapse=Ct,t.Dropdown=Xe,t.Modal=gn,t.Popover=ii,t.Scrollspy=Ci,t.Tab=Wi,t.Toast=Zi,t.Tooltip=qn,Object.defineProperty(t,"__esModule",{value:!0})}); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.css b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.css new file mode 100644 index 000000000..92e3fe871 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js new file mode 100644 index 000000000..c4c0d1f95 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function s(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}function l(o){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{},e=Object.keys(r);"function"==typeof Object.getOwnPropertySymbols&&(e=e.concat(Object.getOwnPropertySymbols(r).filter(function(t){return Object.getOwnPropertyDescriptor(r,t).enumerable}))),e.forEach(function(t){var e,n,i;e=o,i=r[n=t],n in e?Object.defineProperty(e,n,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[n]=i})}return o}g=g&&g.hasOwnProperty("default")?g.default:g,u=u&&u.hasOwnProperty("default")?u.default:u;var e="transitionend";function n(t){var e=this,n=!1;return g(this).one(_.TRANSITION_END,function(){n=!0}),setTimeout(function(){n||_.triggerTransitionEnd(e)},t),this}var _={TRANSITION_END:"bsTransitionEnd",getUID:function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(t){if(!t)return 0;var e=g(t).css("transition-duration"),n=g(t).css("transition-delay"),i=parseFloat(e),o=parseFloat(n);return i||o?(e=e.split(",")[0],n=n.split(",")[0],1e3*(parseFloat(e)+parseFloat(n))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){g(t).trigger(e)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)){var o=n[i],r=e[i],s=r&&_.isElement(r)?"element":(a=r,{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase());if(!new RegExp(o).test(s))throw new Error(t.toUpperCase()+': Option "'+i+'" provided type "'+s+'" but expected type "'+o+'".')}var a},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"!=typeof t.getRootNode)return t instanceof ShadowRoot?t:t.parentNode?_.findShadowRoot(t.parentNode):null;var e=t.getRootNode();return e instanceof ShadowRoot?e:null}};g.fn.emulateTransitionEnd=n,g.event.special[_.TRANSITION_END]={bindType:e,delegateType:e,handle:function(t){if(g(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var o="alert",r="bs.alert",a="."+r,c=g.fn[o],h={CLOSE:"close"+a,CLOSED:"closed"+a,CLICK_DATA_API:"click"+a+".data-api"},f="alert",d="fade",m="show",p=function(){function i(t){this._element=t}var t=i.prototype;return t.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},t.dispose=function(){g.removeData(this._element,r),this._element=null},t._getRootElement=function(t){var e=_.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=g(t).closest("."+f)[0]),n},t._triggerCloseEvent=function(t){var e=g.Event(h.CLOSE);return g(t).trigger(e),e},t._removeElement=function(e){var n=this;if(g(e).removeClass(m),g(e).hasClass(d)){var t=_.getTransitionDurationFromElement(e);g(e).one(_.TRANSITION_END,function(t){return n._destroyElement(e,t)}).emulateTransitionEnd(t)}else this._destroyElement(e)},t._destroyElement=function(t){g(t).detach().trigger(h.CLOSED).remove()},i._jQueryInterface=function(n){return this.each(function(){var t=g(this),e=t.data(r);e||(e=new i(this),t.data(r,e)),"close"===n&&e[n](this)})},i._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),i}();g(document).on(h.CLICK_DATA_API,'[data-dismiss="alert"]',p._handleDismiss(new p)),g.fn[o]=p._jQueryInterface,g.fn[o].Constructor=p,g.fn[o].noConflict=function(){return g.fn[o]=c,p._jQueryInterface};var v="button",y="bs.button",E="."+y,C=".data-api",T=g.fn[v],S="active",b="btn",I="focus",D='[data-toggle^="button"]',w='[data-toggle="buttons"]',A='input:not([type="hidden"])',N=".active",O=".btn",k={CLICK_DATA_API:"click"+E+C,FOCUS_BLUR_DATA_API:"focus"+E+C+" blur"+E+C},P=function(){function n(t){this._element=t}var t=n.prototype;return t.toggle=function(){var t=!0,e=!0,n=g(this._element).closest(w)[0];if(n){var i=this._element.querySelector(A);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(S))t=!1;else{var o=n.querySelector(N);o&&g(o).removeClass(S)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!this._element.classList.contains(S),g(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(S)),t&&g(this._element).toggleClass(S)},t.dispose=function(){g.removeData(this._element,y),this._element=null},n._jQueryInterface=function(e){return this.each(function(){var t=g(this).data(y);t||(t=new n(this),g(this).data(y,t)),"toggle"===e&&t[e]()})},s(n,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),n}();g(document).on(k.CLICK_DATA_API,D,function(t){t.preventDefault();var e=t.target;g(e).hasClass(b)||(e=g(e).closest(O)),P._jQueryInterface.call(g(e),"toggle")}).on(k.FOCUS_BLUR_DATA_API,D,function(t){var e=g(t.target).closest(O)[0];g(e).toggleClass(I,/^focus(in)?$/.test(t.type))}),g.fn[v]=P._jQueryInterface,g.fn[v].Constructor=P,g.fn[v].noConflict=function(){return g.fn[v]=T,P._jQueryInterface};var L="carousel",j="bs.carousel",H="."+j,R=".data-api",x=g.fn[L],F={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},U={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},W="next",q="prev",M="left",K="right",Q={SLIDE:"slide"+H,SLID:"slid"+H,KEYDOWN:"keydown"+H,MOUSEENTER:"mouseenter"+H,MOUSELEAVE:"mouseleave"+H,TOUCHSTART:"touchstart"+H,TOUCHMOVE:"touchmove"+H,TOUCHEND:"touchend"+H,POINTERDOWN:"pointerdown"+H,POINTERUP:"pointerup"+H,DRAG_START:"dragstart"+H,LOAD_DATA_API:"load"+H+R,CLICK_DATA_API:"click"+H+R},B="carousel",V="active",Y="slide",z="carousel-item-right",X="carousel-item-left",$="carousel-item-next",G="carousel-item-prev",J="pointer-event",Z=".active",tt=".active.carousel-item",et=".carousel-item",nt=".carousel-item img",it=".carousel-item-next, .carousel-item-prev",ot=".carousel-indicators",rt="[data-slide], [data-slide-to]",st='[data-ride="carousel"]',at={TOUCH:"touch",PEN:"pen"},lt=function(){function r(t,e){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._element=t,this._indicatorsElement=this._element.querySelector(ot),this._touchSupported="ontouchstart"in document.documentElement||0<navigator.maxTouchPoints,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var t=r.prototype;return t.next=function(){this._isSliding||this._slide(W)},t.nextWhenVisible=function(){!document.hidden&&g(this._element).is(":visible")&&"hidden"!==g(this._element).css("visibility")&&this.next()},t.prev=function(){this._isSliding||this._slide(q)},t.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(it)&&(_.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},t.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},t.to=function(t){var e=this;this._activeElement=this._element.querySelector(tt);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n<t?W:q;this._slide(i,this._items[t])}},t.dispose=function(){g(this._element).off(H),g.removeData(this._element,j),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},t._getConfig=function(t){return t=l({},F,t),_.typeCheckConfig(L,t,U),t},t._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;0<e&&this.prev(),e<0&&this.next()}},t._addEventListeners=function(){var e=this;this._config.keyboard&&g(this._element).on(Q.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&g(this._element).on(Q.MOUSEENTER,function(t){return e.pause(t)}).on(Q.MOUSELEAVE,function(t){return e.cycle(t)}),this._config.touch&&this._addTouchEventListeners()},t._addTouchEventListeners=function(){var n=this;if(this._touchSupported){var e=function(t){n._pointerEvent&&at[t.originalEvent.pointerType.toUpperCase()]?n.touchStartX=t.originalEvent.clientX:n._pointerEvent||(n.touchStartX=t.originalEvent.touches[0].clientX)},i=function(t){n._pointerEvent&&at[t.originalEvent.pointerType.toUpperCase()]&&(n.touchDeltaX=t.originalEvent.clientX-n.touchStartX),n._handleSwipe(),"hover"===n._config.pause&&(n.pause(),n.touchTimeout&&clearTimeout(n.touchTimeout),n.touchTimeout=setTimeout(function(t){return n.cycle(t)},500+n._config.interval))};g(this._element.querySelectorAll(nt)).on(Q.DRAG_START,function(t){return t.preventDefault()}),this._pointerEvent?(g(this._element).on(Q.POINTERDOWN,function(t){return e(t)}),g(this._element).on(Q.POINTERUP,function(t){return i(t)}),this._element.classList.add(J)):(g(this._element).on(Q.TOUCHSTART,function(t){return e(t)}),g(this._element).on(Q.TOUCHMOVE,function(t){var e;(e=t).originalEvent.touches&&1<e.originalEvent.touches.length?n.touchDeltaX=0:n.touchDeltaX=e.originalEvent.touches[0].clientX-n.touchStartX}),g(this._element).on(Q.TOUCHEND,function(t){return i(t)}))}},t._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},t._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(et)):[],this._items.indexOf(t)},t._getItemByDirection=function(t,e){var n=t===W,i=t===q,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+(t===q?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},t._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(tt)),o=g.Event(Q.SLIDE,{relatedTarget:t,direction:e,from:i,to:n});return g(this._element).trigger(o),o},t._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(Z));g(e).removeClass(V);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&g(n).addClass(V)}},t._slide=function(t,e){var n,i,o,r=this,s=this._element.querySelector(tt),a=this._getItemIndex(s),l=e||s&&this._getItemByDirection(t,s),c=this._getItemIndex(l),h=Boolean(this._interval);if(o=t===W?(n=X,i=$,M):(n=z,i=G,K),l&&g(l).hasClass(V))this._isSliding=!1;else if(!this._triggerSlideEvent(l,o).isDefaultPrevented()&&s&&l){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(l);var u=g.Event(Q.SLID,{relatedTarget:l,direction:o,from:a,to:c});if(g(this._element).hasClass(Y)){g(l).addClass(i),_.reflow(l),g(s).addClass(n),g(l).addClass(n);var f=parseInt(l.getAttribute("data-interval"),10);this._config.interval=f?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,f):this._config.defaultInterval||this._config.interval;var d=_.getTransitionDurationFromElement(s);g(s).one(_.TRANSITION_END,function(){g(l).removeClass(n+" "+i).addClass(V),g(s).removeClass(V+" "+i+" "+n),r._isSliding=!1,setTimeout(function(){return g(r._element).trigger(u)},0)}).emulateTransitionEnd(d)}else g(s).removeClass(V),g(l).addClass(V),this._isSliding=!1,g(this._element).trigger(u);h&&this.cycle()}},r._jQueryInterface=function(i){return this.each(function(){var t=g(this).data(j),e=l({},F,g(this).data());"object"==typeof i&&(e=l({},e,i));var n="string"==typeof i?i:e.slide;if(t||(t=new r(this,e),g(this).data(j,t)),"number"==typeof i)t.to(i);else if("string"==typeof n){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}else e.interval&&e.ride&&(t.pause(),t.cycle())})},r._dataApiClickHandler=function(t){var e=_.getSelectorFromElement(this);if(e){var n=g(e)[0];if(n&&g(n).hasClass(B)){var i=l({},g(n).data(),g(this).data()),o=this.getAttribute("data-slide-to");o&&(i.interval=!1),r._jQueryInterface.call(g(n),i),o&&g(n).data(j).to(o),t.preventDefault()}}},s(r,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return F}}]),r}();g(document).on(Q.CLICK_DATA_API,rt,lt._dataApiClickHandler),g(window).on(Q.LOAD_DATA_API,function(){for(var t=[].slice.call(document.querySelectorAll(st)),e=0,n=t.length;e<n;e++){var i=g(t[e]);lt._jQueryInterface.call(i,i.data())}}),g.fn[L]=lt._jQueryInterface,g.fn[L].Constructor=lt,g.fn[L].noConflict=function(){return g.fn[L]=x,lt._jQueryInterface};var ct="collapse",ht="bs.collapse",ut="."+ht,ft=g.fn[ct],dt={toggle:!0,parent:""},gt={toggle:"boolean",parent:"(string|element)"},_t={SHOW:"show"+ut,SHOWN:"shown"+ut,HIDE:"hide"+ut,HIDDEN:"hidden"+ut,CLICK_DATA_API:"click"+ut+".data-api"},mt="show",pt="collapse",vt="collapsing",yt="collapsed",Et="width",Ct="height",Tt=".show, .collapsing",St='[data-toggle="collapse"]',bt=function(){function a(e,t){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(t),this._triggerArray=[].slice.call(document.querySelectorAll('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var n=[].slice.call(document.querySelectorAll(St)),i=0,o=n.length;i<o;i++){var r=n[i],s=_.getSelectorFromElement(r),a=[].slice.call(document.querySelectorAll(s)).filter(function(t){return t===e});null!==s&&0<a.length&&(this._selector=s,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var t=a.prototype;return t.toggle=function(){g(this._element).hasClass(mt)?this.hide():this.show()},t.show=function(){var t,e,n=this;if(!this._isTransitioning&&!g(this._element).hasClass(mt)&&(this._parent&&0===(t=[].slice.call(this._parent.querySelectorAll(Tt)).filter(function(t){return"string"==typeof n._config.parent?t.getAttribute("data-parent")===n._config.parent:t.classList.contains(pt)})).length&&(t=null),!(t&&(e=g(t).not(this._selector).data(ht))&&e._isTransitioning))){var i=g.Event(_t.SHOW);if(g(this._element).trigger(i),!i.isDefaultPrevented()){t&&(a._jQueryInterface.call(g(t).not(this._selector),"hide"),e||g(t).data(ht,null));var o=this._getDimension();g(this._element).removeClass(pt).addClass(vt),this._element.style[o]=0,this._triggerArray.length&&g(this._triggerArray).removeClass(yt).attr("aria-expanded",!0),this.setTransitioning(!0);var r="scroll"+(o[0].toUpperCase()+o.slice(1)),s=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,function(){g(n._element).removeClass(vt).addClass(pt).addClass(mt),n._element.style[o]="",n.setTransitioning(!1),g(n._element).trigger(_t.SHOWN)}).emulateTransitionEnd(s),this._element.style[o]=this._element[r]+"px"}}},t.hide=function(){var t=this;if(!this._isTransitioning&&g(this._element).hasClass(mt)){var e=g.Event(_t.HIDE);if(g(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",_.reflow(this._element),g(this._element).addClass(vt).removeClass(pt).removeClass(mt);var i=this._triggerArray.length;if(0<i)for(var o=0;o<i;o++){var r=this._triggerArray[o],s=_.getSelectorFromElement(r);if(null!==s)g([].slice.call(document.querySelectorAll(s))).hasClass(mt)||g(r).addClass(yt).attr("aria-expanded",!1)}this.setTransitioning(!0);this._element.style[n]="";var a=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,function(){t.setTransitioning(!1),g(t._element).removeClass(vt).addClass(pt).trigger(_t.HIDDEN)}).emulateTransitionEnd(a)}}},t.setTransitioning=function(t){this._isTransitioning=t},t.dispose=function(){g.removeData(this._element,ht),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},t._getConfig=function(t){return(t=l({},dt,t)).toggle=Boolean(t.toggle),_.typeCheckConfig(ct,t,gt),t},t._getDimension=function(){return g(this._element).hasClass(Et)?Et:Ct},t._getParent=function(){var t,n=this;_.isElement(this._config.parent)?(t=this._config.parent,"undefined"!=typeof this._config.parent.jquery&&(t=this._config.parent[0])):t=document.querySelector(this._config.parent);var e='[data-toggle="collapse"][data-parent="'+this._config.parent+'"]',i=[].slice.call(t.querySelectorAll(e));return g(i).each(function(t,e){n._addAriaAndCollapsedClass(a._getTargetFromElement(e),[e])}),t},t._addAriaAndCollapsedClass=function(t,e){var n=g(t).hasClass(mt);e.length&&g(e).toggleClass(yt,!n).attr("aria-expanded",n)},a._getTargetFromElement=function(t){var e=_.getSelectorFromElement(t);return e?document.querySelector(e):null},a._jQueryInterface=function(i){return this.each(function(){var t=g(this),e=t.data(ht),n=l({},dt,t.data(),"object"==typeof i&&i?i:{});if(!e&&n.toggle&&/show|hide/.test(i)&&(n.toggle=!1),e||(e=new a(this,n),t.data(ht,e)),"string"==typeof i){if("undefined"==typeof e[i])throw new TypeError('No method named "'+i+'"');e[i]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return dt}}]),a}();g(document).on(_t.CLICK_DATA_API,St,function(t){"A"===t.currentTarget.tagName&&t.preventDefault();var n=g(this),e=_.getSelectorFromElement(this),i=[].slice.call(document.querySelectorAll(e));g(i).each(function(){var t=g(this),e=t.data(ht)?"toggle":n.data();bt._jQueryInterface.call(t,e)})}),g.fn[ct]=bt._jQueryInterface,g.fn[ct].Constructor=bt,g.fn[ct].noConflict=function(){return g.fn[ct]=ft,bt._jQueryInterface};var It="dropdown",Dt="bs.dropdown",wt="."+Dt,At=".data-api",Nt=g.fn[It],Ot=new RegExp("38|40|27"),kt={HIDE:"hide"+wt,HIDDEN:"hidden"+wt,SHOW:"show"+wt,SHOWN:"shown"+wt,CLICK:"click"+wt,CLICK_DATA_API:"click"+wt+At,KEYDOWN_DATA_API:"keydown"+wt+At,KEYUP_DATA_API:"keyup"+wt+At},Pt="disabled",Lt="show",jt="dropup",Ht="dropright",Rt="dropleft",xt="dropdown-menu-right",Ft="position-static",Ut='[data-toggle="dropdown"]',Wt=".dropdown form",qt=".dropdown-menu",Mt=".navbar-nav",Kt=".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",Qt="top-start",Bt="top-end",Vt="bottom-start",Yt="bottom-end",zt="right-start",Xt="left-start",$t={offset:0,flip:!0,boundary:"scrollParent",reference:"toggle",display:"dynamic"},Gt={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)",reference:"(string|element)",display:"string"},Jt=function(){function c(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var t=c.prototype;return t.toggle=function(){if(!this._element.disabled&&!g(this._element).hasClass(Pt)){var t=c._getParentFromElement(this._element),e=g(this._menu).hasClass(Lt);if(c._clearMenus(),!e){var n={relatedTarget:this._element},i=g.Event(kt.SHOW,n);if(g(t).trigger(i),!i.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof u)throw new TypeError("Bootstrap's dropdowns require Popper.js (https://popper.js.org/)");var o=this._element;"parent"===this._config.reference?o=t:_.isElement(this._config.reference)&&(o=this._config.reference,"undefined"!=typeof this._config.reference.jquery&&(o=this._config.reference[0])),"scrollParent"!==this._config.boundary&&g(t).addClass(Ft),this._popper=new u(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===g(t).closest(Mt).length&&g(document.body).children().on("mouseover",null,g.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),g(this._menu).toggleClass(Lt),g(t).toggleClass(Lt).trigger(g.Event(kt.SHOWN,n))}}}},t.show=function(){if(!(this._element.disabled||g(this._element).hasClass(Pt)||g(this._menu).hasClass(Lt))){var t={relatedTarget:this._element},e=g.Event(kt.SHOW,t),n=c._getParentFromElement(this._element);g(n).trigger(e),e.isDefaultPrevented()||(g(this._menu).toggleClass(Lt),g(n).toggleClass(Lt).trigger(g.Event(kt.SHOWN,t)))}},t.hide=function(){if(!this._element.disabled&&!g(this._element).hasClass(Pt)&&g(this._menu).hasClass(Lt)){var t={relatedTarget:this._element},e=g.Event(kt.HIDE,t),n=c._getParentFromElement(this._element);g(n).trigger(e),e.isDefaultPrevented()||(g(this._menu).toggleClass(Lt),g(n).toggleClass(Lt).trigger(g.Event(kt.HIDDEN,t)))}},t.dispose=function(){g.removeData(this._element,Dt),g(this._element).off(wt),this._element=null,(this._menu=null)!==this._popper&&(this._popper.destroy(),this._popper=null)},t.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},t._addEventListeners=function(){var e=this;g(this._element).on(kt.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},t._getConfig=function(t){return t=l({},this.constructor.Default,g(this._element).data(),t),_.typeCheckConfig(It,t,this.constructor.DefaultType),t},t._getMenuElement=function(){if(!this._menu){var t=c._getParentFromElement(this._element);t&&(this._menu=t.querySelector(qt))}return this._menu},t._getPlacement=function(){var t=g(this._element.parentNode),e=Vt;return t.hasClass(jt)?(e=Qt,g(this._menu).hasClass(xt)&&(e=Bt)):t.hasClass(Ht)?e=zt:t.hasClass(Rt)?e=Xt:g(this._menu).hasClass(xt)&&(e=Yt),e},t._detectNavbar=function(){return 0<g(this._element).closest(".navbar").length},t._getOffset=function(){var e=this,t={};return"function"==typeof this._config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e._config.offset(t.offsets,e._element)||{}),t}:t.offset=this._config.offset,t},t._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),t},c._jQueryInterface=function(e){return this.each(function(){var t=g(this).data(Dt);if(t||(t=new c(this,"object"==typeof e?e:null),g(this).data(Dt,t)),"string"==typeof e){if("undefined"==typeof t[e])throw new TypeError('No method named "'+e+'"');t[e]()}})},c._clearMenus=function(t){if(!t||3!==t.which&&("keyup"!==t.type||9===t.which))for(var e=[].slice.call(document.querySelectorAll(Ut)),n=0,i=e.length;n<i;n++){var o=c._getParentFromElement(e[n]),r=g(e[n]).data(Dt),s={relatedTarget:e[n]};if(t&&"click"===t.type&&(s.clickEvent=t),r){var a=r._menu;if(g(o).hasClass(Lt)&&!(t&&("click"===t.type&&/input|textarea/i.test(t.target.tagName)||"keyup"===t.type&&9===t.which)&&g.contains(o,t.target))){var l=g.Event(kt.HIDE,s);g(o).trigger(l),l.isDefaultPrevented()||("ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),e[n].setAttribute("aria-expanded","false"),g(a).removeClass(Lt),g(o).removeClass(Lt).trigger(g.Event(kt.HIDDEN,s)))}}}},c._getParentFromElement=function(t){var e,n=_.getSelectorFromElement(t);return n&&(e=document.querySelector(n)),e||t.parentNode},c._dataApiKeydownHandler=function(t){if((/input|textarea/i.test(t.target.tagName)?!(32===t.which||27!==t.which&&(40!==t.which&&38!==t.which||g(t.target).closest(qt).length)):Ot.test(t.which))&&(t.preventDefault(),t.stopPropagation(),!this.disabled&&!g(this).hasClass(Pt))){var e=c._getParentFromElement(this),n=g(e).hasClass(Lt);if(n&&(!n||27!==t.which&&32!==t.which)){var i=[].slice.call(e.querySelectorAll(Kt));if(0!==i.length){var o=i.indexOf(t.target);38===t.which&&0<o&&o--,40===t.which&&o<i.length-1&&o++,o<0&&(o=0),i[o].focus()}}else{if(27===t.which){var r=e.querySelector(Ut);g(r).trigger("focus")}g(this).trigger("click")}}},s(c,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return $t}},{key:"DefaultType",get:function(){return Gt}}]),c}();g(document).on(kt.KEYDOWN_DATA_API,Ut,Jt._dataApiKeydownHandler).on(kt.KEYDOWN_DATA_API,qt,Jt._dataApiKeydownHandler).on(kt.CLICK_DATA_API+" "+kt.KEYUP_DATA_API,Jt._clearMenus).on(kt.CLICK_DATA_API,Ut,function(t){t.preventDefault(),t.stopPropagation(),Jt._jQueryInterface.call(g(this),"toggle")}).on(kt.CLICK_DATA_API,Wt,function(t){t.stopPropagation()}),g.fn[It]=Jt._jQueryInterface,g.fn[It].Constructor=Jt,g.fn[It].noConflict=function(){return g.fn[It]=Nt,Jt._jQueryInterface};var Zt="modal",te="bs.modal",ee="."+te,ne=g.fn[Zt],ie={backdrop:!0,keyboard:!0,focus:!0,show:!0},oe={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean",show:"boolean"},re={HIDE:"hide"+ee,HIDDEN:"hidden"+ee,SHOW:"show"+ee,SHOWN:"shown"+ee,FOCUSIN:"focusin"+ee,RESIZE:"resize"+ee,CLICK_DISMISS:"click.dismiss"+ee,KEYDOWN_DISMISS:"keydown.dismiss"+ee,MOUSEUP_DISMISS:"mouseup.dismiss"+ee,MOUSEDOWN_DISMISS:"mousedown.dismiss"+ee,CLICK_DATA_API:"click"+ee+".data-api"},se="modal-dialog-scrollable",ae="modal-scrollbar-measure",le="modal-backdrop",ce="modal-open",he="fade",ue="show",fe=".modal-dialog",de=".modal-body",ge='[data-toggle="modal"]',_e='[data-dismiss="modal"]',me=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",pe=".sticky-top",ve=function(){function o(t,e){this._config=this._getConfig(e),this._element=t,this._dialog=t.querySelector(fe),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}var t=o.prototype;return t.toggle=function(t){return this._isShown?this.hide():this.show(t)},t.show=function(t){var e=this;if(!this._isShown&&!this._isTransitioning){g(this._element).hasClass(he)&&(this._isTransitioning=!0);var n=g.Event(re.SHOW,{relatedTarget:t});g(this._element).trigger(n),this._isShown||n.isDefaultPrevented()||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),g(this._element).on(re.CLICK_DISMISS,_e,function(t){return e.hide(t)}),g(this._dialog).on(re.MOUSEDOWN_DISMISS,function(){g(e._element).one(re.MOUSEUP_DISMISS,function(t){g(t.target).is(e._element)&&(e._ignoreBackdropClick=!0)})}),this._showBackdrop(function(){return e._showElement(t)}))}},t.hide=function(t){var e=this;if(t&&t.preventDefault(),this._isShown&&!this._isTransitioning){var n=g.Event(re.HIDE);if(g(this._element).trigger(n),this._isShown&&!n.isDefaultPrevented()){this._isShown=!1;var i=g(this._element).hasClass(he);if(i&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),g(document).off(re.FOCUSIN),g(this._element).removeClass(ue),g(this._element).off(re.CLICK_DISMISS),g(this._dialog).off(re.MOUSEDOWN_DISMISS),i){var o=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,function(t){return e._hideModal(t)}).emulateTransitionEnd(o)}else this._hideModal()}}},t.dispose=function(){[window,this._element,this._dialog].forEach(function(t){return g(t).off(ee)}),g(document).off(re.FOCUSIN),g.removeData(this._element,te),this._config=null,this._element=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null},t.handleUpdate=function(){this._adjustDialog()},t._getConfig=function(t){return t=l({},ie,t),_.typeCheckConfig(Zt,t,oe),t},t._showElement=function(t){var e=this,n=g(this._element).hasClass(he);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),g(this._dialog).hasClass(se)?this._dialog.querySelector(de).scrollTop=0:this._element.scrollTop=0,n&&_.reflow(this._element),g(this._element).addClass(ue),this._config.focus&&this._enforceFocus();var i=g.Event(re.SHOWN,{relatedTarget:t}),o=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,g(e._element).trigger(i)};if(n){var r=_.getTransitionDurationFromElement(this._dialog);g(this._dialog).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o()},t._enforceFocus=function(){var e=this;g(document).off(re.FOCUSIN).on(re.FOCUSIN,function(t){document!==t.target&&e._element!==t.target&&0===g(e._element).has(t.target).length&&e._element.focus()})},t._setEscapeEvent=function(){var e=this;this._isShown&&this._config.keyboard?g(this._element).on(re.KEYDOWN_DISMISS,function(t){27===t.which&&(t.preventDefault(),e.hide())}):this._isShown||g(this._element).off(re.KEYDOWN_DISMISS)},t._setResizeEvent=function(){var e=this;this._isShown?g(window).on(re.RESIZE,function(t){return e.handleUpdate(t)}):g(window).off(re.RESIZE)},t._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._isTransitioning=!1,this._showBackdrop(function(){g(document.body).removeClass(ce),t._resetAdjustments(),t._resetScrollbar(),g(t._element).trigger(re.HIDDEN)})},t._removeBackdrop=function(){this._backdrop&&(g(this._backdrop).remove(),this._backdrop=null)},t._showBackdrop=function(t){var e=this,n=g(this._element).hasClass(he)?he:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className=le,n&&this._backdrop.classList.add(n),g(this._backdrop).appendTo(document.body),g(this._element).on(re.CLICK_DISMISS,function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._element.focus():e.hide())}),n&&_.reflow(this._backdrop),g(this._backdrop).addClass(ue),!t)return;if(!n)return void t();var i=_.getTransitionDurationFromElement(this._backdrop);g(this._backdrop).one(_.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){g(this._backdrop).removeClass(ue);var o=function(){e._removeBackdrop(),t&&t()};if(g(this._element).hasClass(he)){var r=_.getTransitionDurationFromElement(this._backdrop);g(this._backdrop).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o()}else t&&t()},t._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()},t._setScrollbar=function(){var o=this;if(this._isBodyOverflowing){var t=[].slice.call(document.querySelectorAll(me)),e=[].slice.call(document.querySelectorAll(pe));g(t).each(function(t,e){var n=e.style.paddingRight,i=g(e).css("padding-right");g(e).data("padding-right",n).css("padding-right",parseFloat(i)+o._scrollbarWidth+"px")}),g(e).each(function(t,e){var n=e.style.marginRight,i=g(e).css("margin-right");g(e).data("margin-right",n).css("margin-right",parseFloat(i)-o._scrollbarWidth+"px")});var n=document.body.style.paddingRight,i=g(document.body).css("padding-right");g(document.body).data("padding-right",n).css("padding-right",parseFloat(i)+this._scrollbarWidth+"px")}g(document.body).addClass(ce)},t._resetScrollbar=function(){var t=[].slice.call(document.querySelectorAll(me));g(t).each(function(t,e){var n=g(e).data("padding-right");g(e).removeData("padding-right"),e.style.paddingRight=n||""});var e=[].slice.call(document.querySelectorAll(""+pe));g(e).each(function(t,e){var n=g(e).data("margin-right");"undefined"!=typeof n&&g(e).css("margin-right",n).removeData("margin-right")});var n=g(document.body).data("padding-right");g(document.body).removeData("padding-right"),document.body.style.paddingRight=n||""},t._getScrollbarWidth=function(){var t=document.createElement("div");t.className=ae,document.body.appendChild(t);var e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e},o._jQueryInterface=function(n,i){return this.each(function(){var t=g(this).data(te),e=l({},ie,g(this).data(),"object"==typeof n&&n?n:{});if(t||(t=new o(this,e),g(this).data(te,t)),"string"==typeof n){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n](i)}else e.show&&t.show(i)})},s(o,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return ie}}]),o}();g(document).on(re.CLICK_DATA_API,ge,function(t){var e,n=this,i=_.getSelectorFromElement(this);i&&(e=document.querySelector(i));var o=g(e).data(te)?"toggle":l({},g(e).data(),g(this).data());"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault();var r=g(e).one(re.SHOW,function(t){t.isDefaultPrevented()||r.one(re.HIDDEN,function(){g(n).is(":visible")&&n.focus()})});ve._jQueryInterface.call(g(e),o,this)}),g.fn[Zt]=ve._jQueryInterface,g.fn[Zt].Constructor=ve,g.fn[Zt].noConflict=function(){return g.fn[Zt]=ne,ve._jQueryInterface};var ye=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],Ee={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Ce=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,Te=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function Se(t,s,e){if(0===t.length)return t;if(e&&"function"==typeof e)return e(t);for(var n=(new window.DOMParser).parseFromString(t,"text/html"),a=Object.keys(s),l=[].slice.call(n.body.querySelectorAll("*")),i=function(t,e){var n=l[t],i=n.nodeName.toLowerCase();if(-1===a.indexOf(n.nodeName.toLowerCase()))return n.parentNode.removeChild(n),"continue";var o=[].slice.call(n.attributes),r=[].concat(s["*"]||[],s[i]||[]);o.forEach(function(t){(function(t,e){var n=t.nodeName.toLowerCase();if(-1!==e.indexOf(n))return-1===ye.indexOf(n)||Boolean(t.nodeValue.match(Ce)||t.nodeValue.match(Te));for(var i=e.filter(function(t){return t instanceof RegExp}),o=0,r=i.length;o<r;o++)if(n.match(i[o]))return!0;return!1})(t,r)||n.removeAttribute(t.nodeName)})},o=0,r=l.length;o<r;o++)i(o);return n.body.innerHTML}var be="tooltip",Ie="bs.tooltip",De="."+Ie,we=g.fn[be],Ae="bs-tooltip",Ne=new RegExp("(^|\\s)"+Ae+"\\S+","g"),Oe=["sanitize","whiteList","sanitizeFn"],ke={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object"},Pe={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"},Le={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0<e.length&&t.removeClass(e.join(""))},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ye),e="object"==typeof n?n:null;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Je}},{key:"NAME",get:function(){return Ve}},{key:"DATA_KEY",get:function(){return Ye}},{key:"Event",get:function(){return rn}},{key:"EVENT_KEY",get:function(){return ze}},{key:"DefaultType",get:function(){return Ze}}]),i}(Be);g.fn[Ve]=sn._jQueryInterface,g.fn[Ve].Constructor=sn,g.fn[Ve].noConflict=function(){return g.fn[Ve]=Xe,sn._jQueryInterface};var an="scrollspy",ln="bs.scrollspy",cn="."+ln,hn=g.fn[an],un={offset:10,method:"auto",target:""},fn={offset:"number",method:"string",target:"(string|element)"},dn={ACTIVATE:"activate"+cn,SCROLL:"scroll"+cn,LOAD_DATA_API:"load"+cn+".data-api"},gn="dropdown-item",_n="active",mn='[data-spy="scroll"]',pn=".nav, .list-group",vn=".nav-link",yn=".nav-item",En=".list-group-item",Cn=".dropdown",Tn=".dropdown-item",Sn=".dropdown-toggle",bn="offset",In="position",Dn=function(){function n(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" "+vn+","+this._config.target+" "+En+","+this._config.target+" "+Tn,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,g(this._scrollElement).on(dn.SCROLL,function(t){return n._process(t)}),this.refresh(),this._process()}var t=n.prototype;return t.refresh=function(){var e=this,t=this._scrollElement===this._scrollElement.window?bn:In,o="auto"===this._config.method?t:this._config.method,r=o===In?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map(function(t){var e,n=_.getSelectorFromElement(t);if(n&&(e=document.querySelector(n)),e){var i=e.getBoundingClientRect();if(i.width||i.height)return[g(e)[o]().top+r,n]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},t.dispose=function(){g.removeData(this._element,ln),g(this._scrollElement).off(cn),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},t._getConfig=function(t){if("string"!=typeof(t=l({},un,"object"==typeof t&&t?t:{})).target){var e=g(t.target).attr("id");e||(e=_.getUID(an),g(t.target).attr("id",e)),t.target="#"+e}return _.typeCheckConfig(an,t,fn),t},t._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},t._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},t._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},t._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),n<=t){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t<this._offsets[0]&&0<this._offsets[0])return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t<this._offsets[o+1])&&this._activate(this._targets[o])}}},t._activate=function(e){this._activeTarget=e,this._clear();var t=this._selector.split(",").map(function(t){return t+'[data-target="'+e+'"],'+t+'[href="'+e+'"]'}),n=g([].slice.call(document.querySelectorAll(t.join(","))));n.hasClass(gn)?(n.closest(Cn).find(Sn).addClass(_n),n.addClass(_n)):(n.addClass(_n),n.parents(pn).prev(vn+", "+En).addClass(_n),n.parents(pn).prev(yn).children(vn).addClass(_n)),g(this._scrollElement).trigger(dn.ACTIVATE,{relatedTarget:e})},t._clear=function(){[].slice.call(document.querySelectorAll(this._selector)).filter(function(t){return t.classList.contains(_n)}).forEach(function(t){return t.classList.remove(_n)})},n._jQueryInterface=function(e){return this.each(function(){var t=g(this).data(ln);if(t||(t=new n(this,"object"==typeof e&&e),g(this).data(ln,t)),"string"==typeof e){if("undefined"==typeof t[e])throw new TypeError('No method named "'+e+'"');t[e]()}})},s(n,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return un}}]),n}();g(window).on(dn.LOAD_DATA_API,function(){for(var t=[].slice.call(document.querySelectorAll(mn)),e=t.length;e--;){var n=g(t[e]);Dn._jQueryInterface.call(n,n.data())}}),g.fn[an]=Dn._jQueryInterface,g.fn[an].Constructor=Dn,g.fn[an].noConflict=function(){return g.fn[an]=hn,Dn._jQueryInterface};var wn="bs.tab",An="."+wn,Nn=g.fn.tab,On={HIDE:"hide"+An,HIDDEN:"hidden"+An,SHOW:"show"+An,SHOWN:"shown"+An,CLICK_DATA_API:"click"+An+".data-api"},kn="dropdown-menu",Pn="active",Ln="disabled",jn="fade",Hn="show",Rn=".dropdown",xn=".nav, .list-group",Fn=".active",Un="> li > .active",Wn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',qn=".dropdown-toggle",Mn="> .dropdown-menu .active",Kn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&g(this._element).hasClass(Pn)||g(this._element).hasClass(Ln))){var t,i,e=g(this._element).closest(xn)[0],o=_.getSelectorFromElement(this._element);if(e){var r="UL"===e.nodeName||"OL"===e.nodeName?Un:Fn;i=(i=g.makeArray(g(e).find(r)))[i.length-1]}var s=g.Event(On.HIDE,{relatedTarget:this._element}),a=g.Event(On.SHOW,{relatedTarget:i});if(i&&g(i).trigger(s),g(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){o&&(t=document.querySelector(o)),this._activate(this._element,e);var l=function(){var t=g.Event(On.HIDDEN,{relatedTarget:n._element}),e=g.Event(On.SHOWN,{relatedTarget:i});g(i).trigger(t),g(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){g.removeData(this._element,wn),this._element=null},t._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?g(e).children(Fn):g(e).find(Un))[0],r=n&&o&&g(o).hasClass(jn),s=function(){return i._transitionComplete(t,o,n)};if(o&&r){var a=_.getTransitionDurationFromElement(o);g(o).removeClass(Hn).one(_.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){g(e).removeClass(Pn);var i=g(e.parentNode).find(Mn)[0];i&&g(i).removeClass(Pn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(g(t).addClass(Pn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),_.reflow(t),t.classList.contains(jn)&&t.classList.add(Hn),t.parentNode&&g(t.parentNode).hasClass(kn)){var o=g(t).closest(Rn)[0];if(o){var r=[].slice.call(o.querySelectorAll(qn));g(r).addClass(Pn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=g(this),e=t.data(wn);if(e||(e=new i(this),t.data(wn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),i}();g(document).on(On.CLICK_DATA_API,Wn,function(t){t.preventDefault(),Kn._jQueryInterface.call(g(this),"show")}),g.fn.tab=Kn._jQueryInterface,g.fn.tab.Constructor=Kn,g.fn.tab.noConflict=function(){return g.fn.tab=Nn,Kn._jQueryInterface};var Qn="toast",Bn="bs.toast",Vn="."+Bn,Yn=g.fn[Qn],zn={CLICK_DISMISS:"click.dismiss"+Vn,HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn},Xn="fade",$n="hide",Gn="show",Jn="showing",Zn={animation:"boolean",autohide:"boolean",delay:"number"},ti={animation:!0,autohide:!0,delay:500},ei='[data-dismiss="toast"]',ni=function(){function i(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var t=i.prototype;return t.show=function(){var t=this;g(this._element).trigger(zn.SHOW),this._config.animation&&this._element.classList.add(Xn);var e=function(){t._element.classList.remove(Jn),t._element.classList.add(Gn),g(t._element).trigger(zn.SHOWN),t._config.autohide&&t.hide()};if(this._element.classList.remove($n),this._element.classList.add(Jn),this._config.animation){var n=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},t.hide=function(t){var e=this;this._element.classList.contains(Gn)&&(g(this._element).trigger(zn.HIDE),t?this._close():this._timeout=setTimeout(function(){e._close()},this._config.delay))},t.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains(Gn)&&this._element.classList.remove(Gn),g(this._element).off(zn.CLICK_DISMISS),g.removeData(this._element,Bn),this._element=null,this._config=null},t._getConfig=function(t){return t=l({},ti,g(this._element).data(),"object"==typeof t&&t?t:{}),_.typeCheckConfig(Qn,t,this.constructor.DefaultType),t},t._setListeners=function(){var t=this;g(this._element).on(zn.CLICK_DISMISS,ei,function(){return t.hide(!0)})},t._close=function(){var t=this,e=function(){t._element.classList.add($n),g(t._element).trigger(zn.HIDDEN)};if(this._element.classList.remove(Gn),this._config.animation){var n=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},i._jQueryInterface=function(n){return this.each(function(){var t=g(this),e=t.data(Bn);if(e||(e=new i(this,"object"==typeof n&&n),t.data(Bn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n](this)}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"DefaultType",get:function(){return Zn}},{key:"Default",get:function(){return ti}}]),i}();g.fn[Qn]=ni._jQueryInterface,g.fn[Qn].Constructor=ni,g.fn[Qn].noConflict=function(){return g.fn[Qn]=Yn,ni._jQueryInterface},function(){if("undefined"==typeof g)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=g.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||4<=t[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),t.Util=_,t.Alert=p,t.Button=P,t.Carousel=lt,t.Collapse=bt,t.Dropdown=Jt,t.Modal=ve,t.Popover=sn,t.Scrollspy=Dn,t.Tab=Kn,t.Toast=ni,t.Tooltip=Be,Object.defineProperty(t,"__esModule",{value:!0})}); +//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/static/chart_js_2.9.3/Chart.bundle.min.js b/pikabu_ru/profile_rating__tracking/static/chart_js_2.9.3/Chart.bundle.min.js new file mode 100644 index 000000000..55d9eb03f --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/chart_js_2.9.3/Chart.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Chart.js v2.9.3 + * https://www.chartjs.org + * (c) 2019 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Chart=e()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},i=e((function(t){var e={};for(var i in n)n.hasOwnProperty(i)&&(e[n[i]]=i);var a=t.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var r in a)if(a.hasOwnProperty(r)){if(!("channels"in a[r]))throw new Error("missing channels property: "+r);if(!("labels"in a[r]))throw new Error("missing channel labels property: "+r);if(a[r].labels.length!==a[r].channels)throw new Error("channel and label counts mismatch: "+r);var o=a[r].channels,s=a[r].labels;delete a[r].channels,delete a[r].labels,Object.defineProperty(a[r],"channels",{value:o}),Object.defineProperty(a[r],"labels",{value:s})}a.rgb.hsl=function(t){var e,n,i=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o;return s===o?e=0:i===s?e=(a-r)/l:a===s?e=2+(r-i)/l:r===s&&(e=4+(i-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),n=(o+s)/2,[e,100*(s===o?0:n<=.5?l/(s+o):l/(2-s-o)),100*n]},a.rgb.hsv=function(t){var e,n,i,a,r,o=t[0]/255,s=t[1]/255,l=t[2]/255,u=Math.max(o,s,l),d=u-Math.min(o,s,l),h=function(t){return(u-t)/6/d+.5};return 0===d?a=r=0:(r=d/u,e=h(o),n=h(s),i=h(l),o===u?a=i-n:s===u?a=1/3+e-i:l===u&&(a=2/3+n-e),a<0?a+=1:a>1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d<s&&(s=d,a=l)}return a},a.keyword.rgb=function(t){return n[t]},a.rgb.xyz=function(t){var e=t[0]/255,n=t[1]/255,i=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a<n;a++)t[e[a]]={distance:-1,parent:null};return t}(),n=[t];for(e[t].distance=0;n.length;)for(var a=n.pop(),r=Object.keys(i[a]),o=r.length,s=0;s<o;s++){var l=r[s],u=e[l];-1===u.distance&&(u.distance=e[a].distance+1,u.parent=a,n.unshift(l))}return e}function r(t,e){return function(n){return e(t(n))}}function o(t,e){for(var n=[e[t].parent,t],a=i[e[t].parent][t],o=e[t].parent;e[o].parent;)n.unshift(e[o].parent),a=r(i[e[o].parent][o],a),o=e[o].parent;return a.conversion=n,a}var s={};Object.keys(i).forEach((function(t){s[t]={},Object.defineProperty(s[t],"channels",{value:i[t].channels}),Object.defineProperty(s[t],"labels",{value:i[t].labels});var e=function(t){for(var e=a(t),n={},i=Object.keys(e),r=i.length,s=0;s<r;s++){var l=i[s];null!==e[l].parent&&(n[l]=o(l,e))}return n}(t);Object.keys(e).forEach((function(n){var i=e[n];s[t][n]=function(t){var e=function(e){if(null==e)return e;arguments.length>1&&(e=Array.prototype.slice.call(arguments));var n=t(e);if("object"==typeof n)for(var i=n.length,a=0;a<i;a++)n[a]=Math.round(n[a]);return n};return"conversion"in t&&(e.conversion=t.conversion),e}(i),s[t][n].raw=function(t){var e=function(e){return null==e?e:(arguments.length>1&&(e=Array.prototype.slice.call(arguments)),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(i)}))}));var l=s,u={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},d={getRgba:h,getHsla:c,getRgb:function(t){var e=h(t);return e&&e.slice(0,3)},getHsl:function(t){var e=c(t);return e&&e.slice(0,3)},getHwb:f,getAlpha:function(t){var e=h(t);if(e)return e[3];if(e=c(t))return e[3];if(e=f(t))return e[3]},hexString:function(t,e){e=void 0!==e&&3===t.length?e:t[3];return"#"+b(t[0])+b(t[1])+b(t[2])+(e>=0&&e<1?b(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return g(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:g,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return m(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"},percentaString:m,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return p(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:p,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return y[t.slice(0,3)]}};function h(t){if(t){var e=[0,0,0],n=1,i=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(i){a=(i=i[1])[3];for(var r=0;r<e.length;r++)e[r]=parseInt(i[r]+i[r],16);a&&(n=Math.round(parseInt(a+a,16)/255*100)/100)}else if(i=t.match(/^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i)){a=i[2],i=i[1];for(r=0;r<e.length;r++)e[r]=parseInt(i.slice(2*r,2*r+2),16);a&&(n=Math.round(parseInt(a,16)/255*100)/100)}else if(i=t.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(r=0;r<e.length;r++)e[r]=parseInt(i[r+1]);n=parseFloat(i[4])}else if(i=t.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(r=0;r<e.length;r++)e[r]=Math.round(2.55*parseFloat(i[r+1]));n=parseFloat(i[4])}else if(i=t.match(/(\w+)/)){if("transparent"==i[1])return[0,0,0,0];if(!(e=u[i[1]]))return}for(r=0;r<e.length;r++)e[r]=v(e[r],0,255);return n=n||0==n?v(n,0,1):1,e[3]=n,e}}function c(t){if(t){var e=t.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var n=parseFloat(e[4]);return[v(parseInt(e[1]),0,360),v(parseFloat(e[2]),0,100),v(parseFloat(e[3]),0,100),v(isNaN(n)?1:n,0,1)]}}}function f(t){if(t){var e=t.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var n=parseFloat(e[4]);return[v(parseInt(e[1]),0,360),v(parseFloat(e[2]),0,100),v(parseFloat(e[3]),0,100),v(isNaN(n)?1:n,0,1)]}}}function g(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function m(t,e){return"rgba("+Math.round(t[0]/255*100)+"%, "+Math.round(t[1]/255*100)+"%, "+Math.round(t[2]/255*100)+"%, "+(e||t[3]||1)+")"}function p(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function v(t,e,n){return Math.min(Math.max(e,t),n)}function b(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}var y={};for(var x in u)y[u[x]]=x;var _=function(t){return t instanceof _?t:this instanceof _?(this.valid=!1,this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1},void("string"==typeof t?(e=d.getRgba(t))?this.setValues("rgb",e):(e=d.getHsla(t))?this.setValues("hsl",e):(e=d.getHwb(t))&&this.setValues("hwb",e):"object"==typeof t&&(void 0!==(e=t).r||void 0!==e.red?this.setValues("rgb",e):void 0!==e.l||void 0!==e.lightness?this.setValues("hsl",e):void 0!==e.v||void 0!==e.value?this.setValues("hsv",e):void 0!==e.w||void 0!==e.whiteness?this.setValues("hwb",e):void 0===e.c&&void 0===e.cyan||this.setValues("cmyk",e)))):new _(t);var e};_.prototype={isValid:function(){return this.valid},rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var t=this.values;return 1!==t.alpha?t.hwb.concat([t.alpha]):t.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var t=this.values;return t.rgb.concat([t.alpha])},hslaArray:function(){var t=this.values;return t.hsl.concat([t.alpha])},alpha:function(t){return void 0===t?this.values.alpha:(this.setValues("alpha",t),this)},red:function(t){return this.setChannel("rgb",0,t)},green:function(t){return this.setChannel("rgb",1,t)},blue:function(t){return this.setChannel("rgb",2,t)},hue:function(t){return t&&(t=(t%=360)<0?360+t:t),this.setChannel("hsl",0,t)},saturation:function(t){return this.setChannel("hsl",1,t)},lightness:function(t){return this.setChannel("hsl",2,t)},saturationv:function(t){return this.setChannel("hsv",1,t)},whiteness:function(t){return this.setChannel("hwb",1,t)},blackness:function(t){return this.setChannel("hwb",2,t)},value:function(t){return this.setChannel("hsv",2,t)},cyan:function(t){return this.setChannel("cmyk",0,t)},magenta:function(t){return this.setChannel("cmyk",1,t)},yellow:function(t){return this.setChannel("cmyk",2,t)},black:function(t){return this.setChannel("cmyk",3,t)},hexString:function(){return d.hexString(this.values.rgb)},rgbString:function(){return d.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return d.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return d.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return d.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return d.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return d.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return d.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var t=this.values.rgb;return t[0]<<16|t[1]<<8|t[2]},luminosity:function(){for(var t=this.values.rgb,e=[],n=0;n<t.length;n++){var i=t[n]/255;e[n]=i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4)}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast:function(t){var e=this.luminosity(),n=t.luminosity();return e>n?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=t,i=void 0===e?.5:e,a=2*i-1,r=this.alpha()-n.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*n.red(),o*this.green()+s*n.green(),o*this.blue()+s*n.blue()).alpha(this.alpha()*i+n.alpha()*(1-i))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new _,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},_.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},_.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},_.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i<t.length;i++)n[t.charAt(i)]=e[t][i];return 1!==e.alpha&&(n.a=e.alpha),n},_.prototype.setValues=function(t,e){var n,i,a=this.values,r=this.spaces,o=this.maxes,s=1;if(this.valid=!0,"alpha"===t)s=e;else if(e.length)a[t]=e.slice(0,t.length),s=e[t.length];else if(void 0!==e[t.charAt(0)]){for(n=0;n<t.length;n++)a[t][n]=e[t.charAt(n)];s=e.a}else if(void 0!==e[r[t][0]]){var u=r[t];for(n=0;n<t.length;n++)a[t][n]=e[u[n]];s=e.alpha}if(a.alpha=Math.max(0,Math.min(1,void 0===s?a.alpha:s)),"alpha"===t)return!1;for(n=0;n<t.length;n++)i=Math.max(0,Math.min(o[t][n],a[t][n])),a[t][n]=Math.round(i);for(var d in r)d!==t&&(a[d]=l[t][d](a[t]));return!0},_.prototype.setSpace=function(t,e){var n=e[0];return void 0===n?this.getValues(t):("number"==typeof n&&(n=Array.prototype.slice.call(e)),this.setValues(t,n),this)},_.prototype.setChannel=function(t,e,n){var i=this.values[t];return void 0===n?i[e]:n===i[e]?this:(i[e]=n,this.setValues(t,i),this)},"undefined"!=typeof window&&(window.Color=_);var w,k=_,M={noop:function(){},uid:(w=0,function(){return w++}),isNullOrUndef:function(t){return null==t},isArray:function(t){if(Array.isArray&&Array.isArray(t))return!0;var e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)},isObject:function(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)},isFinite:function(t){return("number"==typeof t||t instanceof Number)&&isFinite(t)},valueOrDefault:function(t,e){return void 0===t?e:t},valueAtIndexOrDefault:function(t,e,n){return M.valueOrDefault(M.isArray(t)?t[e]:t,n)},callback:function(t,e,n){if(t&&"function"==typeof t.call)return t.apply(n,e)},each:function(t,e,n,i){var a,r,o;if(M.isArray(t))if(r=t.length,i)for(a=r-1;a>=0;a--)e.call(n,t[a],a);else for(a=0;a<r;a++)e.call(n,t[a],a);else if(M.isObject(t))for(r=(o=Object.keys(t)).length,a=0;a<r;a++)e.call(n,t[o[a]],o[a])},arrayEquals:function(t,e){var n,i,a,r;if(!t||!e||t.length!==e.length)return!1;for(n=0,i=t.length;n<i;++n)if(a=t[n],r=e[n],a instanceof Array&&r instanceof Array){if(!M.arrayEquals(a,r))return!1}else if(a!==r)return!1;return!0},clone:function(t){if(M.isArray(t))return t.map(M.clone);if(M.isObject(t)){for(var e={},n=Object.keys(t),i=n.length,a=0;a<i;++a)e[n[a]]=M.clone(t[n[a]]);return e}return t},_merger:function(t,e,n,i){var a=e[t],r=n[t];M.isObject(a)&&M.isObject(r)?M.merge(a,r,i):e[t]=M.clone(r)},_mergerIf:function(t,e,n){var i=e[t],a=n[t];M.isObject(i)&&M.isObject(a)?M.mergeIf(i,a):e.hasOwnProperty(t)||(e[t]=M.clone(a))},merge:function(t,e,n){var i,a,r,o,s,l=M.isArray(e)?e:[e],u=l.length;if(!M.isObject(t))return t;for(i=(n=n||{}).merger||M._merger,a=0;a<u;++a)if(e=l[a],M.isObject(e))for(s=0,o=(r=Object.keys(e)).length;s<o;++s)i(r[s],t,e,n);return t},mergeIf:function(t,e){return M.merge(t,e,{merger:M._mergerIf})},extend:Object.assign||function(t){return M.merge(t,[].slice.call(arguments,1),{merger:function(t,e,n){e[t]=n[t]}})},inherits:function(t){var e=this,n=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return e.apply(this,arguments)},i=function(){this.constructor=n};return i.prototype=e.prototype,n.prototype=new i,n.extend=M.inherits,t&&M.extend(n.prototype,t),n.__super__=e.prototype,n},_deprecated:function(t,e,n,i){void 0!==e&&console.warn(t+': "'+n+'" is deprecated. Please use "'+i+'" instead')}},S=M;M.callCallback=M.callback,M.indexOf=function(t,e,n){return Array.prototype.indexOf.call(t,e,n)},M.getValueOrDefault=M.valueOrDefault,M.getValueAtIndexOrDefault=M.valueAtIndexOrDefault;var D={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return(t-=1)*t*t+1},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-((t-=1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return t*t*t*t*t},easeOutQuint:function(t){return(t-=1)*t*t*t*t+1},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return 1-Math.cos(t*(Math.PI/2))},easeOutSine:function(t){return Math.sin(t*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t)-1)},easeInExpo:function(t){return 0===t?0:Math.pow(2,10*(t-1))},easeOutExpo:function(t){return 1===t?1:1-Math.pow(2,-10*t)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(2-Math.pow(2,-10*--t))},easeInCirc:function(t){return t>=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),-i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n))},easeOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),i*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/n)+1)},easeInOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:2==(t/=.5)?1:(n||(n=.45),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),t<1?i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*-.5:i*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-D.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*D.easeInBounce(2*t):.5*D.easeOutBounce(2*t-1)+.5}},C={effects:D};S.easingEffects=D;var P=Math.PI,T=P/180,O=2*P,A=P/2,F=P/4,I=2*P/3,L={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,n,i,a,r){if(r){var o=Math.min(r,a/2,i/2),s=e+o,l=n+o,u=e+i-o,d=n+a-o;t.moveTo(e,l),s<u&&l<d?(t.arc(s,l,o,-P,-A),t.arc(u,l,o,-A,0),t.arc(u,d,o,0,A),t.arc(s,d,o,A,P)):s<u?(t.moveTo(s,n),t.arc(u,l,o,-A,A),t.arc(s,l,o,A,P+A)):l<d?(t.arc(s,l,o,-P,0),t.arc(s,d,o,0,P)):t.arc(s,l,o,-P,P),t.closePath(),t.moveTo(e,n)}else t.rect(e,n,i,a)},drawPoint:function(t,e,n,i,a,r){var o,s,l,u,d,h=(r||0)*T;if(e&&"object"==typeof e&&("[object HTMLImageElement]"===(o=e.toString())||"[object HTMLCanvasElement]"===o))return t.save(),t.translate(i,a),t.rotate(h),t.drawImage(e,-e.width/2,-e.height/2,e.width,e.height),void t.restore();if(!(isNaN(n)||n<=0)){switch(t.beginPath(),e){default:t.arc(i,a,n,0,O),t.closePath();break;case"triangle":t.moveTo(i+Math.sin(h)*n,a-Math.cos(h)*n),h+=I,t.lineTo(i+Math.sin(h)*n,a-Math.cos(h)*n),h+=I,t.lineTo(i+Math.sin(h)*n,a-Math.cos(h)*n),t.closePath();break;case"rectRounded":u=n-(d=.516*n),s=Math.cos(h+F)*u,l=Math.sin(h+F)*u,t.arc(i-s,a-l,d,h-P,h-A),t.arc(i+l,a-s,d,h-A,h),t.arc(i+s,a+l,d,h,h+A),t.arc(i-l,a+s,d,h+A,h+P),t.closePath();break;case"rect":if(!r){u=Math.SQRT1_2*n,t.rect(i-u,a-u,2*u,2*u);break}h+=F;case"rectRot":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+l,a-s),t.lineTo(i+s,a+l),t.lineTo(i-l,a+s),t.closePath();break;case"crossRot":h+=F;case"cross":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s);break;case"star":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s),h+=F,s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s);break;case"line":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l);break;case"dash":t.moveTo(i,a),t.lineTo(i+Math.cos(h)*n,a+Math.sin(h)*n)}t.fill(),t.stroke()}},_isPointInArea:function(t,e){return t.x>e.left-1e-6&&t.x<e.right+1e-6&&t.y>e.top-1e-6&&t.y<e.bottom+1e-6},clipArea:function(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()},unclipArea:function(t){t.restore()},lineTo:function(t,e,n,i){var a=n.steppedLine;if(a){if("middle"===a){var r=(e.x+n.x)/2;t.lineTo(r,i?n.y:e.y),t.lineTo(r,i?e.y:n.y)}else"after"===a&&!i||"after"!==a&&i?t.lineTo(e.x,n.y):t.lineTo(n.x,e.y);t.lineTo(n.x,n.y)}else n.tension?t.bezierCurveTo(i?e.controlPointPreviousX:e.controlPointNextX,i?e.controlPointPreviousY:e.controlPointNextY,i?n.controlPointNextX:n.controlPointPreviousX,i?n.controlPointNextY:n.controlPointPreviousY,n.x,n.y):t.lineTo(n.x,n.y)}},R=L;S.clear=L.clear,S.drawRoundedRectangle=function(t){t.beginPath(),L.roundedRect.apply(L,arguments)};var N={_set:function(t,e){return S.merge(this[t]||(this[t]={}),e)}};N._set("global",{defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",defaultLineHeight:1.2,showLines:!0});var W=N,Y=S.valueOrDefault;var z={toLineHeight:function(t,e){var n=(""+t).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);if(!n||"normal"===n[1])return 1.2*e;switch(t=+n[2],n[3]){case"px":return t;case"%":t/=100}return e*t},toPadding:function(t){var e,n,i,a;return S.isObject(t)?(e=+t.top||0,n=+t.right||0,i=+t.bottom||0,a=+t.left||0):e=n=i=a=+t||0,{top:e,right:n,bottom:i,left:a,height:e+i,width:a+n}},_parseFont:function(t){var e=W.global,n=Y(t.fontSize,e.defaultFontSize),i={family:Y(t.fontFamily,e.defaultFontFamily),lineHeight:S.options.toLineHeight(Y(t.lineHeight,e.defaultLineHeight),n),size:n,style:Y(t.fontStyle,e.defaultFontStyle),weight:null,string:""};return i.string=function(t){return!t||S.isNullOrUndef(t.size)||S.isNullOrUndef(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}(i),i},resolve:function(t,e,n,i){var a,r,o,s=!0;for(a=0,r=t.length;a<r;++a)if(void 0!==(o=t[a])&&(void 0!==e&&"function"==typeof o&&(o=o(e),s=!1),void 0!==n&&S.isArray(o)&&(o=o[n],s=!1),void 0!==o))return i&&!s&&(i.cacheable=!1),o}},E={_factorize:function(t){var e,n=[],i=Math.sqrt(t);for(e=1;e<i;e++)t%e==0&&(n.push(e),n.push(t/e));return i===(0|i)&&n.push(i),n.sort((function(t,e){return t-e})).pop(),n},log10:Math.log10||function(t){var e=Math.log(t)*Math.LOG10E,n=Math.round(e);return t===Math.pow(10,n)?n:e}},V=E;S.log10=E.log10;var H=S,B=C,j=R,U=z,G=V,q={getRtlAdapter:function(t,e,n){return t?function(t,e){return{x:function(n){return t+t+e-n},setWidth:function(t){e=t},textAlign:function(t){return"center"===t?t:"right"===t?"left":"right"},xPlus:function(t,e){return t-e},leftForLtr:function(t,e){return t-e}}}(e,n):{x:function(t){return t},setWidth:function(t){},textAlign:function(t){return t},xPlus:function(t,e){return t+e},leftForLtr:function(t,e){return t}}},overrideTextDirection:function(t,e){var n,i;"ltr"!==e&&"rtl"!==e||(i=[(n=t.canvas.style).getPropertyValue("direction"),n.getPropertyPriority("direction")],n.setProperty("direction",e,"important"),t.prevTextDirection=i)},restoreTextDirection:function(t){var e=t.prevTextDirection;void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}};H.easing=B,H.canvas=j,H.options=U,H.math=G,H.rtl=q;var Z=function(t){H.extend(this,t),this.initialize.apply(this,arguments)};H.extend(Z.prototype,{_type:void 0,initialize:function(){this.hidden=!1},pivot:function(){var t=this;return t._view||(t._view=H.extend({},t._model)),t._start={},t},transition:function(t){var e=this,n=e._model,i=e._start,a=e._view;return n&&1!==t?(a||(a=e._view={}),i||(i=e._start={}),function(t,e,n,i){var a,r,o,s,l,u,d,h,c,f=Object.keys(n);for(a=0,r=f.length;a<r;++a)if(u=n[o=f[a]],e.hasOwnProperty(o)||(e[o]=u),(s=e[o])!==u&&"_"!==o[0]){if(t.hasOwnProperty(o)||(t[o]=s),(d=typeof u)===typeof(l=t[o]))if("string"===d){if((h=k(l)).valid&&(c=k(u)).valid){e[o]=c.mix(h,i).rgbString();continue}}else if(H.isFinite(l)&&H.isFinite(u)){e[o]=l+(u-l)*i;continue}e[o]=u}}(i,a,n,t),e):(e._view=H.extend({},n),e._start=null,e)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return H.isNumber(this._model.x)&&H.isNumber(this._model.y)}}),Z.extend=H.inherits;var $=Z,X=$.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),K=X;Object.defineProperty(X.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(X.prototype,"chartInstance",{get:function(){return this.chart},set:function(t){this.chart=t}}),W._set("global",{animation:{duration:1e3,easing:"easeOutQuart",onProgress:H.noop,onComplete:H.noop}});var J={animations:[],request:null,addAnimation:function(t,e,n,i){var a,r,o=this.animations;for(e.chart=t,e.startTime=Date.now(),e.duration=n,i||(t.animating=!0),a=0,r=o.length;a<r;++a)if(o[a].chart===t)return void(o[a]=e);o.push(e),1===o.length&&this.requestAnimationFrame()},cancelAnimation:function(t){var e=H.findIndex(this.animations,(function(e){return e.chart===t}));-1!==e&&(this.animations.splice(e,1),t.animating=!1)},requestAnimationFrame:function(){var t=this;null===t.request&&(t.request=H.requestAnimFrame.call(window,(function(){t.request=null,t.startDigest()})))},startDigest:function(){this.advance(),this.animations.length>0&&this.requestAnimationFrame()},advance:function(){for(var t,e,n,i,a=this.animations,r=0;r<a.length;)e=(t=a[r]).chart,n=t.numSteps,i=Math.floor((Date.now()-t.startTime)/t.duration*n)+1,t.currentStep=Math.min(i,n),H.callback(t.render,[e,t],e),H.callback(t.onAnimationProgress,[t],e),t.currentStep>=n?(H.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},Q=H.options.resolve,tt=["push","pop","shift","splice","unshift"];function et(t,e){var n=t._chartjs;if(n){var i=n.listeners,a=i.indexOf(e);-1!==a&&i.splice(a,1),i.length>0||(tt.forEach((function(e){delete t[e]})),delete t._chartjs)}}var nt=function(t,e){this.initialize(t,e)};H.extend(nt.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements(),n._type=n.getMeta().type},updateIndex:function(t){this.index=t},linkScales:function(){var t=this.getMeta(),e=this.chart,n=e.scales,i=this.getDataset(),a=e.options.scales;null!==t.xAxisID&&t.xAxisID in n&&!i.xAxisID||(t.xAxisID=i.xAxisID||a.xAxes[0].id),null!==t.yAxisID&&t.yAxisID in n&&!i.yAxisID||(t.yAxisID=i.yAxisID||a.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&et(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,n=this.getMeta(),i=this.getDataset().data||[],a=n.data;for(t=0,e=i.length;t<e;++t)a[t]=a[t]||this.createMetaData(t);n.dataset=n.dataset||this.createMetaDataset()},addElementAndReset:function(t){var e=this.createMetaData(t);this.getMeta().data.splice(t,0,e),this.updateElement(e,t,!0)},buildOrUpdateElements:function(){var t,e,n=this,i=n.getDataset(),a=i.data||(i.data=[]);n._data!==a&&(n._data&&et(n._data,n),a&&Object.isExtensible(a)&&(e=n,(t=a)._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),tt.forEach((function(e){var n="onData"+e.charAt(0).toUpperCase()+e.slice(1),i=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value:function(){var e=Array.prototype.slice.call(arguments),a=i.apply(this,e);return H.each(t._chartjs.listeners,(function(t){"function"==typeof t[n]&&t[n].apply(t,e)})),a}})})))),n._data=a),n.resyncElements()},_configure:function(){this._config=H.merge({},[this.chart.options.datasets[this._type],this.getDataset()],{merger:function(t,e,n){"_meta"!==t&&"data"!==t&&H._merger(t,e,n)}})},_update:function(t){this._configure(),this._cachedDataOpts=null,this.update(t)},update:H.noop,transition:function(t){for(var e=this.getMeta(),n=e.data||[],i=n.length,a=0;a<i;++a)n[a].transition(t);e.dataset&&e.dataset.transition(t)},draw:function(){var t=this.getMeta(),e=t.data||[],n=e.length,i=0;for(t.dataset&&t.dataset.draw();i<n;++i)e[i].draw()},getStyle:function(t){var e,n=this.getMeta(),i=n.dataset;return this._configure(),i&&void 0===t?e=this._resolveDatasetElementOptions(i||{}):(t=t||0,e=this._resolveDataElementOptions(n.data[t]||{},t)),!1!==e.fill&&null!==e.fill||(e.backgroundColor=e.borderColor),e},_resolveDatasetElementOptions:function(t,e){var n,i,a,r,o=this,s=o.chart,l=o._config,u=t.custom||{},d=s.options.elements[o.datasetElementType.prototype._type]||{},h=o._datasetElementOptions,c={},f={chart:s,dataset:o.getDataset(),datasetIndex:o.index,hover:e};for(n=0,i=h.length;n<i;++n)a=h[n],r=e?"hover"+a.charAt(0).toUpperCase()+a.slice(1):a,c[a]=Q([u[r],l[r],d[r]],f);return c},_resolveDataElementOptions:function(t,e){var n=this,i=t&&t.custom,a=n._cachedDataOpts;if(a&&!i)return a;var r,o,s,l,u=n.chart,d=n._config,h=u.options.elements[n.dataElementType.prototype._type]||{},c=n._dataElementOptions,f={},g={chart:u,dataIndex:e,dataset:n.getDataset(),datasetIndex:n.index},m={cacheable:!i};if(i=i||{},H.isArray(c))for(o=0,s=c.length;o<s;++o)f[l=c[o]]=Q([i[l],d[l],h[l]],g,e,m);else for(o=0,s=(r=Object.keys(c)).length;o<s;++o)f[l=r[o]]=Q([i[l],d[c[l]],d[l],h[l]],g,e,m);return m.cacheable&&(n._cachedDataOpts=Object.freeze(f)),f},removeHoverStyle:function(t){H.merge(t._model,t.$previousStyle||{}),delete t.$previousStyle},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],n=t._index,i=t.custom||{},a=t._model,r=H.getHoverColor;t.$previousStyle={backgroundColor:a.backgroundColor,borderColor:a.borderColor,borderWidth:a.borderWidth},a.backgroundColor=Q([i.hoverBackgroundColor,e.hoverBackgroundColor,r(a.backgroundColor)],void 0,n),a.borderColor=Q([i.hoverBorderColor,e.hoverBorderColor,r(a.borderColor)],void 0,n),a.borderWidth=Q([i.hoverBorderWidth,e.hoverBorderWidth,a.borderWidth],void 0,n)},_removeDatasetHoverStyle:function(){var t=this.getMeta().dataset;t&&this.removeHoverStyle(t)},_setDatasetHoverStyle:function(){var t,e,n,i,a,r,o=this.getMeta().dataset,s={};if(o){for(r=o._model,a=this._resolveDatasetElementOptions(o,!0),t=0,e=(i=Object.keys(a)).length;t<e;++t)s[n=i[t]]=r[n],r[n]=a[n];o.$previousStyle=s}},resyncElements:function(){var t=this.getMeta(),e=this.getDataset().data,n=t.data.length,i=e.length;i<n?t.data.splice(i,n-i):i>n&&this.insertElements(n,i-n)},insertElements:function(t,e){for(var n=0;n<e;++n)this.addElementAndReset(t+n)},onDataPush:function(){var t=arguments.length;this.insertElements(this.getDataset().data.length-t,t)},onDataPop:function(){this.getMeta().data.pop()},onDataShift:function(){this.getMeta().data.shift()},onDataSplice:function(t,e){this.getMeta().data.splice(t,e),this.insertElements(t,arguments.length-2)},onDataUnshift:function(){this.insertElements(0,arguments.length)}}),nt.extend=H.inherits;var it=nt,at=2*Math.PI;function rt(t,e){var n=e.startAngle,i=e.endAngle,a=e.pixelMargin,r=a/e.outerRadius,o=e.x,s=e.y;t.beginPath(),t.arc(o,s,e.outerRadius,n-r,i+r),e.innerRadius>a?(r=a/e.innerRadius,t.arc(o,s,e.innerRadius-a,i+r,n-r,!0)):t.arc(o,s,a,i+Math.PI/2,n-Math.PI/2),t.closePath(),t.clip()}function ot(t,e,n){var i="inner"===e.borderAlign;i?(t.lineWidth=2*e.borderWidth,t.lineJoin="round"):(t.lineWidth=e.borderWidth,t.lineJoin="bevel"),n.fullCircles&&function(t,e,n,i){var a,r=n.endAngle;for(i&&(n.endAngle=n.startAngle+at,rt(t,n),n.endAngle=r,n.endAngle===n.startAngle&&n.fullCircles&&(n.endAngle+=at,n.fullCircles--)),t.beginPath(),t.arc(n.x,n.y,n.innerRadius,n.startAngle+at,n.startAngle,!0),a=0;a<n.fullCircles;++a)t.stroke();for(t.beginPath(),t.arc(n.x,n.y,e.outerRadius,n.startAngle,n.startAngle+at),a=0;a<n.fullCircles;++a)t.stroke()}(t,e,n,i),i&&rt(t,n),t.beginPath(),t.arc(n.x,n.y,e.outerRadius,n.startAngle,n.endAngle),t.arc(n.x,n.y,n.innerRadius,n.endAngle,n.startAngle,!0),t.closePath(),t.stroke()}W._set("global",{elements:{arc:{backgroundColor:W.global.defaultColor,borderColor:"#fff",borderWidth:2,borderAlign:"center"}}});var st=$.extend({_type:"arc",inLabelRange:function(t){var e=this._view;return!!e&&Math.pow(t-e.x,2)<Math.pow(e.radius+e.hoverRadius,2)},inRange:function(t,e){var n=this._view;if(n){for(var i=H.getAngleFromPoint(n,{x:t,y:e}),a=i.angle,r=i.distance,o=n.startAngle,s=n.endAngle;s<o;)s+=at;for(;a>s;)a-=at;for(;a<o;)a+=at;var l=a>=o&&a<=s,u=r>=n.innerRadius&&r<=n.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t,e=this._chart.ctx,n=this._view,i="inner"===n.borderAlign?.33:0,a={x:n.x,y:n.y,innerRadius:n.innerRadius,outerRadius:Math.max(n.outerRadius-i,0),pixelMargin:i,startAngle:n.startAngle,endAngle:n.endAngle,fullCircles:Math.floor(n.circumference/at)};if(e.save(),e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+at,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),t=0;t<a.fullCircles;++t)e.fill();a.endAngle=a.startAngle+n.circumference%at}e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),e.fill(),n.borderWidth&&ot(e,n,a),e.restore()}}),lt=H.valueOrDefault,ut=W.global.defaultColor;W._set("global",{elements:{line:{tension:.4,backgroundColor:ut,borderWidth:3,borderColor:ut,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var dt=$.extend({_type:"line",draw:function(){var t,e,n,i=this,a=i._view,r=i._chart.ctx,o=a.spanGaps,s=i._children.slice(),l=W.global,u=l.elements.line,d=-1,h=i._loop;if(s.length){if(i._loop){for(t=0;t<s.length;++t)if(e=H.previousItem(s,t),!s[t]._view.skip&&e._view.skip){s=s.slice(t).concat(s.slice(0,t)),h=o;break}h&&s.push(s[0])}for(r.save(),r.lineCap=a.borderCapStyle||u.borderCapStyle,r.setLineDash&&r.setLineDash(a.borderDash||u.borderDash),r.lineDashOffset=lt(a.borderDashOffset,u.borderDashOffset),r.lineJoin=a.borderJoinStyle||u.borderJoinStyle,r.lineWidth=lt(a.borderWidth,u.borderWidth),r.strokeStyle=a.borderColor||l.defaultColor,r.beginPath(),(n=s[0]._view).skip||(r.moveTo(n.x,n.y),d=0),t=1;t<s.length;++t)n=s[t]._view,e=-1===d?H.previousItem(s,t):s[d],n.skip||(d!==t-1&&!o||-1===d?r.moveTo(n.x,n.y):H.canvas.lineTo(r,e._view,n),d=t);h&&r.closePath(),r.stroke(),r.restore()}}}),ht=H.valueOrDefault,ct=W.global.defaultColor;function ft(t){var e=this._view;return!!e&&Math.abs(t-e.x)<e.radius+e.hitRadius}W._set("global",{elements:{point:{radius:3,pointStyle:"circle",backgroundColor:ct,borderColor:ct,borderWidth:1,hitRadius:1,hoverRadius:4,hoverBorderWidth:1}}});var gt=$.extend({_type:"point",inRange:function(t,e){var n=this._view;return!!n&&Math.pow(t-n.x,2)+Math.pow(e-n.y,2)<Math.pow(n.hitRadius+n.radius,2)},inLabelRange:ft,inXRange:ft,inYRange:function(t){var e=this._view;return!!e&&Math.abs(t-e.y)<e.radius+e.hitRadius},getCenterPoint:function(){var t=this._view;return{x:t.x,y:t.y}},getArea:function(){return Math.PI*Math.pow(this._view.radius,2)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y,padding:t.radius+t.borderWidth}},draw:function(t){var e=this._view,n=this._chart.ctx,i=e.pointStyle,a=e.rotation,r=e.radius,o=e.x,s=e.y,l=W.global,u=l.defaultColor;e.skip||(void 0===t||H.canvas._isPointInArea(e,t))&&(n.strokeStyle=e.borderColor||u,n.lineWidth=ht(e.borderWidth,l.elements.point.borderWidth),n.fillStyle=e.backgroundColor||u,H.canvas.drawPoint(n,i,r,o,s,a))}}),mt=W.global.defaultColor;function pt(t){return t&&void 0!==t.width}function vt(t){var e,n,i,a,r;return pt(t)?(r=t.width/2,e=t.x-r,n=t.x+r,i=Math.min(t.y,t.base),a=Math.max(t.y,t.base)):(r=t.height/2,e=Math.min(t.x,t.base),n=Math.max(t.x,t.base),i=t.y-r,a=t.y+r),{left:e,top:i,right:n,bottom:a}}function bt(t,e,n){return t===e?n:t===n?e:t}function yt(t,e,n){var i,a,r,o,s=t.borderWidth,l=function(t){var e=t.borderSkipped,n={};return e?(t.horizontal?t.base>t.x&&(e=bt(e,"left","right")):t.base<t.y&&(e=bt(e,"bottom","top")),n[e]=!0,n):n}(t);return H.isObject(s)?(i=+s.top||0,a=+s.right||0,r=+s.bottom||0,o=+s.left||0):i=a=r=o=+s||0,{t:l.top||i<0?0:i>n?n:i,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>n?n:r,l:l.left||o<0?0:o>e?e:o}}function xt(t,e,n){var i=null===e,a=null===n,r=!(!t||i&&a)&&vt(t);return r&&(i||e>=r.left&&e<=r.right)&&(a||n>=r.top&&n<=r.bottom)}W._set("global",{elements:{rectangle:{backgroundColor:mt,borderColor:mt,borderSkipped:"bottom",borderWidth:0}}});var _t=$.extend({_type:"rectangle",draw:function(){var t=this._chart.ctx,e=this._view,n=function(t){var e=vt(t),n=e.right-e.left,i=e.bottom-e.top,a=yt(t,n/2,i/2);return{outer:{x:e.left,y:e.top,w:n,h:i},inner:{x:e.left+a.l,y:e.top+a.t,w:n-a.l-a.r,h:i-a.t-a.b}}}(e),i=n.outer,a=n.inner;t.fillStyle=e.backgroundColor,t.fillRect(i.x,i.y,i.w,i.h),i.w===a.w&&i.h===a.h||(t.save(),t.beginPath(),t.rect(i.x,i.y,i.w,i.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return xt(this._view,t,e)},inLabelRange:function(t,e){var n=this._view;return pt(n)?xt(n,t,null):xt(n,null,e)},inXRange:function(t){return xt(this._view,t,null)},inYRange:function(t){return xt(this._view,null,t)},getCenterPoint:function(){var t,e,n=this._view;return pt(n)?(t=n.x,e=(n.y+n.base)/2):(t=(n.x+n.base)/2,e=n.y),{x:t,y:e}},getArea:function(){var t=this._view;return pt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),wt={},kt=st,Mt=dt,St=gt,Dt=_t;wt.Arc=kt,wt.Line=Mt,wt.Point=St,wt.Rectangle=Dt;var Ct=H._deprecated,Pt=H.valueOrDefault;function Tt(t,e,n){var i,a,r=n.barThickness,o=e.stackCount,s=e.pixels[t],l=H.isNullOrUndef(r)?function(t,e){var n,i,a,r,o=t._length;for(a=1,r=e.length;a<r;++a)o=Math.min(o,Math.abs(e[a]-e[a-1]));for(a=0,r=t.getTicks().length;a<r;++a)i=t.getPixelForTick(a),o=a>0?Math.min(o,Math.abs(i-n)):o,n=i;return o}(e.scale,e.pixels):-1;return H.isNullOrUndef(r)?(i=l*n.categoryPercentage,a=n.barPercentage):(i=r*o,a=1),{chunk:i/o,ratio:a,start:s-i/2}}W._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),W._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var Ot=it.extend({dataElementType:wt.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,e,n=this;it.prototype.initialize.apply(n,arguments),(t=n.getMeta()).stack=n.getDataset().stack,t.bar=!0,e=n._getIndexScale().options,Ct("bar chart",e.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Ct("bar chart",e.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Ct("bar chart",e.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Ct("bar chart",n._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Ct("bar chart",e.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(t){var e,n,i=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,n=i.length;e<n;++e)this.updateElement(i[e],e,t)},updateElement:function(t,e,n){var i=this,a=i.getMeta(),r=i.getDataset(),o=i._resolveDataElementOptions(t,e);t._xScale=i.getScaleForId(a.xAxisID),t._yScale=i.getScaleForId(a.yAxisID),t._datasetIndex=i.index,t._index=e,t._model={backgroundColor:o.backgroundColor,borderColor:o.borderColor,borderSkipped:o.borderSkipped,borderWidth:o.borderWidth,datasetLabel:r.label,label:i.chart.data.labels[e]},H.isArray(r.data[e])&&(t._model.borderSkipped=null),i._updateElementGeometry(t,e,n,o),t.pivot()},_updateElementGeometry:function(t,e,n,i){var a=this,r=t._model,o=a._getValueScale(),s=o.getBasePixel(),l=o.isHorizontal(),u=a._ruler||a.getRuler(),d=a.calculateBarValuePixels(a.index,e,i),h=a.calculateBarIndexPixels(a.index,e,u,i);r.horizontal=l,r.base=n?s:d.base,r.x=l?n?s:d.head:h.center,r.y=l?h.center:n?s:d.head,r.height=l?h.size:void 0,r.width=l?void 0:h.size},_getStacks:function(t){var e,n,i=this._getIndexScale(),a=i._getMatchingVisibleMetas(this._type),r=i.options.stacked,o=a.length,s=[];for(e=0;e<o&&(n=a[e],(!1===r||-1===s.indexOf(n.stack)||void 0===r&&void 0===n.stack)&&s.push(n.stack),n.index!==t);++e);return s},getStackCount:function(){return this._getStacks().length},getStackIndex:function(t,e){var n=this._getStacks(t),i=void 0!==e?n.indexOf(e):-1;return-1===i?n.length-1:i},getRuler:function(){var t,e,n=this._getIndexScale(),i=[];for(t=0,e=this.getMeta().data.length;t<e;++t)i.push(n.getPixelForValue(null,t,this.index));return{pixels:i,start:n._startPixel,end:n._endPixel,stackCount:this.getStackCount(),scale:n}},calculateBarValuePixels:function(t,e,n){var i,a,r,o,s,l,u,d=this.chart,h=this._getValueScale(),c=h.isHorizontal(),f=d.data.datasets,g=h._getMatchingVisibleMetas(this._type),m=h._parseValue(f[t].data[e]),p=n.minBarLength,v=h.options.stacked,b=this.getMeta().stack,y=void 0===m.start?0:m.max>=0&&m.min>=0?m.min:m.max,x=void 0===m.start?m.end:m.max>=0&&m.min>=0?m.max-m.min:m.min-m.max,_=g.length;if(v||void 0===v&&void 0!==b)for(i=0;i<_&&(a=g[i]).index!==t;++i)a.stack===b&&(r=void 0===(u=h._parseValue(f[a.index].data[e])).start?u.end:u.min>=0&&u.max>=0?u.max:u.min,(m.min<0&&r<0||m.max>=0&&r>0)&&(y+=r));return o=h.getPixelForValue(y),l=(s=h.getPixelForValue(y+x))-o,void 0!==p&&Math.abs(l)<p&&(l=p,s=x>=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t<a.length-1?a[t+1]:null,l=n.categoryPercentage;return null===o&&(o=r-(null===s?e.end-e.start:s-r)),null===s&&(s=r+r-o),i=r-(r-Math.min(o,s))/2*l,{chunk:Math.abs(s-o)/2*l/e.stackCount,ratio:n.barPercentage,start:i}}(e,n,i):Tt(e,n,i),r=this.getStackIndex(t,this.getMeta().stack),o=a.start+a.chunk*r+a.chunk/2,s=Math.min(Pt(i.maxBarThickness,1/0),a.chunk*a.ratio);return{base:o-s/2,head:o+s/2,center:o,size:s}},draw:function(){var t=this.chart,e=this._getValueScale(),n=this.getMeta().data,i=this.getDataset(),a=n.length,r=0;for(H.canvas.clipArea(t.ctx,t.chartArea);r<a;++r){var o=e._parseValue(i.data[r]);isNaN(o.min)||isNaN(o.max)||n[r].draw()}H.canvas.unclipArea(t.ctx)},_resolveDataElementOptions:function(){var t=this,e=H.extend({},it.prototype._resolveDataElementOptions.apply(t,arguments)),n=t._getIndexScale().options,i=t._getValueScale().options;return e.barPercentage=Pt(n.barPercentage,e.barPercentage),e.barThickness=Pt(n.barThickness,e.barThickness),e.categoryPercentage=Pt(n.categoryPercentage,e.categoryPercentage),e.maxBarThickness=Pt(n.maxBarThickness,e.maxBarThickness),e.minBarLength=Pt(i.minBarLength,e.minBarLength),e}}),At=H.valueOrDefault,Ft=H.options.resolve;W._set("bubble",{hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(t,e){var n=e.datasets[t.datasetIndex].label||"",i=e.datasets[t.datasetIndex].data[t.index];return n+": ("+t.xLabel+", "+t.yLabel+", "+i.r+")"}}}});var It=it.extend({dataElementType:wt.Point,_dataElementOptions:["backgroundColor","borderColor","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","pointStyle","rotation"],update:function(t){var e=this,n=e.getMeta().data;H.each(n,(function(n,i){e.updateElement(n,i,t)}))},updateElement:function(t,e,n){var i=this,a=i.getMeta(),r=t.custom||{},o=i.getScaleForId(a.xAxisID),s=i.getScaleForId(a.yAxisID),l=i._resolveDataElementOptions(t,e),u=i.getDataset().data[e],d=i.index,h=n?o.getPixelForDecimal(.5):o.getPixelForValue("object"==typeof u?u:NaN,e,d),c=n?s.getBasePixel():s.getPixelForValue(u,e,d);t._xScale=o,t._yScale=s,t._options=l,t._datasetIndex=d,t._index=e,t._model={backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,hitRadius:l.hitRadius,pointStyle:l.pointStyle,rotation:l.rotation,radius:n?0:l.radius,skip:r.skip||isNaN(h)||isNaN(c),x:h,y:c},t.pivot()},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=At(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=At(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=At(n.hoverBorderWidth,n.borderWidth),e.radius=n.radius+n.hoverRadius},_resolveDataElementOptions:function(t,e){var n=this,i=n.chart,a=n.getDataset(),r=t.custom||{},o=a.data[e]||{},s=it.prototype._resolveDataElementOptions.apply(n,arguments),l={chart:i,dataIndex:e,dataset:a,datasetIndex:n.index};return n._cachedDataOpts===s&&(s=H.extend({},s)),s.radius=Ft([r.radius,o.r,n._config.radius,i.options.elements.point.radius],l,e),s}}),Lt=H.valueOrDefault,Rt=Math.PI,Nt=2*Rt,Wt=Rt/2;W._set("doughnut",{animation:{animateRotate:!0,animateScale:!1},hover:{mode:"single"},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data,o=r.datasets,s=r.labels;if(a.setAttribute("class",t.id+"-legend"),o.length)for(e=0,n=o[0].data.length;e<n;++e)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=o[0].backgroundColor[e],s[e]&&i.appendChild(document.createTextNode(s[e]));return a.outerHTML},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map((function(n,i){var a=t.getDatasetMeta(0),r=a.controller.getStyle(i);return{text:n,fillStyle:r.backgroundColor,strokeStyle:r.borderColor,lineWidth:r.borderWidth,hidden:isNaN(e.datasets[0].data[i])||a.data[i].hidden,index:i}})):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n<i;++n)(a=o.getDatasetMeta(n)).data[r]&&(a.data[r].hidden=!a.data[r].hidden);o.update()}},cutoutPercentage:50,rotation:-Wt,circumference:Nt,tooltips:{callbacks:{title:function(){return""},label:function(t,e){var n=e.labels[t.index],i=": "+e.datasets[t.datasetIndex].data[t.index];return H.isArray(n)?(n=n.slice())[0]+=i:n+=i,n}}}});var Yt=it.extend({dataElementType:wt.Arc,linkScales:H.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],getRingIndex:function(t){for(var e=0,n=0;n<t;++n)this.chart.isDatasetVisible(n)&&++e;return e},update:function(t){var e,n,i,a,r=this,o=r.chart,s=o.chartArea,l=o.options,u=1,d=1,h=0,c=0,f=r.getMeta(),g=f.data,m=l.cutoutPercentage/100||0,p=l.circumference,v=r._getRingWeight(r.index);if(p<Nt){var b=l.rotation%Nt,y=(b+=b>=Rt?-Nt:b<-Rt?Nt:0)+p,x=Math.cos(b),_=Math.sin(b),w=Math.cos(y),k=Math.sin(y),M=b<=0&&y>=0||y>=Nt,S=b<=Wt&&y>=Wt||y>=Nt+Wt,D=b<=-Wt&&y>=-Wt||y>=Rt+Wt,C=b===-Rt||y>=Rt?-1:Math.min(x,x*m,w,w*m),P=D?-1:Math.min(_,_*m,k,k*m),T=M?1:Math.max(x,x*m,w,w*m),O=S?1:Math.max(_,_*m,k,k*m);u=(T-C)/2,d=(O-P)/2,h=-(T+C)/2,c=-(O+P)/2}for(i=0,a=g.length;i<a;++i)g[i]._options=r._resolveDataElementOptions(g[i],i);for(o.borderWidth=r.getMaxBorderWidth(),e=(s.right-s.left-o.borderWidth)/u,n=(s.bottom-s.top-o.borderWidth)/d,o.outerRadius=Math.max(Math.min(e,n)/2,0),o.innerRadius=Math.max(o.outerRadius*m,0),o.radiusLength=(o.outerRadius-o.innerRadius)/(r._getVisibleDatasetWeightTotal()||1),o.offsetX=h*o.outerRadius,o.offsetY=c*o.outerRadius,f.total=r.calculateTotal(),r.outerRadius=o.outerRadius-o.radiusLength*r._getRingWeightOffset(r.index),r.innerRadius=Math.max(r.outerRadius-o.radiusLength*v,0),i=0,a=g.length;i<a;++i)r.updateElement(g[i],i,t)},updateElement:function(t,e,n){var i=this,a=i.chart,r=a.chartArea,o=a.options,s=o.animation,l=(r.left+r.right)/2,u=(r.top+r.bottom)/2,d=o.rotation,h=o.rotation,c=i.getDataset(),f=n&&s.animateRotate?0:t.hidden?0:i.calculateCircumference(c.data[e])*(o.circumference/Nt),g=n&&s.animateScale?0:i.innerRadius,m=n&&s.animateScale?0:i.outerRadius,p=t._options||{};H.extend(t,{_datasetIndex:i.index,_index:e,_model:{backgroundColor:p.backgroundColor,borderColor:p.borderColor,borderWidth:p.borderWidth,borderAlign:p.borderAlign,x:l+a.offsetX,y:u+a.offsetY,startAngle:d,endAngle:h,circumference:f,outerRadius:m,innerRadius:g,label:H.valueAtIndexOrDefault(c.label,e,a.data.labels[e])}});var v=t._model;n&&s.animateRotate||(v.startAngle=0===e?o.rotation:i.getMeta().data[e-1]._model.endAngle,v.endAngle=v.startAngle+v.circumference),t.pivot()},calculateTotal:function(){var t,e=this.getDataset(),n=this.getMeta(),i=0;return H.each(n.data,(function(n,a){t=e.data[a],isNaN(t)||n.hidden||(i+=Math.abs(t))})),i},calculateCircumference:function(t){var e=this.getMeta().total;return e>0&&!isNaN(t)?Nt*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,n,i,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,n=d.data.datasets.length;e<n;++e)if(d.isDatasetVisible(e)){t=(i=d.getDatasetMeta(e)).data,e!==this.index&&(r=i.controller);break}if(!t)return 0;for(e=0,n=t.length;e<n;++e)a=t[e],r?(r._configure(),o=r._resolveDataElementOptions(a,e)):o=a._options,"inner"!==o.borderAlign&&(s=o.borderWidth,u=(l=o.hoverBorderWidth)>(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Lt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Lt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Lt(n.hoverBorderWidth,n.borderWidth)},_getRingWeightOffset:function(t){for(var e=0,n=0;n<t;++n)this.chart.isDatasetVisible(n)&&(e+=this._getRingWeight(n));return e},_getRingWeight:function(t){return Math.max(Lt(this.chart.data.datasets[t].weight,1),0)},_getVisibleDatasetWeightTotal:function(){return this._getRingWeightOffset(this.chart.data.datasets.length)}});W._set("horizontalBar",{hover:{mode:"index",axis:"y"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{type:"category",position:"left",offset:!0,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{mode:"index",axis:"y"}}),W._set("global",{datasets:{horizontalBar:{categoryPercentage:.8,barPercentage:.9}}});var zt=Ot.extend({_getValueScaleId:function(){return this.getMeta().xAxisID},_getIndexScaleId:function(){return this.getMeta().yAxisID}}),Et=H.valueOrDefault,Vt=H.options.resolve,Ht=H.canvas._isPointInArea;function Bt(t,e){var n=t&&t.options.ticks||{},i=n.reverse,a=void 0===n.min?e:0,r=void 0===n.max?e:0;return{start:i?r:a,end:i?a:r}}function jt(t,e,n){var i=n/2,a=Bt(t,i),r=Bt(e,i);return{top:r.end,right:a.end,bottom:r.start,left:a.start}}function Ut(t){var e,n,i,a;return H.isObject(t)?(e=t.top,n=t.right,i=t.bottom,a=t.left):e=n=i=a=t,{top:e,right:n,bottom:i,left:a}}W._set("line",{showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}});var Gt=it.extend({datasetElementType:wt.Line,dataElementType:wt.Point,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth","cubicInterpolationMode","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},update:function(t){var e,n,i=this,a=i.getMeta(),r=a.dataset,o=a.data||[],s=i.chart.options,l=i._config,u=i._showLine=Et(l.showLine,s.showLines);for(i._xScale=i.getScaleForId(a.xAxisID),i._yScale=i.getScaleForId(a.yAxisID),u&&(void 0!==l.tension&&void 0===l.lineTension&&(l.lineTension=l.tension),r._scale=i._yScale,r._datasetIndex=i.index,r._children=o,r._model=i._resolveDatasetElementOptions(r),r.pivot()),e=0,n=o.length;e<n;++e)i.updateElement(o[e],e,t);for(u&&0!==r._model.tension&&i.updateBezierControlPoints(),e=0,n=o.length;e<n;++e)o[e].pivot()},updateElement:function(t,e,n){var i,a,r=this,o=r.getMeta(),s=t.custom||{},l=r.getDataset(),u=r.index,d=l.data[e],h=r._xScale,c=r._yScale,f=o.dataset._model,g=r._resolveDataElementOptions(t,e);i=h.getPixelForValue("object"==typeof d?d:NaN,e,u),a=n?c.getBasePixel():r.calculatePointY(d,e,u),t._xScale=h,t._yScale=c,t._options=g,t._datasetIndex=u,t._index=e,t._model={x:i,y:a,skip:s.skip||isNaN(i)||isNaN(a),radius:g.radius,pointStyle:g.pointStyle,rotation:g.rotation,backgroundColor:g.backgroundColor,borderColor:g.borderColor,borderWidth:g.borderWidth,tension:Et(s.tension,f?f.tension:0),steppedLine:!!f&&f.steppedLine,hitRadius:g.hitRadius}},_resolveDatasetElementOptions:function(t){var e=this,n=e._config,i=t.custom||{},a=e.chart.options,r=a.elements.line,o=it.prototype._resolveDatasetElementOptions.apply(e,arguments);return o.spanGaps=Et(n.spanGaps,a.spanGaps),o.tension=Et(n.lineTension,r.tension),o.steppedLine=Vt([i.steppedLine,n.steppedLine,r.stepped]),o.clip=Ut(Et(n.clip,jt(e._xScale,e._yScale,o.borderWidth))),o},calculatePointY:function(t,e,n){var i,a,r,o,s,l,u,d=this.chart,h=this._yScale,c=0,f=0;if(h.options.stacked){for(s=+h.getRightValue(t),u=(l=d._getSortedVisibleDatasetMetas()).length,i=0;i<u&&(r=l[i]).index!==n;++i)a=d.data.datasets[r.index],"line"===r.type&&r.yAxisID===h.id&&((o=+h.getRightValue(a.data[e]))<0?f+=o||0:c+=o||0);return s<0?h.getPixelForValue(f+s):h.getPixelForValue(c+s)}return h.getPixelForValue(t)},updateBezierControlPoints:function(){var t,e,n,i,a=this.chart,r=this.getMeta(),o=r.dataset._model,s=a.chartArea,l=r.data||[];function u(t,e,n){return Math.max(Math.min(t,n),e)}if(o.spanGaps&&(l=l.filter((function(t){return!t._model.skip}))),"monotone"===o.cubicInterpolationMode)H.splineCurveMonotone(l);else for(t=0,e=l.length;t<e;++t)n=l[t]._model,i=H.splineCurve(H.previousItem(l,t)._model,n,H.nextItem(l,t)._model,o.tension),n.controlPointPreviousX=i.previous.x,n.controlPointPreviousY=i.previous.y,n.controlPointNextX=i.next.x,n.controlPointNextY=i.next.y;if(a.options.elements.line.capBezierPoints)for(t=0,e=l.length;t<e;++t)n=l[t]._model,Ht(n,s)&&(t>0&&Ht(l[t-1]._model,s)&&(n.controlPointPreviousX=u(n.controlPointPreviousX,s.left,s.right),n.controlPointPreviousY=u(n.controlPointPreviousY,s.top,s.bottom)),t<l.length-1&&Ht(l[t+1]._model,s)&&(n.controlPointNextX=u(n.controlPointNextX,s.left,s.right),n.controlPointNextY=u(n.controlPointNextY,s.top,s.bottom)))},draw:function(){var t,e=this.chart,n=this.getMeta(),i=n.data||[],a=e.chartArea,r=e.canvas,o=0,s=i.length;for(this._showLine&&(t=n.dataset._model.clip,H.canvas.clipArea(e.ctx,{left:!1===t.left?0:a.left-t.left,right:!1===t.right?r.width:a.right+t.right,top:!1===t.top?0:a.top-t.top,bottom:!1===t.bottom?r.height:a.bottom+t.bottom}),n.dataset.draw(),H.canvas.unclipArea(e.ctx));o<s;++o)i[o].draw(a)},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=Et(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Et(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Et(n.hoverBorderWidth,n.borderWidth),e.radius=Et(n.hoverRadius,n.radius)}}),qt=H.options.resolve;W._set("polarArea",{scale:{type:"radialLinear",angleLines:{display:!1},gridLines:{circular:!0},pointLabels:{display:!1},ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data,o=r.datasets,s=r.labels;if(a.setAttribute("class",t.id+"-legend"),o.length)for(e=0,n=o[0].data.length;e<n;++e)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=o[0].backgroundColor[e],s[e]&&i.appendChild(document.createTextNode(s[e]));return a.outerHTML},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map((function(n,i){var a=t.getDatasetMeta(0),r=a.controller.getStyle(i);return{text:n,fillStyle:r.backgroundColor,strokeStyle:r.borderColor,lineWidth:r.borderWidth,hidden:isNaN(e.datasets[0].data[i])||a.data[i].hidden,index:i}})):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n<i;++n)(a=o.getDatasetMeta(n)).data[r].hidden=!a.data[r].hidden;o.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}});var Zt=it.extend({dataElementType:wt.Arc,linkScales:H.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(t){var e,n,i,a=this,r=a.getDataset(),o=a.getMeta(),s=a.chart.options.startAngle||0,l=a._starts=[],u=a._angles=[],d=o.data;for(a._updateRadius(),o.count=a.countVisibleElements(),e=0,n=r.data.length;e<n;e++)l[e]=s,i=a._computeAngle(e),u[e]=i,s+=i;for(e=0,n=d.length;e<n;++e)d[e]._options=a._resolveDataElementOptions(d[e],e),a.updateElement(d[e],e,t)},_updateRadius:function(){var t=this,e=t.chart,n=e.chartArea,i=e.options,a=Math.min(n.right-n.left,n.bottom-n.top);e.outerRadius=Math.max(a/2,0),e.innerRadius=Math.max(i.cutoutPercentage?e.outerRadius/100*i.cutoutPercentage:1,0),e.radiusLength=(e.outerRadius-e.innerRadius)/e.getVisibleDatasetCount(),t.outerRadius=e.outerRadius-e.radiusLength*t.index,t.innerRadius=t.outerRadius-e.radiusLength},updateElement:function(t,e,n){var i=this,a=i.chart,r=i.getDataset(),o=a.options,s=o.animation,l=a.scale,u=a.data.labels,d=l.xCenter,h=l.yCenter,c=o.startAngle,f=t.hidden?0:l.getDistanceFromCenterForValue(r.data[e]),g=i._starts[e],m=g+(t.hidden?0:i._angles[e]),p=s.animateScale?0:l.getDistanceFromCenterForValue(r.data[e]),v=t._options||{};H.extend(t,{_datasetIndex:i.index,_index:e,_scale:l,_model:{backgroundColor:v.backgroundColor,borderColor:v.borderColor,borderWidth:v.borderWidth,borderAlign:v.borderAlign,x:d,y:h,innerRadius:0,outerRadius:n?p:f,startAngle:n&&s.animateRotate?c:g,endAngle:n&&s.animateRotate?c:m,label:H.valueAtIndexOrDefault(u,e,u[e])}}),t.pivot()},countVisibleElements:function(){var t=this.getDataset(),e=this.getMeta(),n=0;return H.each(e.data,(function(e,i){isNaN(t.data[i])||e.hidden||n++})),n},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor,a=H.valueOrDefault;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=a(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=a(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=a(n.hoverBorderWidth,n.borderWidth)},_computeAngle:function(t){var e=this,n=this.getMeta().count,i=e.getDataset(),a=e.getMeta();if(isNaN(i.data[t])||a.data[t].hidden)return 0;var r={chart:e.chart,dataIndex:t,dataset:i,datasetIndex:e.index};return qt([e.chart.options.elements.arc.angle,2*Math.PI/n],r,t)}});W._set("pie",H.clone(W.doughnut)),W._set("pie",{cutoutPercentage:0});var $t=Yt,Xt=H.valueOrDefault;W._set("radar",{spanGaps:!1,scale:{type:"radialLinear"},elements:{line:{fill:"start",tension:0}}});var Kt=it.extend({datasetElementType:wt.Line,dataElementType:wt.Point,linkScales:H.noop,_datasetElementOptions:["backgroundColor","borderWidth","borderColor","borderCapStyle","borderDash","borderDashOffset","borderJoinStyle","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(t){var e,n,i=this,a=i.getMeta(),r=a.dataset,o=a.data||[],s=i.chart.scale,l=i._config;for(void 0!==l.tension&&void 0===l.lineTension&&(l.lineTension=l.tension),r._scale=s,r._datasetIndex=i.index,r._children=o,r._loop=!0,r._model=i._resolveDatasetElementOptions(r),r.pivot(),e=0,n=o.length;e<n;++e)i.updateElement(o[e],e,t);for(i.updateBezierControlPoints(),e=0,n=o.length;e<n;++e)o[e].pivot()},updateElement:function(t,e,n){var i=this,a=t.custom||{},r=i.getDataset(),o=i.chart.scale,s=o.getPointPositionForValue(e,r.data[e]),l=i._resolveDataElementOptions(t,e),u=i.getMeta().dataset._model,d=n?o.xCenter:s.x,h=n?o.yCenter:s.y;t._scale=o,t._options=l,t._datasetIndex=i.index,t._index=e,t._model={x:d,y:h,skip:a.skip||isNaN(d)||isNaN(h),radius:l.radius,pointStyle:l.pointStyle,rotation:l.rotation,backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,tension:Xt(a.tension,u?u.tension:0),hitRadius:l.hitRadius}},_resolveDatasetElementOptions:function(){var t=this,e=t._config,n=t.chart.options,i=it.prototype._resolveDatasetElementOptions.apply(t,arguments);return i.spanGaps=Xt(e.spanGaps,n.spanGaps),i.tension=Xt(e.lineTension,n.elements.line.tension),i},updateBezierControlPoints:function(){var t,e,n,i,a=this.getMeta(),r=this.chart.chartArea,o=a.data||[];function s(t,e,n){return Math.max(Math.min(t,n),e)}for(a.dataset._model.spanGaps&&(o=o.filter((function(t){return!t._model.skip}))),t=0,e=o.length;t<e;++t)n=o[t]._model,i=H.splineCurve(H.previousItem(o,t,!0)._model,n,H.nextItem(o,t,!0)._model,n.tension),n.controlPointPreviousX=s(i.previous.x,r.left,r.right),n.controlPointPreviousY=s(i.previous.y,r.top,r.bottom),n.controlPointNextX=s(i.next.x,r.left,r.right),n.controlPointNextY=s(i.next.y,r.top,r.bottom)},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=Xt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Xt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Xt(n.hoverBorderWidth,n.borderWidth),e.radius=Xt(n.hoverRadius,n.radius)}});W._set("scatter",{hover:{mode:"single"},scales:{xAxes:[{id:"x-axis-1",type:"linear",position:"bottom"}],yAxes:[{id:"y-axis-1",type:"linear",position:"left"}]},tooltips:{callbacks:{title:function(){return""},label:function(t){return"("+t.xLabel+", "+t.yLabel+")"}}}}),W._set("global",{datasets:{scatter:{showLine:!1}}});var Jt={bar:Ot,bubble:It,doughnut:Yt,horizontalBar:zt,line:Gt,polarArea:Zt,pie:$t,radar:Kt,scatter:Gt};function Qt(t,e){return t.native?{x:t.x,y:t.y}:H.getRelativePosition(t,e)}function te(t,e){var n,i,a,r,o,s,l=t._getSortedVisibleDatasetMetas();for(i=0,r=l.length;i<r;++i)for(a=0,o=(n=l[i].data).length;a<o;++a)(s=n[a])._view.skip||e(s)}function ee(t,e){var n=[];return te(t,(function(t){t.inRange(e.x,e.y)&&n.push(t)})),n}function ne(t,e,n,i){var a=Number.POSITIVE_INFINITY,r=[];return te(t,(function(t){if(!n||t.inRange(e.x,e.y)){var o=t.getCenterPoint(),s=i(e,o);s<a?(r=[t],a=s):s===a&&r.push(t)}})),r}function ie(t){var e=-1!==t.indexOf("x"),n=-1!==t.indexOf("y");return function(t,i){var a=e?Math.abs(t.x-i.x):0,r=n?Math.abs(t.y-i.y):0;return Math.sqrt(Math.pow(a,2)+Math.pow(r,2))}}function ae(t,e,n){var i=Qt(e,t);n.axis=n.axis||"x";var a=ie(n.axis),r=n.intersect?ee(t,i):ne(t,i,!1,a),o=[];return r.length?(t._getSortedVisibleDatasetMetas().forEach((function(t){var e=t.data[r[0]._index];e&&!e._view.skip&&o.push(e)})),o):[]}var re={modes:{single:function(t,e){var n=Qt(e,t),i=[];return te(t,(function(t){if(t.inRange(n.x,n.y))return i.push(t),i})),i.slice(0,1)},label:ae,index:ae,dataset:function(t,e,n){var i=Qt(e,t);n.axis=n.axis||"xy";var a=ie(n.axis),r=n.intersect?ee(t,i):ne(t,i,!1,a);return r.length>0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return ae(t,e,{intersect:!1})},point:function(t,e){return ee(t,Qt(e,t))},nearest:function(t,e,n){var i=Qt(e,t);n.axis=n.axis||"xy";var a=ie(n.axis);return ne(t,i,n.intersect,a)},x:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inXRange(i.x)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a},y:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inYRange(i.y)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a}}},oe=H.extend;function se(t,e){return H.where(t,(function(t){return t.pos===e}))}function le(t,e){return t.sort((function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i.index-a.index:i.weight-a.weight}))}function ue(t,e,n,i){return Math.max(t[n],e[n])+Math.max(t[i],e[i])}function de(t,e,n){var i,a,r=n.box,o=t.maxPadding;if(n.size&&(t[n.pos]-=n.size),n.size=n.horizontal?r.height:r.width,t[n.pos]+=n.size,r.getPadding){var s=r.getPadding();o.top=Math.max(o.top,s.top),o.left=Math.max(o.left,s.left),o.bottom=Math.max(o.bottom,s.bottom),o.right=Math.max(o.right,s.right)}if(i=e.outerWidth-ue(o,t,"left","right"),a=e.outerHeight-ue(o,t,"top","bottom"),i!==t.w||a!==t.h)return t.w=i,t.h=a,n.horizontal?i!==t.w:a!==t.h}function he(t,e){var n=e.maxPadding;function i(t){var i={left:0,top:0,right:0,bottom:0};return t.forEach((function(t){i[t]=Math.max(e[t],n[t])})),i}return i(t?["left","right"]:["top","bottom"])}function ce(t,e,n){var i,a,r,o,s,l,u=[];for(i=0,a=t.length;i<a;++i)(o=(r=t[i]).box).update(r.width||e.w,r.height||e.h,he(r.horizontal,e)),de(e,n,r)&&(l=!0,u.length&&(s=!0)),o.fullWidth||u.push(r);return s&&ce(u,e,n)||l}function fe(t,e,n){var i,a,r,o,s=n.padding,l=e.x,u=e.y;for(i=0,a=t.length;i<a;++i)o=(r=t[i]).box,r.horizontal?(o.left=o.fullWidth?s.left:e.left,o.right=o.fullWidth?n.outerWidth-s.right:e.left+e.w,o.top=u,o.bottom=u+o.height,o.width=o.right-o.left,u=o.bottom):(o.left=l,o.right=l+o.width,o.top=e.top,o.bottom=e.top+e.h,o.height=o.bottom-o.top,l=o.right);e.x=l,e.y=u}W._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var ge,me={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,e._layers=e._layers||function(){return[{z:0,draw:function(){e.draw.apply(e,arguments)}}]},t.boxes.push(e)},removeBox:function(t,e){var n=t.boxes?t.boxes.indexOf(e):-1;-1!==n&&t.boxes.splice(n,1)},configure:function(t,e,n){for(var i,a=["fullWidth","position","weight"],r=a.length,o=0;o<r;++o)i=a[o],n.hasOwnProperty(i)&&(e[i]=n[i])},update:function(t,e,n){if(t){var i=t.options.layout||{},a=H.options.toPadding(i.padding),r=e-a.width,o=n-a.height,s=function(t){var e=function(t){var e,n,i,a=[];for(e=0,n=(t||[]).length;e<n;++e)i=t[e],a.push({index:e,box:i,pos:i.position,horizontal:i.isHorizontal(),weight:i.weight});return a}(t),n=le(se(e,"left"),!0),i=le(se(e,"right")),a=le(se(e,"top"),!0),r=le(se(e,"bottom"));return{leftAndTop:n.concat(a),rightAndBottom:i.concat(r),chartArea:se(e,"chartArea"),vertical:n.concat(i),horizontal:a.concat(r)}}(t.boxes),l=s.vertical,u=s.horizontal,d=Object.freeze({outerWidth:e,outerHeight:n,padding:a,availableWidth:r,vBoxMaxWidth:r/2/l.length,hBoxMaxHeight:o/2}),h=oe({maxPadding:oe({},a),w:r,h:o,x:a.left,y:a.top},a);!function(t,e){var n,i,a;for(n=0,i=t.length;n<i;++n)(a=t[n]).width=a.horizontal?a.box.fullWidth&&e.availableWidth:e.vBoxMaxWidth,a.height=a.horizontal&&e.hBoxMaxHeight}(l.concat(u),d),ce(l,h,d),ce(u,h,d)&&ce(l,h,d),function(t){var e=t.maxPadding;function n(n){var i=Math.max(e[n]-t[n],0);return t[n]+=i,i}t.y+=n("top"),t.x+=n("left"),n("right"),n("bottom")}(h),fe(s.leftAndTop,h,d),h.x+=h.w,h.y+=h.h,fe(s.rightAndBottom,h,d),t.chartArea={left:h.left,top:h.top,right:h.left+h.w,bottom:h.top+h.h},H.each(s.chartArea,(function(e){var n=e.box;oe(n,t.chartArea),n.update(h.w,h.h)}))}}},pe=(ge=Object.freeze({__proto__:null,default:"@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ge.default||ge,ve="$chartjs",be="chartjs-size-monitor",ye="chartjs-render-monitor",xe="chartjs-render-animation",_e=["animationstart","webkitAnimationStart"],we={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function ke(t,e){var n=H.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}var Me=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Se(t,e,n){t.addEventListener(e,n,Me)}function De(t,e,n){t.removeEventListener(e,n,Me)}function Ce(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function Pe(t){var e=document.createElement("div");return e.className=t||"",e}function Te(t,e,n){var i,a,r,o,s=t[ve]||(t[ve]={}),l=s.resizer=function(t){var e=Pe(be),n=Pe(be+"-expand"),i=Pe(be+"-shrink");n.appendChild(Pe()),i.appendChild(Pe()),e.appendChild(n),e.appendChild(i),e._reset=function(){n.scrollLeft=1e6,n.scrollTop=1e6,i.scrollLeft=1e6,i.scrollTop=1e6};var a=function(){e._reset(),t()};return Se(n,"scroll",a.bind(n,"expand")),Se(i,"scroll",a.bind(i,"shrink")),e}((i=function(){if(s.resizer){var i=n.options.maintainAspectRatio&&t.parentNode,a=i?i.clientWidth:0;e(Ce("resize",n)),i&&i.clientWidth<a&&n.canvas&&e(Ce("resize",n))}},r=!1,o=[],function(){o=Array.prototype.slice.call(arguments),a=a||this,r||(r=!0,H.requestAnimFrame.call(window,(function(){r=!1,i.apply(a,o)})))}));!function(t,e){var n=t[ve]||(t[ve]={}),i=n.renderProxy=function(t){t.animationName===xe&&e()};H.each(_e,(function(e){Se(t,e,i)})),n.reflow=!!t.offsetParent,t.classList.add(ye)}(t,(function(){if(s.resizer){var e=t.parentNode;e&&e!==l.parentNode&&e.insertBefore(l,e.firstChild),l._reset()}}))}function Oe(t){var e=t[ve]||{},n=e.resizer;delete e.resizer,function(t){var e=t[ve]||{},n=e.renderProxy;n&&(H.each(_e,(function(e){De(t,e,n)})),delete e.renderProxy),t.classList.remove(ye)}(t),n&&n.parentNode&&n.parentNode.removeChild(n)}var Ae={disableCSSInjection:!1,_enabled:"undefined"!=typeof window&&"undefined"!=typeof document,_ensureLoaded:function(t){if(!this.disableCSSInjection){var e=t.getRootNode?t.getRootNode():document;!function(t,e){var n=t[ve]||(t[ve]={});if(!n.containsStyles){n.containsStyles=!0,e="/* Chart.js */\n"+e;var i=document.createElement("style");i.setAttribute("type","text/css"),i.appendChild(document.createTextNode(e)),t.appendChild(i)}}(e.host?e:document.head,pe)}},acquireContext:function(t,e){"string"==typeof t?t=document.getElementById(t):t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas);var n=t&&t.getContext&&t.getContext("2d");return n&&n.canvas===t?(this._ensureLoaded(t),function(t,e){var n=t.style,i=t.getAttribute("height"),a=t.getAttribute("width");if(t[ve]={initial:{height:i,width:a,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||"block",null===a||""===a){var r=ke(t,"width");void 0!==r&&(t.width=r)}if(null===i||""===i)if(""===t.style.height)t.height=t.width/(e.options.aspectRatio||2);else{var o=ke(t,"height");void 0!==r&&(t.height=o)}}(t,e),n):null},releaseContext:function(t){var e=t.canvas;if(e[ve]){var n=e[ve].initial;["height","width"].forEach((function(t){var i=n[t];H.isNullOrUndef(i)?e.removeAttribute(t):e.setAttribute(t,i)})),H.each(n.style||{},(function(t,n){e.style[n]=t})),e.width=e.width,delete e[ve]}},addEventListener:function(t,e,n){var i=t.canvas;if("resize"!==e){var a=n[ve]||(n[ve]={});Se(i,e,(a.proxies||(a.proxies={}))[t.id+"_"+e]=function(e){n(function(t,e){var n=we[t.type]||t.type,i=H.getRelativePosition(t,e);return Ce(n,e,i.x,i.y,t)}(e,t))})}else Te(i,n,t)},removeEventListener:function(t,e,n){var i=t.canvas;if("resize"!==e){var a=((n[ve]||{}).proxies||{})[t.id+"_"+e];a&&De(i,e,a)}else Oe(i)}};H.addEvent=Se,H.removeEvent=De;var Fe=Ae._enabled?Ae:{acquireContext:function(t){return t&&t.canvas&&(t=t.canvas),t&&t.getContext("2d")||null}},Ie=H.extend({initialize:function(){},acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},Fe);W._set("global",{plugins:{}});var Le={_plugins:[],_cacheId:0,register:function(t){var e=this._plugins;[].concat(t).forEach((function(t){-1===e.indexOf(t)&&e.push(t)})),this._cacheId++},unregister:function(t){var e=this._plugins;[].concat(t).forEach((function(t){var n=e.indexOf(t);-1!==n&&e.splice(n,1)})),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e,n){var i,a,r,o,s,l=this.descriptors(t),u=l.length;for(i=0;i<u;++i)if("function"==typeof(s=(r=(a=l[i]).plugin)[e])&&((o=[t].concat(n||[])).push(a.options),!1===s.apply(r,o)))return!1;return!0},descriptors:function(t){var e=t.$plugins||(t.$plugins={});if(e.id===this._cacheId)return e.descriptors;var n=[],i=[],a=t&&t.config||{},r=a.options&&a.options.plugins||{};return this._plugins.concat(a.plugins||[]).forEach((function(t){if(-1===n.indexOf(t)){var e=t.id,a=r[e];!1!==a&&(!0===a&&(a=H.clone(W.global.plugins[e])),n.push(t),i.push({plugin:t,options:a||{}}))}})),e.descriptors=i,e.id=this._cacheId,i},_invalidate:function(t){delete t.$plugins}},Re={constructors:{},defaults:{},registerScaleType:function(t,e,n){this.constructors[t]=e,this.defaults[t]=H.clone(n)},getScaleConstructor:function(t){return this.constructors.hasOwnProperty(t)?this.constructors[t]:void 0},getScaleDefaults:function(t){return this.defaults.hasOwnProperty(t)?H.merge({},[W.scale,this.defaults[t]]):{}},updateScaleDefaults:function(t,e){this.defaults.hasOwnProperty(t)&&(this.defaults[t]=H.extend(this.defaults[t],e))},addScalesToLayout:function(t){H.each(t.scales,(function(e){e.fullWidth=e.options.fullWidth,e.position=e.options.position,e.weight=e.options.weight,me.addBox(t,e)}))}},Ne=H.valueOrDefault,We=H.rtl.getRtlAdapter;W._set("global",{tooltips:{enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:H.noop,title:function(t,e){var n="",i=e.labels,a=i?i.length:0;if(t.length>0){var r=t[0];r.label?n=r.label:r.xLabel?n=r.xLabel:a>0&&r.index<a&&(n=i[r.index])}return n},afterTitle:H.noop,beforeBody:H.noop,beforeLabel:H.noop,label:function(t,e){var n=e.datasets[t.datasetIndex].label||"";return n&&(n+=": "),H.isNullOrUndef(t.value)?n+=t.yLabel:n+=t.value,n},labelColor:function(t,e){var n=e.getDatasetMeta(t.datasetIndex).data[t.index]._view;return{borderColor:n.borderColor,backgroundColor:n.backgroundColor}},labelTextColor:function(){return this._options.bodyFontColor},afterLabel:H.noop,afterBody:H.noop,beforeFooter:H.noop,footer:H.noop,afterFooter:H.noop}}});var Ye={average:function(t){if(!t.length)return!1;var e,n,i=0,a=0,r=0;for(e=0,n=t.length;e<n;++e){var o=t[e];if(o&&o.hasValue()){var s=o.tooltipPosition();i+=s.x,a+=s.y,++r}}return{x:i/r,y:a/r}},nearest:function(t,e){var n,i,a,r=e.x,o=e.y,s=Number.POSITIVE_INFINITY;for(n=0,i=t.length;n<i;++n){var l=t[n];if(l&&l.hasValue()){var u=l.getCenterPoint(),d=H.distanceBetweenPoints(e,u);d<s&&(s=d,a=l)}}if(a){var h=a.tooltipPosition();r=h.x,o=h.y}return{x:r,y:o}}};function ze(t,e){return e&&(H.isArray(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function Ee(t){return("string"==typeof t||t instanceof String)&&t.indexOf("\n")>-1?t.split("\n"):t}function Ve(t){var e=W.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,rtl:t.rtl,textDirection:t.textDirection,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Ne(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Ne(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Ne(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Ne(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Ne(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Ne(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Ne(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Ne(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Ne(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function He(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function Be(t){return ze([],Ee(t))}var je=$.extend({initialize:function(){this._model=Ve(this._options),this._lastActive=[]},getTitle:function(){var t=this,e=t._options,n=e.callbacks,i=n.beforeTitle.apply(t,arguments),a=n.title.apply(t,arguments),r=n.afterTitle.apply(t,arguments),o=[];return o=ze(o,Ee(i)),o=ze(o,Ee(a)),o=ze(o,Ee(r))},getBeforeBody:function(){return Be(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var n=this,i=n._options.callbacks,a=[];return H.each(t,(function(t){var r={before:[],lines:[],after:[]};ze(r.before,Ee(i.beforeLabel.call(n,t,e))),ze(r.lines,i.label.call(n,t,e)),ze(r.after,Ee(i.afterLabel.call(n,t,e))),a.push(r)})),a},getAfterBody:function(){return Be(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this,e=t._options.callbacks,n=e.beforeFooter.apply(t,arguments),i=e.footer.apply(t,arguments),a=e.afterFooter.apply(t,arguments),r=[];return r=ze(r,Ee(n)),r=ze(r,Ee(i)),r=ze(r,Ee(a))},update:function(t){var e,n,i,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=Ve(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},y={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],w=[];x=Ye[c.position].call(h,m,h._eventPosition);var k=[];for(e=0,n=m.length;e<n;++e)k.push((i=m[e],a=void 0,r=void 0,o=void 0,s=void 0,l=void 0,u=void 0,d=void 0,a=i._xScale,r=i._yScale||i._scale,o=i._index,s=i._datasetIndex,l=i._chart.getDatasetMeta(s).controller,u=l._getIndexScale(),d=l._getValueScale(),{xLabel:a?a.getLabelForIndex(o,s):"",yLabel:r?r.getLabelForIndex(o,s):"",label:u?""+u.getLabelForIndex(o,s):"",value:d?""+d.getLabelForIndex(o,s):"",index:o,datasetIndex:s,x:i._model.x,y:i._model.y}));c.filter&&(k=k.filter((function(t){return c.filter(t,p)}))),c.itemSort&&(k=k.sort((function(t,e){return c.itemSort(t,e,p)}))),H.each(k,(function(t){_.push(c.callbacks.labelColor.call(h,t,h._chart)),w.push(c.callbacks.labelTextColor.call(h,t,h._chart))})),g.title=h.getTitle(k,p),g.beforeBody=h.getBeforeBody(k,p),g.body=h.getBody(k,p),g.afterBody=h.getAfterBody(k,p),g.footer=h.getFooter(k,p),g.x=x.x,g.y=x.y,g.caretPadding=c.caretPadding,g.labelColors=_,g.labelTextColors=w,g.dataPoints=k,y=function(t,e){var n=t._chart.ctx,i=2*e.yPadding,a=0,r=e.body,o=r.reduce((function(t,e){return t+e.before.length+e.lines.length+e.after.length}),0);o+=e.beforeBody.length+e.afterBody.length;var s=e.title.length,l=e.footer.length,u=e.titleFontSize,d=e.bodyFontSize,h=e.footerFontSize;i+=s*u,i+=s?(s-1)*e.titleSpacing:0,i+=s?e.titleMarginBottom:0,i+=o*d,i+=o?(o-1)*e.bodySpacing:0,i+=l?e.footerMarginTop:0,i+=l*h,i+=l?(l-1)*e.footerSpacing:0;var c=0,f=function(t){a=Math.max(a,n.measureText(t).width+c)};return n.font=H.fontString(u,e._titleFontStyle,e._titleFontFamily),H.each(e.title,f),n.font=H.fontString(d,e._bodyFontStyle,e._bodyFontFamily),H.each(e.beforeBody.concat(e.afterBody),f),c=e.displayColors?d+2:0,H.each(r,(function(t){H.each(t.before,f),H.each(t.lines,f),H.each(t.after,f)})),c=0,n.font=H.fontString(h,e._footerFontStyle,e._footerFontFamily),H.each(e.footer,f),{width:a+=2*e.xPadding,height:i}}(this,g),b=function(t,e,n,i){var a=t.x,r=t.y,o=t.caretSize,s=t.caretPadding,l=t.cornerRadius,u=n.xAlign,d=n.yAlign,h=o+s,c=l+s;return"right"===u?a-=e.width:"center"===u&&((a-=e.width/2)+e.width>i.width&&(a=i.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,y,v=function(t,e){var n,i,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.y<e.height?h="top":s.y>l.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(n=function(t){return t<=c},i=function(t){return t>c}):(n=function(t){return t<=e.width/2},i=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},n(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):i(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,y),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=y.width,g.height=y.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var n=this._chart.ctx,i=this._view,a=this.getCaretPosition(t,e,i);n.lineTo(a.x1,a.y1),n.lineTo(a.x2,a.y2),n.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,n){var i,a,r,o,s,l,u=n.caretSize,d=n.cornerRadius,h=n.xAlign,c=n.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(i=f)-u,r=i,o=s+u,l=s-u):(a=(i=f+m)+u,r=i,o=s-u,l=s+u);else if("left"===h?(i=(a=f+d+u)-u,r=a+u):"right"===h?(i=(a=f+m-d-u)-u,r=a+u):(i=(a=n.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=i,i=v}return{x1:i,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,n){var i,a,r,o=e.title,s=o.length;if(s){var l=We(e.rtl,e.x,e.width);for(t.x=He(e,e._titleAlign),n.textAlign=l.textAlign(e._titleAlign),n.textBaseline="middle",i=e.titleFontSize,a=e.titleSpacing,n.fillStyle=e.titleFontColor,n.font=H.fontString(i,e._titleFontStyle,e._titleFontFamily),r=0;r<s;++r)n.fillText(o[r],l.x(t.x),t.y+i/2),t.y+=i+a,r+1===s&&(t.y+=e.titleMarginBottom-a)}},drawBody:function(t,e,n){var i,a,r,o,s,l,u,d,h=e.bodyFontSize,c=e.bodySpacing,f=e._bodyAlign,g=e.body,m=e.displayColors,p=0,v=m?He(e,"left"):0,b=We(e.rtl,e.x,e.width),y=function(e){n.fillText(e,b.x(t.x+p),t.y+h/2),t.y+=h+c},x=b.textAlign(f);for(n.textAlign=f,n.textBaseline="middle",n.font=H.fontString(h,e._bodyFontStyle,e._bodyFontFamily),t.x=He(e,x),n.fillStyle=e.bodyFontColor,H.each(e.beforeBody,y),p=m&&"right"!==x?"center"===f?h/2+1:h+2:0,s=0,u=g.length;s<u;++s){for(i=g[s],a=e.labelTextColors[s],r=e.labelColors[s],n.fillStyle=a,H.each(i.before,y),l=0,d=(o=i.lines).length;l<d;++l){if(m){var _=b.x(v);n.fillStyle=e.legendColorBackground,n.fillRect(b.leftForLtr(_,h),t.y,h,h),n.lineWidth=1,n.strokeStyle=r.borderColor,n.strokeRect(b.leftForLtr(_,h),t.y,h,h),n.fillStyle=r.backgroundColor,n.fillRect(b.leftForLtr(b.xPlus(_,1),h-2),t.y+1,h-2,h-2),n.fillStyle=a}y(o[l])}H.each(i.after,y)}p=0,H.each(e.afterBody,y),t.y-=c},drawFooter:function(t,e,n){var i,a,r=e.footer,o=r.length;if(o){var s=We(e.rtl,e.x,e.width);for(t.x=He(e,e._footerAlign),t.y+=e.footerMarginTop,n.textAlign=s.textAlign(e._footerAlign),n.textBaseline="middle",i=e.footerFontSize,n.fillStyle=e.footerFontColor,n.font=H.fontString(i,e._footerFontStyle,e._footerFontFamily),a=0;a<o;++a)n.fillText(r[a],s.x(t.x),t.y+i/2),t.y+=i+e.footerSpacing}},drawBackground:function(t,e,n,i){n.fillStyle=e.backgroundColor,n.strokeStyle=e.borderColor,n.lineWidth=e.borderWidth;var a=e.xAlign,r=e.yAlign,o=t.x,s=t.y,l=i.width,u=i.height,d=e.cornerRadius;n.beginPath(),n.moveTo(o+d,s),"top"===r&&this.drawCaret(t,i),n.lineTo(o+l-d,s),n.quadraticCurveTo(o+l,s,o+l,s+d),"center"===r&&"right"===a&&this.drawCaret(t,i),n.lineTo(o+l,s+u-d),n.quadraticCurveTo(o+l,s+u,o+l-d,s+u),"bottom"===r&&this.drawCaret(t,i),n.lineTo(o+d,s+u),n.quadraticCurveTo(o,s+u,o,s+u-d),"center"===r&&"left"===a&&this.drawCaret(t,i),n.lineTo(o,s+d),n.quadraticCurveTo(o,s,o+d,s),n.closePath(),n.fill(),e.borderWidth>0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,H.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),H.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!H.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ue=Ye,Ge=je;Ge.positioners=Ue;var qe=H.valueOrDefault;function Ze(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a<s;++a)o=n[t][a],r=qe(o.type,"xAxes"===t?"category":"linear"),a>=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?H.merge(e[t][a],[Re.getScaleDefaults(r),o]):H.merge(e[t][a],o)}else H._merger(t,e,n,i)}})}function $e(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||{},r=n[t];"scales"===t?e[t]=Ze(a,r):"scale"===t?e[t]=H.merge(a,[Re.getScaleDefaults(r.type),r]):H._merger(t,e,n,i)}})}function Xe(t){var e=t.options;H.each(t.scales,(function(e){me.removeBox(t,e)})),e=$e(W.global,W[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Ke(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(H.findIndex(t,a)>=0);return i}function Je(t){return"top"===t||"bottom"===t}function Qe(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}W._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var tn=function(t,e){return this.construct(t,e),this};H.extend(tn.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=$e(W.global,W[t.type],t.options||{}),t}(e);var i=Ie.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=H.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,tn.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Le.notify(t,"beforeInit"),H.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Le.notify(t,"afterInit"),t},clear:function(){return H.canvas.clear(this),this},stop:function(){return J.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(H.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:H.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",H.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Le.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;H.each(e.xAxes,(function(t,n){t.id||(t.id=Ke(e.xAxes,"x-axis-",n))})),H.each(e.yAxes,(function(t,n){t.id||(t.id=Ke(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),H.each(i,(function(e){var i=e.options,r=i.id,o=qe(i.type,e.dtype);Je(i.position)!==Je(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Re.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),H.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Re.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t<e;t++){var r=a[t],o=n.getDatasetMeta(t),s=r.type||n.config.type;if(o.type&&o.type!==s&&(n.destroyDatasetMeta(t),o=n.getDatasetMeta(t)),o.type=s,o.order=r.order||0,o.index=t,o.controller)o.controller.updateIndex(t),o.controller.linkScales();else{var l=Jt[o.type];if(void 0===l)throw new Error('"'+o.type+'" is not a chart type.');o.controller=new l(n,t),i.push(o.controller)}}return i},resetElements:function(){var t=this;H.each(t.data.datasets,(function(e,n){t.getDatasetMeta(n).controller.reset()}),t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(t){var e,n,i=this;if(t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]}),Xe(i),Le._invalidate(i),!1!==Le.notify(i,"beforeUpdate")){i.tooltip._data=i.data;var a=i.buildOrUpdateControllers();for(e=0,n=i.data.datasets.length;e<n;e++)i.getDatasetMeta(e).controller.buildOrUpdateElements();i.updateLayout(),i.options.animation&&i.options.animation.duration&&H.each(a,(function(t){t.reset()})),i.updateDatasets(),i.tooltip.initialize(),i.lastActive=[],Le.notify(i,"afterUpdate"),i._layers.sort(Qe("z","_idx")),i._bufferedRender?i._bufferedRequest={duration:t.duration,easing:t.easing,lazy:t.lazy}:i.render(t)}},updateLayout:function(){var t=this;!1!==Le.notify(t,"beforeLayout")&&(me.update(this,this.width,this.height),t._layers=[],H.each(t.boxes,(function(e){e._configure&&e._configure(),t._layers.push.apply(t._layers,e._layers())}),t),t._layers.forEach((function(t,e){t._idx=e})),Le.notify(t,"afterScaleUpdate"),Le.notify(t,"afterLayout"))},updateDatasets:function(){if(!1!==Le.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t<e;++t)this.updateDataset(t);Le.notify(this,"afterDatasetsUpdate")}},updateDataset:function(t){var e=this.getDatasetMeta(t),n={meta:e,index:t};!1!==Le.notify(this,"beforeDatasetUpdate",[n])&&(e.controller._update(),Le.notify(this,"afterDatasetUpdate",[n]))},render:function(t){var e=this;t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]});var n=e.options.animation,i=qe(t.duration,n&&n.duration),a=t.lazy;if(!1!==Le.notify(e,"beforeRender")){var r=function(t){Le.notify(e,"afterRender"),H.callback(n&&n.onComplete,[t],e)};if(n&&i){var o=new K({numSteps:i/16.66,easing:t.easing||n.easing,render:function(t,e){var n=H.easing.effects[e.easing],i=e.currentStep,a=i/e.numSteps;t.draw(n(a),a,i)},onAnimationProgress:n.onProgress,onAnimationComplete:r});J.addAnimation(e,o,i,a)}else e.draw(),r(new K({numSteps:0,chart:e}));return e}},draw:function(t){var e,n,i=this;if(i.clear(),H.isNullOrUndef(t)&&(t=1),i.transition(t),!(i.width<=0||i.height<=0)&&!1!==Le.notify(i,"beforeDraw",[t])){for(n=i._layers,e=0;e<n.length&&n[e].z<=0;++e)n[e].draw(i.chartArea);for(i.drawDatasets(t);e<n.length;++e)n[e].draw(i.chartArea);i._drawTooltip(t),Le.notify(i,"afterDraw",[t])}},transition:function(t){for(var e=0,n=(this.data.datasets||[]).length;e<n;++e)this.isDatasetVisible(e)&&this.getDatasetMeta(e).controller.transition(t);this.tooltip.transition(t)},_getSortedDatasetMetas:function(t){var e,n,i=[];for(e=0,n=(this.data.datasets||[]).length;e<n;++e)t&&!this.isDatasetVisible(e)||i.push(this.getDatasetMeta(e));return i.sort(Qe("order","index")),i},_getSortedVisibleDatasetMetas:function(){return this._getSortedDatasetMetas(!0)},drawDatasets:function(t){var e,n;if(!1!==Le.notify(this,"beforeDatasetsDraw",[t])){for(n=(e=this._getSortedVisibleDatasetMetas()).length-1;n>=0;--n)this.drawDataset(e[n],t);Le.notify(this,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n={meta:t,index:t.index,easingValue:e};!1!==Le.notify(this,"beforeDatasetDraw",[n])&&(t.controller.draw(e),Le.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,n={tooltip:e,easingValue:t};!1!==Le.notify(this,"beforeTooltipDraw",[n])&&(e.draw(),Le.notify(this,"afterTooltipDraw",[n]))},getElementAtEvent:function(t){return re.modes.single(this,t)},getElementsAtEvent:function(t){return re.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return re.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,n){var i=re.modes[e];return"function"==typeof i?i(this,t,n):[]},getDatasetAtEvent:function(t){return re.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var n=e._meta[this.id];return n||(n=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e.order||0,index:t}),n},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e<n;++e)this.isDatasetVisible(e)&&t++;return t},isDatasetVisible:function(t){var e=this.getDatasetMeta(t);return"boolean"==typeof e.hidden?!e.hidden:!this.data.datasets[t].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroyDatasetMeta:function(t){var e=this.id,n=this.data.datasets[t],i=n._meta&&n._meta[e];i&&(i.controller.destroy(),delete n._meta[e])},destroy:function(){var t,e,n=this,i=n.canvas;for(n.stop(),t=0,e=n.data.datasets.length;t<e;++t)n.destroyDatasetMeta(t);i&&(n.unbindEvents(),H.canvas.clear(n),Ie.releaseContext(n.ctx),n.canvas=null,n.ctx=null),Le.notify(n,"destroy"),delete tn.instances[n.id]},toBase64Image:function(){return this.canvas.toDataURL.apply(this.canvas,arguments)},initToolTip:function(){var t=this;t.tooltip=new Ge({_chart:t,_chartInstance:t,_data:t.data,_options:t.options.tooltips},t)},bindEvents:function(){var t=this,e=t._listeners={},n=function(){t.eventHandler.apply(t,arguments)};H.each(t.options.events,(function(i){Ie.addEventListener(t,i,n),e[i]=n})),t.options.responsive&&(n=function(){t.resize()},Ie.addEventListener(t,"resize",n),e.resize=n)},unbindEvents:function(){var t=this,e=t._listeners;e&&(delete t._listeners,H.each(e,(function(e,n){Ie.removeEventListener(t,n,e)})))},updateHoverStyle:function(t,e,n){var i,a,r,o=n?"set":"remove";for(a=0,r=t.length;a<r;++a)(i=t[a])&&this.getDatasetMeta(i._datasetIndex).controller[o+"HoverStyle"](i);"dataset"===e&&this.getDatasetMeta(t[0]._datasetIndex).controller["_"+o+"DatasetHoverStyle"]()},eventHandler:function(t){var e=this,n=e.tooltip;if(!1!==Le.notify(e,"beforeEvent",[t])){e._bufferedRender=!0,e._bufferedRequest=null;var i=e.handleEvent(t);n&&(i=n._start?n.handleEvent(t):i|n.handleEvent(t)),Le.notify(e,"afterEvent",[t]);var a=e._bufferedRequest;return a?e.render(a):i&&!e.animating&&(e.stop(),e.render({duration:e.options.hover.animationDuration,lazy:!0})),e._bufferedRender=!1,e._bufferedRequest=null,e}},handleEvent:function(t){var e,n=this,i=n.options||{},a=i.hover;return n.lastActive=n.lastActive||[],"mouseout"===t.type?n.active=[]:n.active=n.getElementsAtEventForMode(t,a.mode,a),H.callback(i.onHover||i.hover.onHover,[t.native,n.active],n),"mouseup"!==t.type&&"click"!==t.type||i.onClick&&i.onClick.call(n,t.native,n.active),n.lastActive.length&&n.updateHoverStyle(n.lastActive,a.mode,!1),n.active.length&&a.mode&&n.updateHoverStyle(n.active,a.mode,!0),e=!H.arrayEquals(n.active,n.lastActive),n.lastActive=n.active,e}}),tn.instances={};var en=tn;tn.Controller=tn,tn.types={},H.configMerge=$e,H.scaleMerge=Ze;function nn(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}function an(t){this.options=t||{}}H.extend(an.prototype,{formats:nn,parse:nn,format:nn,add:nn,diff:nn,startOf:nn,endOf:nn,_create:function(t){return t}}),an.override=function(t){H.extend(an.prototype,t)};var rn={_date:an},on={formatters:{values:function(t){return H.isArray(t)?t:""+t},linear:function(t,e,n){var i=n.length>3?n[2]-n[1]:n[1]-n[0];Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));var a=H.log10(Math.abs(i)),r="";if(0!==t)if(Math.max(Math.abs(n[0]),Math.abs(n[n.length-1]))<1e-4){var o=H.log10(Math.abs(t)),s=Math.floor(o)-Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toExponential(s)}else{var l=-1*Math.floor(a);l=Math.max(Math.min(l,20),0),r=t.toFixed(l)}else r="0";return r},logarithmic:function(t,e,n){var i=t/Math.pow(10,Math.floor(H.log10(t)));return 0===t?"0":1===i||2===i||5===i||0===e||e===n.length-1?t.toExponential():""}}},sn=H.isArray,ln=H.isNullOrUndef,un=H.valueOrDefault,dn=H.valueAtIndexOrDefault;function hn(t,e,n){var i,a=t.getTicks().length,r=Math.min(e,a-1),o=t.getPixelForTick(r),s=t._startPixel,l=t._endPixel;if(!(n&&(i=1===a?Math.max(o-s,l-o):0===e?(t.getPixelForTick(1)-o)/2:(o-t.getPixelForTick(r-1))/2,(o+=r<e?i:-i)<s-1e-6||o>l+1e-6)))return o}function cn(t,e,n,i){var a,r,o,s,l,u,d,h,c,f,g,m,p,v=n.length,b=[],y=[],x=[];for(a=0;a<v;++a){if(s=n[a].label,l=n[a].major?e.major:e.minor,t.font=u=l.string,d=i[u]=i[u]||{data:{},gc:[]},h=l.lineHeight,c=f=0,ln(s)||sn(s)){if(sn(s))for(r=0,o=s.length;r<o;++r)g=s[r],ln(g)||sn(g)||(c=H.measureText(t,d.data,d.gc,c,g),f+=h)}else c=H.measureText(t,d.data,d.gc,c,s),f=h;b.push(c),y.push(f),x.push(h/2)}function _(t){return{width:b[t]||0,height:y[t]||0,offset:x[t]||0}}return function(t,e){H.each(t,(function(t){var n,i=t.gc,a=i.length/2;if(a>e){for(n=0;n<a;++n)delete t.data[i[n]];i.splice(0,a)}}))}(i,v),m=b.indexOf(Math.max.apply(null,b)),p=y.indexOf(Math.max.apply(null,y)),{first:_(0),last:_(v-1),widest:_(m),highest:_(p)}}function fn(t){return t.drawTicks?t.tickMarkLength:0}function gn(t){var e,n;return t.display?(e=H.options._parseFont(t),n=H.options.toPadding(t.padding),e.lineHeight+n.height):0}function mn(t,e){return H.extend(H.options._parseFont({fontFamily:un(e.fontFamily,t.fontFamily),fontSize:un(e.fontSize,t.fontSize),fontStyle:un(e.fontStyle,t.fontStyle),lineHeight:un(e.lineHeight,t.lineHeight)}),{color:H.options.resolve([e.fontColor,t.fontColor,W.global.defaultFontColor])})}function pn(t){var e=mn(t,t.minor);return{minor:e,major:t.major.enabled?mn(t,t.major):e}}function vn(t){var e,n,i,a=[];for(n=0,i=t.length;n<i;++n)void 0!==(e=t[n])._index&&a.push(e);return a}function bn(t,e,n,i){var a,r,o,s,l=un(n,0),u=Math.min(un(i,t.length),t.length),d=0;for(e=Math.ceil(e),i&&(e=(a=i-n)/Math.floor(a/e)),s=l;s<0;)d++,s=Math.round(l+d*e);for(r=Math.max(l,0);r<u;r++)o=t[r],r===s?(o._index=r,d++,s=Math.round(l+d*e)):delete o.label}W._set("scale",{display:!0,position:"left",offset:!1,gridLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",zeroLineBorderDash:[],zeroLineBorderDashOffset:0,offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{display:!1,labelString:"",padding:{top:4,bottom:4}},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:0,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:on.formatters.values,minor:{},major:{}}});var yn=$.extend({zeroLineIndex:0,getPadding:function(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}},getTicks:function(){return this._ticks},_getLabels:function(){var t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]},mergeTicksOptions:function(){},beforeUpdate:function(){H.callback(this.options.beforeUpdate,[this])},update:function(t,e,n){var i,a,r,o,s,l=this,u=l.options.ticks,d=u.sampleSize;if(l.beforeUpdate(),l.maxWidth=t,l.maxHeight=e,l.margins=H.extend({left:0,right:0,top:0,bottom:0},n),l._ticks=null,l.ticks=null,l._labelSizes=null,l._maxLabelLines=0,l.longestLabelWidth=0,l.longestTextCache=l.longestTextCache||{},l._gridLineItems=null,l._labelItems=null,l.beforeSetDimensions(),l.setDimensions(),l.afterSetDimensions(),l.beforeDataLimits(),l.determineDataLimits(),l.afterDataLimits(),l.beforeBuildTicks(),o=l.buildTicks()||[],(!(o=l.afterBuildTicks(o)||o)||!o.length)&&l.ticks)for(o=[],i=0,a=l.ticks.length;i<a;++i)o.push({value:l.ticks[i],major:!1});return l._ticks=o,s=d<o.length,r=l._convertTicksToLabels(s?function(t,e){for(var n=[],i=t.length/e,a=0,r=t.length;a<r;a+=i)n.push(t[Math.floor(a)]);return n}(o,d):o),l._configure(),l.beforeCalculateTickRotation(),l.calculateTickRotation(),l.afterCalculateTickRotation(),l.beforeFit(),l.fit(),l.afterFit(),l._ticksToDraw=u.display&&(u.autoSkip||"auto"===u.source)?l._autoSkip(o):o,s&&(r=l._convertTicksToLabels(l._ticksToDraw)),l.ticks=r,l.afterUpdate(),l.minSize},_configure:function(){var t,e,n=this,i=n.options.ticks.reverse;n.isHorizontal()?(t=n.left,e=n.right):(t=n.top,e=n.bottom,i=!i),n._startPixel=t,n._endPixel=e,n._reversePixels=i,n._length=e-t},afterUpdate:function(){H.callback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){H.callback(this.options.beforeSetDimensions,[this])},setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0},afterSetDimensions:function(){H.callback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){H.callback(this.options.beforeDataLimits,[this])},determineDataLimits:H.noop,afterDataLimits:function(){H.callback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){H.callback(this.options.beforeBuildTicks,[this])},buildTicks:H.noop,afterBuildTicks:function(t){var e=this;return sn(t)&&t.length?H.callback(e.options.afterBuildTicks,[e,t]):(e.ticks=H.callback(e.options.afterBuildTicks,[e,e.ticks])||e.ticks,t)},beforeTickToLabelConversion:function(){H.callback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var t=this.options.ticks;this.ticks=this.ticks.map(t.userCallback||t.callback,this)},afterTickToLabelConversion:function(){H.callback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){H.callback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var t,e,n,i,a,r,o,s=this,l=s.options,u=l.ticks,d=s.getTicks().length,h=u.minRotation||0,c=u.maxRotation,f=h;!s._isVisible()||!u.display||h>=c||d<=1||!s.isHorizontal()?s.labelRotation=h:(e=(t=s._getLabelSizes()).widest.width,n=t.highest.height-t.highest.offset,i=Math.min(s.maxWidth,s.chart.width-e),e+6>(a=l.offset?s.maxWidth/d:i/(d-1))&&(a=i/(d-(l.offset?.5:1)),r=s.maxHeight-fn(l.gridLines)-u.padding-gn(l.scaleLabel),o=Math.sqrt(e*e+n*n),f=H.toDegrees(Math.min(Math.asin(Math.min((t.highest.height+6)/a,1)),Math.asin(Math.min(r/o,1))-Math.asin(n/o))),f=Math.max(h,Math.min(c,f))),s.labelRotation=f)},afterCalculateTickRotation:function(){H.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){H.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},n=t.chart,i=t.options,a=i.ticks,r=i.scaleLabel,o=i.gridLines,s=t._isVisible(),l="bottom"===i.position,u=t.isHorizontal();if(u?e.width=t.maxWidth:s&&(e.width=fn(o)+gn(r)),u?s&&(e.height=fn(o)+gn(r)):e.height=t.maxHeight,a.display&&s){var d=pn(a),h=t._getLabelSizes(),c=h.first,f=h.last,g=h.widest,m=h.highest,p=.4*d.minor.lineHeight,v=a.padding;if(u){var b=0!==t.labelRotation,y=H.toRadians(t.labelRotation),x=Math.cos(y),_=Math.sin(y),w=_*g.width+x*(m.height-(b?m.offset:0))+(b?0:p);e.height=Math.min(t.maxHeight,e.height+w+v);var k,M,S=t.getPixelForTick(0)-t.left,D=t.right-t.getPixelForTick(t.getTicks().length-1);b?(k=l?x*c.width+_*c.offset:_*(c.height-c.offset),M=l?_*(f.height-f.offset):x*f.width+_*f.offset):(k=c.width/2,M=f.width/2),t.paddingLeft=Math.max((k-S)*t.width/(t.width-S),0)+3,t.paddingRight=Math.max((M-D)*t.width/(t.width-D),0)+3}else{var C=a.mirror?0:g.width+v+p;e.width=Math.min(t.maxWidth,e.width+C),t.paddingTop=c.height/2,t.paddingBottom=f.height/2}}t.handleMargins(),u?(t.width=t._length=n.width-t.margins.left-t.margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=n.height-t.margins.top-t.margins.bottom)},handleMargins:function(){var t=this;t.margins&&(t.margins.left=Math.max(t.paddingLeft,t.margins.left),t.margins.top=Math.max(t.paddingTop,t.margins.top),t.margins.right=Math.max(t.paddingRight,t.margins.right),t.margins.bottom=Math.max(t.paddingBottom,t.margins.bottom))},afterFit:function(){H.callback(this.options.afterFit,[this])},isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ln(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},_convertTicksToLabels:function(t){var e,n,i,a=this;for(a.ticks=t.map((function(t){return t.value})),a.beforeTickToLabelConversion(),e=a.convertTicksToLabels(t)||a.ticks,a.afterTickToLabelConversion(),n=0,i=t.length;n<i;++n)t[n].label=e[n];return e},_getLabelSizes:function(){var t=this,e=t._labelSizes;return e||(t._labelSizes=e=cn(t.ctx,pn(t.options.ticks),t.getTicks(),t.longestTextCache),t.longestLabelWidth=e.widest.width),e},_parseValue:function(t){var e,n,i,a;return sn(t)?(e=+this.getRightValue(t[0]),n=+this.getRightValue(t[1]),i=Math.min(e,n),a=Math.max(e,n)):(e=void 0,n=t=+this.getRightValue(t),i=t,a=t),{min:i,max:a,start:e,end:n}},_getScaleLabel:function(t){var e=this._parseValue(t);return void 0!==e.start?"["+e.start+", "+e.end+"]":+this.getRightValue(t)},getLabelForIndex:H.noop,getPixelForValue:H.noop,getValueForPixel:H.noop,getPixelForTick:function(t){var e=this.options.offset,n=this._ticks.length,i=1/Math.max(n-(e?0:1),1);return t<0||t>n-1?null:this.getPixelForDecimal(t*i+(e?i/2:0))},getPixelForDecimal:function(t){return this._reversePixels&&(t=1-t),this._startPixel+t*this._length},getDecimalForPixel:function(t){var e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,n,i,a,r=this.options.ticks,o=this._length,s=r.maxTicksLimit||o/this._tickSize()+1,l=r.major.enabled?function(t){var e,n,i=[];for(e=0,n=t.length;e<n;e++)t[e].major&&i.push(e);return i}(t):[],u=l.length,d=l[0],h=l[u-1];if(u>s)return function(t,e,n){var i,a,r=0,o=e[0];for(n=Math.ceil(n),i=0;i<t.length;i++)a=t[i],i===o?(a._index=i,o=e[++r*n]):delete a.label}(t,l,u/s),vn(t);if(i=function(t,e,n,i){var a,r,o,s,l=function(t){var e,n,i=t.length;if(i<2)return!1;for(n=t[0],e=1;e<i;++e)if(t[e]-t[e-1]!==n)return!1;return n}(t),u=(e.length-1)/i;if(!l)return Math.max(u,1);for(o=0,s=(a=H.math._factorize(l)).length-1;o<s;o++)if((r=a[o])>u)return r;return Math.max(u,1)}(l,t,0,s),u>0){for(e=0,n=u-1;e<n;e++)bn(t,i,l[e],l[e+1]);return a=u>1?(h-d)/(u-1):null,bn(t,i,H.isNullOrUndef(a)?0:d-a,d),bn(t,i,h,H.isNullOrUndef(a)?t.length:h+a),vn(t)}return bn(t,i),vn(t)},_tickSize:function(){var t=this.options.ticks,e=H.toRadians(this.labelRotation),n=Math.abs(Math.cos(e)),i=Math.abs(Math.sin(e)),a=this._getLabelSizes(),r=t.autoSkipPadding||0,o=a?a.widest.width+r:0,s=a?a.highest.height+r:0;return this.isHorizontal()?s*n>o*i?o/n:s/i:s*i<o*n?s/n:o/i},_isVisible:function(){var t,e,n,i=this.chart,a=this.options.display;if("auto"!==a)return!!a;for(t=0,e=i.data.datasets.length;t<e;++t)if(i.isDatasetVisible(t)&&((n=i.getDatasetMeta(t)).xAxisID===this.id||n.yAxisID===this.id))return!0;return!1},_computeGridLineItems:function(t){var e,n,i,a,r,o,s,l,u,d,h,c,f,g,m,p,v,b=this,y=b.chart,x=b.options,_=x.gridLines,w=x.position,k=_.offsetGridLines,M=b.isHorizontal(),S=b._ticksToDraw,D=S.length+(k?1:0),C=fn(_),P=[],T=_.drawBorder?dn(_.lineWidth,0,0):0,O=T/2,A=H._alignPixel,F=function(t){return A(y,t,T)};for("top"===w?(e=F(b.bottom),s=b.bottom-C,u=e-O,h=F(t.top)+O,f=t.bottom):"bottom"===w?(e=F(b.top),h=t.top,f=F(t.bottom)-O,s=e+O,u=b.top+C):"left"===w?(e=F(b.right),o=b.right-C,l=e-O,d=F(t.left)+O,c=t.right):(e=F(b.left),d=t.left,c=F(t.right)-O,o=e+O,l=b.left+C),n=0;n<D;++n)i=S[n]||{},ln(i.label)&&n<S.length||(n===b.zeroLineIndex&&x.offset===k?(g=_.zeroLineWidth,m=_.zeroLineColor,p=_.zeroLineBorderDash||[],v=_.zeroLineBorderDashOffset||0):(g=dn(_.lineWidth,n,1),m=dn(_.color,n,"rgba(0,0,0,0.1)"),p=_.borderDash||[],v=_.borderDashOffset||0),void 0!==(a=hn(b,i._index||n,k))&&(r=A(y,a,g),M?o=l=d=c=r:s=u=h=f=r,P.push({tx1:o,ty1:s,tx2:l,ty2:u,x1:d,y1:h,x2:c,y2:f,width:g,color:m,borderDash:p,borderDashOffset:v})));return P.ticksLength=D,P.borderValue=e,P},_computeLabelItems:function(){var t,e,n,i,a,r,o,s,l,u,d,h,c=this,f=c.options,g=f.ticks,m=f.position,p=g.mirror,v=c.isHorizontal(),b=c._ticksToDraw,y=pn(g),x=g.padding,_=fn(f.gridLines),w=-H.toRadians(c.labelRotation),k=[];for("top"===m?(r=c.bottom-_-x,o=w?"left":"center"):"bottom"===m?(r=c.top+_+x,o=w?"right":"center"):"left"===m?(a=c.right-(p?0:_)-x,o=p?"left":"right"):(a=c.left+(p?0:_)+x,o=p?"right":"left"),t=0,e=b.length;t<e;++t)i=(n=b[t]).label,ln(i)||(s=c.getPixelForTick(n._index||t)+g.labelOffset,u=(l=n.major?y.major:y.minor).lineHeight,d=sn(i)?i.length:1,v?(a=s,h="top"===m?((w?1:.5)-d)*u:(w?0:.5)*u):(r=s,h=(1-d)*u/2),k.push({x:a,y:r,rotation:w,label:i,font:l,textOffset:h,textAlign:o}));return k},_drawGrid:function(t){var e=this,n=e.options.gridLines;if(n.display){var i,a,r,o,s,l=e.ctx,u=e.chart,d=H._alignPixel,h=n.drawBorder?dn(n.lineWidth,0,0):0,c=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));for(r=0,o=c.length;r<o;++r)i=(s=c[r]).width,a=s.color,i&&a&&(l.save(),l.lineWidth=i,l.strokeStyle=a,l.setLineDash&&(l.setLineDash(s.borderDash),l.lineDashOffset=s.borderDashOffset),l.beginPath(),n.drawTicks&&(l.moveTo(s.tx1,s.ty1),l.lineTo(s.tx2,s.ty2)),n.drawOnChartArea&&(l.moveTo(s.x1,s.y1),l.lineTo(s.x2,s.y2)),l.stroke(),l.restore());if(h){var f,g,m,p,v=h,b=dn(n.lineWidth,c.ticksLength-1,1),y=c.borderValue;e.isHorizontal()?(f=d(u,e.left,v)-v/2,g=d(u,e.right,b)+b/2,m=p=y):(m=d(u,e.top,v)-v/2,p=d(u,e.bottom,b)+b/2,f=g=y),l.lineWidth=h,l.strokeStyle=dn(n.color,0),l.beginPath(),l.moveTo(f,m),l.lineTo(g,p),l.stroke()}}},_drawLabels:function(){var t=this;if(t.options.ticks.display){var e,n,i,a,r,o,s,l,u=t.ctx,d=t._labelItems||(t._labelItems=t._computeLabelItems());for(e=0,i=d.length;e<i;++e){if(o=(r=d[e]).font,u.save(),u.translate(r.x,r.y),u.rotate(r.rotation),u.font=o.string,u.fillStyle=o.color,u.textBaseline="middle",u.textAlign=r.textAlign,s=r.label,l=r.textOffset,sn(s))for(n=0,a=s.length;n<a;++n)u.fillText(""+s[n],0,l),l+=o.lineHeight;else u.fillText(s,0,l);u.restore()}}},_drawTitle:function(){var t=this,e=t.ctx,n=t.options,i=n.scaleLabel;if(i.display){var a,r,o=un(i.fontColor,W.global.defaultFontColor),s=H.options._parseFont(i),l=H.options.toPadding(i.padding),u=s.lineHeight/2,d=n.position,h=0;if(t.isHorizontal())a=t.left+t.width/2,r="bottom"===d?t.bottom-u-l.bottom:t.top+u+l.top;else{var c="left"===d;a=c?t.left+u+l.top:t.right-u-l.top,r=t.top+t.height/2,h=c?-.5*Math.PI:.5*Math.PI}e.save(),e.translate(a,r),e.rotate(h),e.textAlign="center",e.textBaseline="middle",e.fillStyle=o,e.font=s.string,e.fillText(i.labelString,0,0),e.restore()}},draw:function(t){this._isVisible()&&(this._drawGrid(t),this._drawTitle(),this._drawLabels())},_layers:function(){var t=this,e=t.options,n=e.ticks&&e.ticks.z||0,i=e.gridLines&&e.gridLines.z||0;return t._isVisible()&&n!==i&&t.draw===t._draw?[{z:i,draw:function(){t._drawGrid.apply(t,arguments),t._drawTitle.apply(t,arguments)}},{z:n,draw:function(){t._drawLabels.apply(t,arguments)}}]:[{z:n,draw:function(){t.draw.apply(t,arguments)}}]},_getMatchingVisibleMetas:function(t){var e=this,n=e.isHorizontal();return e.chart._getSortedVisibleDatasetMetas().filter((function(i){return(!t||i.type===t)&&(n?i.xAxisID===e.id:i.yAxisID===e.id)}))}});yn.prototype._draw=yn.prototype.draw;var xn=yn,_n=H.isNullOrUndef,wn=xn.extend({determineDataLimits:function(){var t,e=this,n=e._getLabels(),i=e.options.ticks,a=i.min,r=i.max,o=0,s=n.length-1;void 0!==a&&(t=n.indexOf(a))>=0&&(o=t),void 0!==r&&(t=n.indexOf(r))>=0&&(s=t),e.minIndex=o,e.maxIndex=s,e.min=n[o],e.max=n[s]},buildTicks:function(){var t=this._getLabels(),e=this.minIndex,n=this.maxIndex;this.ticks=0===e&&n===t.length-1?t:t.slice(e,n+1)},getLabelForIndex:function(t,e){var n=this.chart;return n.getDatasetMeta(e).controller._getValueScaleId()===this.id?this.getRightValue(n.data.datasets[e].data[t]):this._getLabels()[t]},_configure:function(){var t=this,e=t.options.offset,n=t.ticks;xn.prototype._configure.call(t),t.isHorizontal()||(t._reversePixels=!t._reversePixels),n&&(t._startValue=t.minIndex-(e?.5:0),t._valueRange=Math.max(n.length-(e?0:1),1))},getPixelForValue:function(t,e,n){var i,a,r,o=this;return _n(e)||_n(n)||(t=o.chart.data.datasets[n].data[e]),_n(t)||(i=o.isHorizontal()?t.x:t.y),(void 0!==i||void 0!==t&&isNaN(e))&&(a=o._getLabels(),t=H.valueOrDefault(i,t),e=-1!==(r=a.indexOf(t))?r:e,isNaN(e)&&(e=t)),o.getPixelForDecimal((e-o._startValue)/o._valueRange)},getPixelForTick:function(t){var e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t],t+this.minIndex)},getValueForPixel:function(t){var e=Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange);return Math.min(Math.max(e,0),this.ticks.length-1)},getBasePixel:function(){return this.bottom}}),kn={position:"bottom"};wn._defaults=kn;var Mn=H.noop,Sn=H.isNullOrUndef;var Dn=xn.extend({getRightValue:function(t){return"string"==typeof t?+t:xn.prototype.getRightValue.call(this,t)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var n=H.sign(t.min),i=H.sign(t.max);n<0&&i<0?t.max=0:n>0&&i>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,n=e.stepSize,i=e.maxTicksLimit;return n?t=Math.ceil(this.max/n)-Math.floor(this.min/n)+1:(t=this._computeTickLimit(),i=i||11),i&&(t=Math.min(i,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Mn,buildTicks:function(){var t=this,e=t.options.ticks,n=t.getTickLimit(),i={maxTicks:n=Math.max(2,n),min:e.min,max:e.max,precision:e.precision,stepSize:H.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var n,i,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=H.niceNum((g-f)/u/l)*l;if(m<1e-14&&Sn(d)&&Sn(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=H.niceNum(r*m/u/l)*l),s||Sn(c)?n=Math.pow(10,H._decimalPlaces(m)):(n=Math.pow(10,c),m=Math.ceil(m*n)/n),i=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!Sn(d)&&H.almostWhole(d/m,m/1e3)&&(i=d),!Sn(h)&&H.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-i)/m,r=H.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),i=Math.round(i*n)/n,a=Math.round(a*n)/n,o.push(Sn(d)?i:d);for(var p=1;p<r;++p)o.push(Math.round((i+p*m)*n)/n);return o.push(Sn(h)?a:h),o}(i,t);t.handleDirectionalChanges(),t.max=H.max(a),t.min=H.min(a),e.reverse?(a.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max)},convertTicksToLabels:function(){var t=this;t.ticksAsNumbers=t.ticks.slice(),t.zeroLineIndex=t.ticks.indexOf(0),xn.prototype.convertTicksToLabels.call(t)},_configure:function(){var t,e=this,n=e.getTicks(),i=e.min,a=e.max;xn.prototype._configure.call(e),e.options.offset&&n.length&&(i-=t=(a-i)/Math.max(n.length-1,1)/2,a+=t),e._startValue=i,e._endValue=a,e._valueRange=a-i}}),Cn={position:"left",ticks:{callback:on.formatters.linear}};function Pn(t,e,n,i){var a,r,o=t.options,s=function(t,e,n){var i=[n.type,void 0===e&&void 0===n.stack?n.index:"",n.stack].join(".");return void 0===t[i]&&(t[i]={pos:[],neg:[]}),t[i]}(e,o.stacked,n),l=s.pos,u=s.neg,d=i.length;for(a=0;a<d;++a)r=t._parseValue(i[a]),isNaN(r.min)||isNaN(r.max)||n.data[a].hidden||(l[a]=l[a]||0,u[a]=u[a]||0,o.relativePoints?l[a]=100:r.min<0||r.max<0?u[a]+=r.min:l[a]+=r.max)}function Tn(t,e,n){var i,a,r=n.length;for(i=0;i<r;++i)a=t._parseValue(n[i]),isNaN(a.min)||isNaN(a.max)||e.data[i].hidden||(t.min=Math.min(t.min,a.min),t.max=Math.max(t.max,a.max))}var On=Dn.extend({determineDataLimits:function(){var t,e,n,i,a=this,r=a.options,o=a.chart.data.datasets,s=a._getMatchingVisibleMetas(),l=r.stacked,u={},d=s.length;if(a.min=Number.POSITIVE_INFINITY,a.max=Number.NEGATIVE_INFINITY,void 0===l)for(t=0;!l&&t<d;++t)l=void 0!==(e=s[t]).stack;for(t=0;t<d;++t)n=o[(e=s[t]).index].data,l?Pn(a,u,e,n):Tn(a,e,n);H.each(u,(function(t){i=t.pos.concat(t.neg),a.min=Math.min(a.min,H.min(i)),a.max=Math.max(a.max,H.max(i))})),a.min=H.isFinite(a.min)&&!isNaN(a.min)?a.min:0,a.max=H.isFinite(a.max)&&!isNaN(a.max)?a.max:1,a.handleTickRangeOptions()},_computeTickLimit:function(){var t;return this.isHorizontal()?Math.ceil(this.width/40):(t=H.options._parseFont(this.options.ticks),Math.ceil(this.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return this._getScaleLabel(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){return this.getPixelForDecimal((+this.getRightValue(t)-this._startValue)/this._valueRange)},getValueForPixel:function(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange},getPixelForTick:function(t){var e=this.ticksAsNumbers;return t<0||t>e.length-1?null:this.getPixelForValue(e[t])}}),An=Cn;On._defaults=An;var Fn=H.valueOrDefault,In=H.math.log10;var Ln={position:"left",ticks:{callback:on.formatters.logarithmic}};function Rn(t,e){return H.isFinite(t)&&t>=0?t:e}var Nn=xn.extend({determineDataLimits:function(){var t,e,n,i,a,r,o=this,s=o.options,l=o.chart,u=l.data.datasets,d=o.isHorizontal();function h(t){return d?t.xAxisID===o.id:t.yAxisID===o.id}o.min=Number.POSITIVE_INFINITY,o.max=Number.NEGATIVE_INFINITY,o.minNotZero=Number.POSITIVE_INFINITY;var c=s.stacked;if(void 0===c)for(t=0;t<u.length;t++)if(e=l.getDatasetMeta(t),l.isDatasetVisible(t)&&h(e)&&void 0!==e.stack){c=!0;break}if(s.stacked||c){var f={};for(t=0;t<u.length;t++){var g=[(e=l.getDatasetMeta(t)).type,void 0===s.stacked&&void 0===e.stack?t:"",e.stack].join(".");if(l.isDatasetVisible(t)&&h(e))for(void 0===f[g]&&(f[g]=[]),a=0,r=(i=u[t].data).length;a<r;a++){var m=f[g];n=o._parseValue(i[a]),isNaN(n.min)||isNaN(n.max)||e.data[a].hidden||n.min<0||n.max<0||(m[a]=m[a]||0,m[a]+=n.max)}}H.each(f,(function(t){if(t.length>0){var e=H.min(t),n=H.max(t);o.min=Math.min(o.min,e),o.max=Math.max(o.max,n)}}))}else for(t=0;t<u.length;t++)if(e=l.getDatasetMeta(t),l.isDatasetVisible(t)&&h(e))for(a=0,r=(i=u[t].data).length;a<r;a++)n=o._parseValue(i[a]),isNaN(n.min)||isNaN(n.max)||e.data[a].hidden||n.min<0||n.max<0||(o.min=Math.min(n.min,o.min),o.max=Math.max(n.max,o.max),0!==n.min&&(o.minNotZero=Math.min(n.min,o.minNotZero)));o.min=H.isFinite(o.min)?o.min:null,o.max=H.isFinite(o.max)?o.max:null,o.minNotZero=H.isFinite(o.minNotZero)?o.minNotZero:null,this.handleTickRangeOptions()},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;t.min=Rn(e.min,t.min),t.max=Rn(e.max,t.max),t.min===t.max&&(0!==t.min&&null!==t.min?(t.min=Math.pow(10,Math.floor(In(t.min))-1),t.max=Math.pow(10,Math.floor(In(t.max))+1)):(t.min=1,t.max=10)),null===t.min&&(t.min=Math.pow(10,Math.floor(In(t.max))-1)),null===t.max&&(t.max=0!==t.min?Math.pow(10,Math.floor(In(t.min))+1):10),null===t.minNotZero&&(t.min>0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(In(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,n=!t.isHorizontal(),i={min:Rn(e.min),max:Rn(e.max)},a=t.ticks=function(t,e){var n,i,a=[],r=Fn(t.min,Math.pow(10,Math.floor(In(e.min)))),o=Math.floor(In(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(n=Math.floor(In(e.minNotZero)),i=Math.floor(e.minNotZero/Math.pow(10,n)),a.push(r),r=i*Math.pow(10,n)):(n=Math.floor(In(r)),i=Math.floor(r/Math.pow(10,n)));var l=n<0?Math.pow(10,Math.abs(n)):1;do{a.push(r),10===++i&&(i=1,l=++n>=0?1:l),r=Math.round(i*Math.pow(10,n)*l)/l}while(n<o||n===o&&i<s);var u=Fn(t.max,r);return a.push(u),a}(i,t);t.max=H.max(a),t.min=H.min(a),e.reverse?(n=!n,t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),n&&a.reverse()},convertTicksToLabels:function(){this.tickValues=this.ticks.slice(),xn.prototype.convertTicksToLabels.call(this)},getLabelForIndex:function(t,e){return this._getScaleLabel(this.chart.data.datasets[e].data[t])},getPixelForTick:function(t){var e=this.tickValues;return t<0||t>e.length-1?null:this.getPixelForValue(e[t])},_getFirstTickValue:function(t){var e=Math.floor(In(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},_configure:function(){var t=this,e=t.min,n=0;xn.prototype._configure.call(t),0===e&&(e=t._getFirstTickValue(t.minNotZero),n=Fn(t.options.ticks.fontSize,W.global.defaultFontSize)/t._length),t._startValue=In(e),t._valueOffset=n,t._valueRange=(In(t.max)-In(e))/(1-n)},getPixelForValue:function(t){var e=this,n=0;return(t=+e.getRightValue(t))>e.min&&t>0&&(n=(In(t)-e._startValue)/e._valueRange+e._valueOffset),e.getPixelForDecimal(n)},getValueForPixel:function(t){var e=this,n=e.getDecimalForPixel(t);return 0===n&&0===e.min?0:Math.pow(10,e._startValue+(n-e._valueOffset)*e._valueRange)}}),Wn=Ln;Nn._defaults=Wn;var Yn=H.valueOrDefault,zn=H.valueAtIndexOrDefault,En=H.options.resolve,Vn={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:on.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function Hn(t){var e=t.ticks;return e.display&&t.display?Yn(e.fontSize,W.global.defaultFontSize)+2*e.backdropPaddingY:0}function Bn(t,e,n,i,a){return t===i||t===a?{start:e-n/2,end:e+n/2}:t<i||t>a?{start:e-n,end:e}:{start:e,end:e+n}}function jn(t){return 0===t||180===t?"center":t<180?"left":"right"}function Un(t,e,n,i){var a,r,o=n.y+i/2;if(H.isArray(e))for(a=0,r=e.length;a<r;++a)t.fillText(e[a],n.x,o),o+=i;else t.fillText(e,n.x,o)}function Gn(t,e,n){90===t||270===t?n.y-=e.h/2:(t>270||t<90)&&(n.y-=e.h)}function qn(t){return H.isNumber(t)?t:0}var Zn=Dn.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Hn(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,n=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;H.each(e.data.datasets,(function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);H.each(a.data,(function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(n=Math.min(r,n),i=Math.max(r,i))}))}})),t.min=n===Number.POSITIVE_INFINITY?0:n,t.max=i===Number.NEGATIVE_INFINITY?0:i,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Hn(this.options))},convertTicksToLabels:function(){var t=this;Dn.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map((function(){var e=H.callback(t.options.pointLabels.callback,arguments,t);return e||0===e?e:""}))},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,n,i,a=H.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=t.chart.data.labels.length;for(e=0;e<d;e++){i=t.getPointPosition(e,t.drawingArea+5),s=t.ctx,l=a.lineHeight,u=t.pointLabels[e],n=H.isArray(u)?{w:H.longestText(s,s.font,u),h:u.length*l}:{w:s.measureText(u).width,h:l},t._pointLabelSizes[e]=n;var h=t.getIndexAngle(e),c=H.toDegrees(h)%360,f=Bn(c,i.x,n.w,0,180),g=Bn(c,i.y,n.h,90,270);f.start<r.l&&(r.l=f.start,o.l=h),f.end>r.r&&(r.r=f.end,o.r=h),g.start<r.t&&(r.t=g.start,o.t=h),g.end>r.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,n){var i=this,a=e.l/Math.sin(n.l),r=Math.max(e.r-i.width,0)/Math.sin(n.r),o=-e.t/Math.cos(n.t),s=-Math.max(e.b-(i.height-i.paddingTop),0)/Math.cos(n.b);a=qn(a),r=qn(r),o=qn(o),s=qn(s),i.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),i.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,n,i){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=n+a.drawingArea,l=a.height-a.paddingTop-i-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){var e=this.chart,n=(t*(360/e.data.labels.length)+((e.options||{}).startAngle||0))%360;return(n<0?n+360:n)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(H.isNullOrUndef(t))return NaN;var n=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*n:(t-e.min)*n},getPointPosition:function(t,e){var n=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(n)*e+this.xCenter,y:Math.sin(n)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(t){var e=this.min,n=this.max;return this.getPointPositionForValue(t||0,this.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0)},_drawGrid:function(){var t,e,n,i=this,a=i.ctx,r=i.options,o=r.gridLines,s=r.angleLines,l=Yn(s.lineWidth,o.lineWidth),u=Yn(s.color,o.color);if(r.pointLabels.display&&function(t){var e=t.ctx,n=t.options,i=n.pointLabels,a=Hn(n),r=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),o=H.options._parseFont(i);e.save(),e.font=o.string,e.textBaseline="middle";for(var s=t.chart.data.labels.length-1;s>=0;s--){var l=0===s?a/2:0,u=t.getPointPosition(s,r+l+5),d=zn(i.fontColor,s,W.global.defaultFontColor);e.fillStyle=d;var h=t.getIndexAngle(s),c=H.toDegrees(h);e.textAlign=jn(c),Gn(c,t._pointLabelSizes[s],u),Un(e,t.pointLabels[s],u,o.lineHeight)}e.restore()}(i),o.display&&H.each(i.ticks,(function(t,n){0!==n&&(e=i.getDistanceFromCenterForValue(i.ticksAsNumbers[n]),function(t,e,n,i){var a,r=t.ctx,o=e.circular,s=t.chart.data.labels.length,l=zn(e.color,i-1),u=zn(e.lineWidth,i-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,n,0,2*Math.PI);else{a=t.getPointPosition(0,n),r.moveTo(a.x,a.y);for(var d=1;d<s;d++)a=t.getPointPosition(d,n),r.lineTo(a.x,a.y)}r.closePath(),r.stroke(),r.restore()}}(i,o,e,n))})),s.display&&l&&u){for(a.save(),a.lineWidth=l,a.strokeStyle=u,a.setLineDash&&(a.setLineDash(En([s.borderDash,o.borderDash,[]])),a.lineDashOffset=En([s.borderDashOffset,o.borderDashOffset,0])),t=i.chart.data.labels.length-1;t>=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=H.options._parseFont(n),s=Yn(n.fontColor,W.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",H.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:H.noop}),$n=Vn;Zn._defaults=$n;var Xn=H._deprecated,Kn=H.options.resolve,Jn=H.valueOrDefault,Qn=Number.MIN_SAFE_INTEGER||-9007199254740991,ti=Number.MAX_SAFE_INTEGER||9007199254740991,ei={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ni=Object.keys(ei);function ii(t,e){return t-e}function ai(t){return H.valueOrDefault(t.time.min,t.ticks.min)}function ri(t){return H.valueOrDefault(t.time.max,t.ticks.max)}function oi(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]<n)o=i+1;else{if(!(a[e]>n))return{lo:a,hi:r};s=i-1}}return{lo:r,hi:null}}(t,e,n),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(n-r[e])/s:0,u=(o[i]-r[i])*l;return r[i]+u}function si(t,e){var n=t._adapter,i=t.options.time,a=i.parser,r=a||i.format,o=e;return"function"==typeof a&&(o=a(o)),H.isFinite(o)||(o="string"==typeof r?n.parse(o,r):n.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),H.isFinite(o)||(o=n.parse(o))),o)}function li(t,e){if(H.isNullOrUndef(e))return null;var n=t.options.time,i=si(t,t.getRightValue(e));return null===i?i:(n.round&&(i=+t._adapter.startOf(i,n.round)),i)}function ui(t,e,n,i){var a,r,o,s=ni.length;for(a=ni.indexOf(t);a<s-1;++a)if(o=(r=ei[ni[a]]).steps?r.steps:ti,r.common&&Math.ceil((n-e)/(o*r.size))<=i)return ni[a];return ni[s-1]}function di(t,e,n){var i,a,r=[],o={},s=e.length;for(i=0;i<s;++i)o[a=e[i]]=i,r.push({value:a,major:!1});return 0!==s&&n?function(t,e,n,i){var a,r,o=t._adapter,s=+o.startOf(e[0].value,i),l=e[e.length-1].value;for(a=s;a<=l;a=+o.add(a,1,i))(r=n[a])>=0&&(e[r].major=!0);return e}(t,r,o,n):r}var hi=xn.extend({initialize:function(){this.mergeTicksOptions(),xn.prototype.initialize.call(this)},update:function(){var t=this,e=t.options,n=e.time||(e.time={}),i=t._adapter=new rn._date(e.adapters.date);return Xn("time scale",n.format,"time.format","time.parser"),Xn("time scale",n.min,"time.min","ticks.min"),Xn("time scale",n.max,"time.max","ticks.max"),H.mergeIf(n.displayFormats,i.formats()),xn.prototype.update.apply(t,arguments)},getRightValue:function(t){return t&&void 0!==t.t&&(t=t.t),xn.prototype.getRightValue.call(this,t)},determineDataLimits:function(){var t,e,n,i,a,r,o,s=this,l=s.chart,u=s._adapter,d=s.options,h=d.time.unit||"day",c=ti,f=Qn,g=[],m=[],p=[],v=s._getLabels();for(t=0,n=v.length;t<n;++t)p.push(li(s,v[t]));for(t=0,n=(l.data.datasets||[]).length;t<n;++t)if(l.isDatasetVisible(t))if(a=l.data.datasets[t].data,H.isObject(a[0]))for(m[t]=[],e=0,i=a.length;e<i;++e)r=li(s,a[e]),g.push(r),m[t][e]=r;else m[t]=p.slice(0),o||(g=g.concat(p),o=!0);else m[t]=[];p.length&&(c=Math.min(c,p[0]),f=Math.max(f,p[p.length-1])),g.length&&(g=n>1?function(t){var e,n,i,a={},r=[];for(e=0,n=t.length;e<n;++e)a[i=t[e]]||(a[i]=!0,r.push(i));return r}(g).sort(ii):g.sort(ii),c=Math.min(c,g[0]),f=Math.max(f,g[g.length-1])),c=li(s,ai(d))||c,f=li(s,ri(d))||f,c=c===ti?+u.startOf(Date.now(),h):c,f=f===Qn?+u.endOf(Date.now(),h)+1:f,s.min=Math.min(c,f),s.max=Math.max(c+1,f),s._table=[],s._timestamps={data:g,datasets:m,labels:p}},buildTicks:function(){var t,e,n,i=this,a=i.min,r=i.max,o=i.options,s=o.ticks,l=o.time,u=i._timestamps,d=[],h=i.getLabelCapacity(a),c=s.source,f=o.distribution;for(u="data"===c||"auto"===c&&"series"===f?u.data:"labels"===c?u.labels:function(t,e,n,i){var a,r=t._adapter,o=t.options,s=o.time,l=s.unit||ui(s.minUnit,e,n,i),u=Kn([s.stepSize,s.unitStepSize,1]),d="week"===l&&s.isoWeekday,h=e,c=[];if(d&&(h=+r.startOf(h,"isoWeek",d)),h=+r.startOf(h,d?"day":l),r.diff(n,e,l)>1e5*u)throw e+" and "+n+" are too far apart with stepSize of "+u+" "+l;for(a=h;a<n;a=+r.add(a,u,l))c.push(a);return a!==n&&"ticks"!==o.bounds||c.push(a),c}(i,a,r,h),"ticks"===o.bounds&&u.length&&(a=u[0],r=u[u.length-1]),a=li(i,ai(o))||a,r=li(i,ri(o))||r,t=0,e=u.length;t<e;++t)(n=u[t])>=a&&n<=r&&d.push(n);return i.min=a,i.max=r,i._unit=l.unit||(s.autoSkip?ui(l.minUnit,i.min,i.max,h):function(t,e,n,i,a){var r,o;for(r=ni.length-1;r>=ni.indexOf(n);r--)if(o=ni[r],ei[o].common&&t._adapter.diff(a,i,o)>=e-1)return o;return ni[n?ni.indexOf(n):0]}(i,d.length,l.minUnit,i.min,i.max)),i._majorUnit=s.major.enabled&&"year"!==i._unit?function(t){for(var e=ni.indexOf(t)+1,n=ni.length;e<n;++e)if(ei[ni[e]].common)return ni[e]}(i._unit):void 0,i._table=function(t,e,n,i){if("linear"===i||!t.length)return[{time:e,pos:0},{time:n,pos:1}];var a,r,o,s,l,u=[],d=[e];for(a=0,r=t.length;a<r;++a)(s=t[a])>e&&s<n&&d.push(s);for(d.push(n),a=0,r=d.length;a<r;++a)l=d[a+1],o=d[a-1],s=d[a],void 0!==o&&void 0!==l&&Math.round((l+o)/2)===s||u.push({time:s,pos:a/(r-1)});return u}(i._timestamps.data,a,r,f),i._offsets=function(t,e,n,i,a){var r,o,s=0,l=0;return a.offset&&e.length&&(r=oi(t,"time",e[0],"pos"),s=1===e.length?1-r:(oi(t,"time",e[1],"pos")-r)/2,o=oi(t,"time",e[e.length-1],"pos"),l=1===e.length?o:(o-oi(t,"time",e[e.length-2],"pos"))/2),{start:s,end:l,factor:1/(s+1+l)}}(i._table,d,0,0,o),s.reverse&&d.reverse(),di(i,d,i._majorUnit)},getLabelForIndex:function(t,e){var n=this,i=n._adapter,a=n.chart.data,r=n.options.time,o=a.labels&&t<a.labels.length?a.labels[t]:"",s=a.datasets[e].data[t];return H.isObject(s)&&(o=n.getRightValue(s)),r.tooltipFormat?i.format(si(n,o),r.tooltipFormat):"string"==typeof o?o:i.format(si(n,o),r.displayFormats.datetime)},tickFormatFunction:function(t,e,n,i){var a=this._adapter,r=this.options,o=r.time.displayFormats,s=o[this._unit],l=this._majorUnit,u=o[l],d=n[e],h=r.ticks,c=l&&u&&d&&d.major,f=a.format(t,i||(c?u:s)),g=c?h.major:h.minor,m=Kn([g.callback,g.userCallback,h.callback,h.userCallback]);return m?m(f,e,n):f},convertTicksToLabels:function(t){var e,n,i=[];for(e=0,n=t.length;e<n;++e)i.push(this.tickFormatFunction(t[e].value,e,t));return i},getPixelForOffset:function(t){var e=this._offsets,n=oi(this._table,"time",t,"pos");return this.getPixelForDecimal((e.start+n)*e.factor)},getPixelForValue:function(t,e,n){var i=null;if(void 0!==e&&void 0!==n&&(i=this._timestamps.datasets[n][e]),null===i&&(i=li(this,t)),null!==i)return this.getPixelForOffset(i)},getPixelForTick:function(t){var e=this.getTicks();return t>=0&&t<e.length?this.getPixelForOffset(e[t].value):null},getValueForPixel:function(t){var e=this._offsets,n=this.getDecimalForPixel(t)/e.factor-e.end,i=oi(this._table,"pos",n,"time");return this._adapter._create(i)},_getLabelSize:function(t){var e=this.options.ticks,n=this.ctx.measureText(t).width,i=H.toRadians(this.isHorizontal()?e.maxRotation:e.minRotation),a=Math.cos(i),r=Math.sin(i),o=Jn(e.fontSize,W.global.defaultFontSize);return{w:n*a+o*r,h:n*r+o*a}},getLabelWidth:function(t){return this._getLabelSize(t).w},getLabelCapacity:function(t){var e=this,n=e.options.time,i=n.displayFormats,a=i[n.unit]||i.millisecond,r=e.tickFormatFunction(t,0,di(e,[t],e._majorUnit),a),o=e._getLabelSize(r),s=Math.floor(e.isHorizontal()?e.width/o.w:e.height/o.h);return e.options.offset&&s--,s>0?s:1}}),ci={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};hi._defaults=ci;var fi={category:wn,linear:On,logarithmic:Nn,radialLinear:Zn,time:hi},gi=e((function(e,n){e.exports=function(){var n,i;function a(){return n.apply(null,arguments)}function r(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function o(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function s(t){return void 0===t}function l(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function u(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function d(t,e){var n,i=[];for(n=0;n<t.length;++n)i.push(e(t[n],n));return i}function h(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function c(t,e){for(var n in e)h(e,n)&&(t[n]=e[n]);return h(e,"toString")&&(t.toString=e.toString),h(e,"valueOf")&&(t.valueOf=e.valueOf),t}function f(t,e,n,i){return Ie(t,e,n,i,!0).utc()}function g(t){return null==t._pf&&(t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),t._pf}function m(t){if(null==t._isValid){var e=g(t),n=i.call(e.parsedDateParts,(function(t){return null!=t})),a=!isNaN(t._d.getTime())&&e.overflow<0&&!e.empty&&!e.invalidMonth&&!e.invalidWeekday&&!e.weekdayMismatch&&!e.nullInput&&!e.invalidFormat&&!e.userInvalidated&&(!e.meridiem||e.meridiem&&n);if(t._strict&&(a=a&&0===e.charsLeftOver&&0===e.unusedTokens.length&&void 0===e.bigHour),null!=Object.isFrozen&&Object.isFrozen(t))return a;t._isValid=a}return t._isValid}function p(t){var e=f(NaN);return null!=t?c(g(e),t):g(e).userInvalidated=!0,e}i=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;i<n;i++)if(i in e&&t.call(this,e[i],i,e))return!0;return!1};var v=a.momentProperties=[];function b(t,e){var n,i,a;if(s(e._isAMomentObject)||(t._isAMomentObject=e._isAMomentObject),s(e._i)||(t._i=e._i),s(e._f)||(t._f=e._f),s(e._l)||(t._l=e._l),s(e._strict)||(t._strict=e._strict),s(e._tzm)||(t._tzm=e._tzm),s(e._isUTC)||(t._isUTC=e._isUTC),s(e._offset)||(t._offset=e._offset),s(e._pf)||(t._pf=g(e)),s(e._locale)||(t._locale=e._locale),v.length>0)for(n=0;n<v.length;n++)s(a=e[i=v[n]])||(t[i]=a);return t}var y=!1;function x(t){b(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===y&&(y=!0,a.updateOffset(this),y=!1)}function _(t){return t instanceof x||null!=t&&null!=t._isAMomentObject}function w(t){return t<0?Math.ceil(t)||0:Math.floor(t)}function k(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=w(e)),n}function M(t,e,n){var i,a=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),o=0;for(i=0;i<a;i++)(n&&t[i]!==e[i]||!n&&k(t[i])!==k(e[i]))&&o++;return o+r}function S(t){!1===a.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function D(t,e){var n=!0;return c((function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,t),n){for(var i,r=[],o=0;o<arguments.length;o++){if(i="","object"==typeof arguments[o]){for(var s in i+="\n["+o+"] ",arguments[0])i+=s+": "+arguments[0][s]+", ";i=i.slice(0,-2)}else i=arguments[o];r.push(i)}S(t+"\nArguments: "+Array.prototype.slice.call(r).join("")+"\n"+(new Error).stack),n=!1}return e.apply(this,arguments)}),e)}var C,P={};function T(t,e){null!=a.deprecationHandler&&a.deprecationHandler(t,e),P[t]||(S(e),P[t]=!0)}function O(t){return t instanceof Function||"[object Function]"===Object.prototype.toString.call(t)}function A(t,e){var n,i=c({},t);for(n in e)h(e,n)&&(o(t[n])&&o(e[n])?(i[n]={},c(i[n],t[n]),c(i[n],e[n])):null!=e[n]?i[n]=e[n]:delete i[n]);for(n in t)h(t,n)&&!h(e,n)&&o(t[n])&&(i[n]=c({},i[n]));return i}function F(t){null!=t&&this.set(t)}a.suppressDeprecationWarnings=!1,a.deprecationHandler=null,C=Object.keys?Object.keys:function(t){var e,n=[];for(e in t)h(t,e)&&n.push(e);return n};var I={};function L(t,e){var n=t.toLowerCase();I[n]=I[n+"s"]=I[e]=t}function R(t){return"string"==typeof t?I[t]||I[t.toLowerCase()]:void 0}function N(t){var e,n,i={};for(n in t)h(t,n)&&(e=R(n))&&(i[e]=t[n]);return i}var W={};function Y(t,e){W[t]=e}function z(t,e,n){var i=""+Math.abs(t),a=e-i.length;return(t>=0?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}var E=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},B={};function j(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(B[t]=a),e&&(B[e[0]]=function(){return z(a.apply(this,arguments),e[1],e[2])}),n&&(B[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function U(t,e){return t.isValid()?(e=G(e,t.localeData()),H[e]=H[e]||function(t){var e,n,i,a=t.match(E);for(e=0,n=a.length;e<n;e++)B[a[e]]?a[e]=B[a[e]]:a[e]=(i=a[e]).match(/\[[\s\S]/)?i.replace(/^\[|\]$/g,""):i.replace(/\\/g,"");return function(e){var i,r="";for(i=0;i<n;i++)r+=O(a[i])?a[i].call(e,t):a[i];return r}}(e),H[e](t)):t.localeData().invalidDate()}function G(t,e){var n=5;function i(t){return e.longDateFormat(t)||t}for(V.lastIndex=0;n>=0&&V.test(t);)t=t.replace(V,i),V.lastIndex=0,n-=1;return t}var q=/\d/,Z=/\d\d/,$=/\d{3}/,X=/\d{4}/,K=/[+-]?\d{6}/,J=/\d\d?/,Q=/\d\d\d\d?/,tt=/\d\d\d\d\d\d?/,et=/\d{1,3}/,nt=/\d{1,4}/,it=/[+-]?\d{1,6}/,at=/\d+/,rt=/[+-]?\d+/,ot=/Z|[+-]\d\d:?\d\d/gi,st=/Z|[+-]\d\d(?::?\d\d)?/gi,lt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,ut={};function dt(t,e,n){ut[t]=O(e)?e:function(t,i){return t&&n?n:e}}function ht(t,e){return h(ut,t)?ut[t](e._strict,e._locale):new RegExp(ct(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(t,e,n,i,a){return e||n||i||a}))))}function ct(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ft={};function gt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n<t.length;n++)ft[t[n]]=i}function mt(t,e){gt(t,(function(t,n,i,a){i._w=i._w||{},e(t,i._w,i,a)}))}function pt(t,e,n){null!=e&&h(ft,t)&&ft[t](e,n._a,n,t)}var vt=0,bt=1,yt=2,xt=3,_t=4,wt=5,kt=6,Mt=7,St=8;function Dt(t){return Ct(t)?366:365}function Ct(t){return t%4==0&&t%100!=0||t%400==0}j("Y",0,0,(function(){var t=this.year();return t<=9999?""+t:"+"+t})),j(0,["YY",2],0,(function(){return this.year()%100})),j(0,["YYYY",4],0,"year"),j(0,["YYYYY",5],0,"year"),j(0,["YYYYYY",6,!0],0,"year"),L("year","y"),Y("year",1),dt("Y",rt),dt("YY",J,Z),dt("YYYY",nt,X),dt("YYYYY",it,K),dt("YYYYYY",it,K),gt(["YYYYY","YYYYYY"],vt),gt("YYYY",(function(t,e){e[vt]=2===t.length?a.parseTwoDigitYear(t):k(t)})),gt("YY",(function(t,e){e[vt]=a.parseTwoDigitYear(t)})),gt("Y",(function(t,e){e[vt]=parseInt(t,10)})),a.parseTwoDigitYear=function(t){return k(t)+(k(t)>68?1900:2e3)};var Pt,Tt=Ot("FullYear",!0);function Ot(t,e){return function(n){return null!=n?(Ft(this,t,n),a.updateOffset(this,e),this):At(this,t)}}function At(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function Ft(t,e,n){t.isValid()&&!isNaN(n)&&("FullYear"===e&&Ct(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](n,t.month(),It(n,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](n))}function It(t,e){if(isNaN(t)||isNaN(e))return NaN;var n=function(t,e){return(t%e+e)%e}(e,12);return t+=(e-n)/12,1===n?Ct(t)?29:28:31-n%7%2}Pt=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e<this.length;++e)if(this[e]===t)return e;return-1},j("M",["MM",2],"Mo",(function(){return this.month()+1})),j("MMM",0,0,(function(t){return this.localeData().monthsShort(this,t)})),j("MMMM",0,0,(function(t){return this.localeData().months(this,t)})),L("month","M"),Y("month",8),dt("M",J),dt("MM",J,Z),dt("MMM",(function(t,e){return e.monthsShortRegex(t)})),dt("MMMM",(function(t,e){return e.monthsRegex(t)})),gt(["M","MM"],(function(t,e){e[bt]=k(t)-1})),gt(["MMM","MMMM"],(function(t,e,n,i){var a=n._locale.monthsParse(t,i,n._strict);null!=a?e[bt]=a:g(n).invalidMonth=t}));var Lt=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Rt="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Nt="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_");function Wt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],i=0;i<12;++i)r=f([2e3,i]),this._shortMonthsParse[i]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[i]=this.months(r,"").toLocaleLowerCase();return n?"MMM"===e?-1!==(a=Pt.call(this._shortMonthsParse,o))?a:null:-1!==(a=Pt.call(this._longMonthsParse,o))?a:null:"MMM"===e?-1!==(a=Pt.call(this._shortMonthsParse,o))?a:-1!==(a=Pt.call(this._longMonthsParse,o))?a:null:-1!==(a=Pt.call(this._longMonthsParse,o))?a:-1!==(a=Pt.call(this._shortMonthsParse,o))?a:null}function Yt(t,e){var n;if(!t.isValid())return t;if("string"==typeof e)if(/^\d+$/.test(e))e=k(e);else if(!l(e=t.localeData().monthsParse(e)))return t;return n=Math.min(t.date(),It(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t}function zt(t){return null!=t?(Yt(this,t),a.updateOffset(this,!0),this):At(this,"Month")}var Et=lt,Vt=lt;function Ht(){function t(t,e){return e.length-t.length}var e,n,i=[],a=[],r=[];for(e=0;e<12;e++)n=f([2e3,e]),i.push(this.monthsShort(n,"")),a.push(this.months(n,"")),r.push(this.months(n,"")),r.push(this.monthsShort(n,""));for(i.sort(t),a.sort(t),r.sort(t),e=0;e<12;e++)i[e]=ct(i[e]),a[e]=ct(a[e]);for(e=0;e<24;e++)r[e]=ct(r[e]);this._monthsRegex=new RegExp("^("+r.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+i.join("|")+")","i")}function Bt(t,e,n,i,a,r,o){var s;return t<100&&t>=0?(s=new Date(t+400,e,n,i,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,n,i,a,r,o),s}function jt(t){var e;if(t<100&&t>=0){var n=Array.prototype.slice.call(arguments);n[0]=t+400,e=new Date(Date.UTC.apply(null,n)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Ut(t,e,n){var i=7+e-n;return-(7+jt(t,0,i).getUTCDay()-e)%7+i-1}function Gt(t,e,n,i,a){var r,o,s=1+7*(e-1)+(7+n-i)%7+Ut(t,i,a);return s<=0?o=Dt(r=t-1)+s:s>Dt(t)?(r=t+1,o=s-Dt(t)):(r=t,o=s),{year:r,dayOfYear:o}}function qt(t,e,n){var i,a,r=Ut(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?i=o+Zt(a=t.year()-1,e,n):o>Zt(t.year(),e,n)?(i=o-Zt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Zt(t,e,n){var i=Ut(t,e,n),a=Ut(t+1,e,n);return(Dt(t)-i+a)/7}function $t(t,e){return t.slice(e,7).concat(t.slice(0,e))}j("w",["ww",2],"wo","week"),j("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),dt("w",J),dt("ww",J,Z),dt("W",J),dt("WW",J,Z),mt(["w","ww","W","WW"],(function(t,e,n,i){e[i.substr(0,1)]=k(t)})),j("d",0,"do","day"),j("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),j("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),j("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),j("e",0,0,"weekday"),j("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),dt("d",J),dt("e",J),dt("E",J),dt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),dt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),dt("dddd",(function(t,e){return e.weekdaysRegex(t)})),mt(["dd","ddd","dddd"],(function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t})),mt(["d","e","E"],(function(t,e,n,i){e[i]=k(t)}));var Xt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Kt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Qt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null}var te=lt,ee=lt,ne=lt;function ie(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ct(s[e]),l[e]=ct(l[e]),u[e]=ct(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function ae(){return this.hours()%12||12}function re(t,e){j(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function oe(t,e){return e._meridiemParse}j("H",["HH",2],0,"hour"),j("h",["hh",2],0,ae),j("k",["kk",2],0,(function(){return this.hours()||24})),j("hmm",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)})),j("hmmss",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)+z(this.seconds(),2)})),j("Hmm",0,0,(function(){return""+this.hours()+z(this.minutes(),2)})),j("Hmmss",0,0,(function(){return""+this.hours()+z(this.minutes(),2)+z(this.seconds(),2)})),re("a",!0),re("A",!1),L("hour","h"),Y("hour",13),dt("a",oe),dt("A",oe),dt("H",J),dt("h",J),dt("k",J),dt("HH",J,Z),dt("hh",J,Z),dt("kk",J,Z),dt("hmm",Q),dt("hmmss",tt),dt("Hmm",Q),dt("Hmmss",tt),gt(["H","HH"],xt),gt(["k","kk"],(function(t,e,n){var i=k(t);e[xt]=24===i?0:i})),gt(["a","A"],(function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t})),gt(["h","hh"],(function(t,e,n){e[xt]=k(t),g(n).bigHour=!0})),gt("hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i)),g(n).bigHour=!0})),gt("hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a)),g(n).bigHour=!0})),gt("Hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i))})),gt("Hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a))}));var se,le=Ot("Hours",!0),ue={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Xt,weekdaysMin:Jt,weekdaysShort:Kt,meridiemParse:/[ap]\.?m?\.?/i},de={},he={};function ce(t){return t?t.toLowerCase().replace("_","-"):t}function fe(n){var i=null;if(!de[n]&&e&&e.exports)try{i=se._abbr,t(),ge(i)}catch(t){}return de[n]}function ge(t,e){var n;return t&&((n=s(e)?pe(t):me(t,e))?se=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),se._abbr}function me(t,e){if(null!==e){var n,i=ue;if(e.abbr=t,null!=de[t])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),i=de[t]._config;else if(null!=e.parentLocale)if(null!=de[e.parentLocale])i=de[e.parentLocale]._config;else{if(null==(n=fe(e.parentLocale)))return he[e.parentLocale]||(he[e.parentLocale]=[]),he[e.parentLocale].push({name:t,config:e}),null;i=n._config}return de[t]=new F(A(i,e)),he[t]&&he[t].forEach((function(t){me(t.name,t.config)})),ge(t),de[t]}return delete de[t],null}function pe(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return se;if(!r(t)){if(e=fe(t))return e;t=[t]}return function(t){for(var e,n,i,a,r=0;r<t.length;){for(e=(a=ce(t[r]).split("-")).length,n=(n=ce(t[r+1]))?n.split("-"):null;e>0;){if(i=fe(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&M(a,n,!0)>=e-1)break;e--}r++}return se}(t)}function ve(t){var e,n=t._a;return n&&-2===g(t).overflow&&(e=n[bt]<0||n[bt]>11?bt:n[yt]<1||n[yt]>It(n[vt],n[bt])?yt:n[xt]<0||n[xt]>24||24===n[xt]&&(0!==n[_t]||0!==n[wt]||0!==n[kt])?xt:n[_t]<0||n[_t]>59?_t:n[wt]<0||n[wt]>59?wt:n[kt]<0||n[kt]>999?kt:-1,g(t)._overflowDayOfYear&&(e<vt||e>yt)&&(e=yt),g(t)._overflowWeeks&&-1===e&&(e=Mt),g(t)._overflowWeekday&&-1===e&&(e=St),g(t).overflow=e),t}function be(t,e,n){return null!=t?t:null!=e?e:n}function ye(t){var e,n,i,r,o,s=[];if(!t._d){for(i=function(t){var e=new Date(a.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[bt]&&function(t){var e,n,i,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,n=be(e.GG,t._a[vt],qt(Le(),1,4).year),i=be(e.W,1),((a=be(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=qt(Le(),r,o);n=be(e.gg,t._a[vt],u.year),i=be(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Zt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=Gt(n,i,a,r,o),t._a[vt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=be(t._a[vt],i[vt]),(t._dayOfYear>Dt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),n=jt(o,0,t._dayOfYear),t._a[bt]=n.getUTCMonth(),t._a[yt]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=i[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[xt]&&0===t._a[_t]&&0===t._a[wt]&&0===t._a[kt]&&(t._nextDay=!0,t._a[xt]=0),t._d=(t._useUTC?jt:Bt).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[xt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(g(t).weekdayMismatch=!0)}}var xe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_e=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,we=/Z|[+-]\d\d(?::?\d\d)?/,ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Me=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Se=/^\/?Date\((\-?\d+)/i;function De(t){var e,n,i,a,r,o,s=t._i,l=xe.exec(s)||_e.exec(s);if(l){for(g(t).iso=!0,e=0,n=ke.length;e<n;e++)if(ke[e][1].exec(l[1])){a=ke[e][0],i=!1!==ke[e][2];break}if(null==a)return void(t._isValid=!1);if(l[3]){for(e=0,n=Me.length;e<n;e++)if(Me[e][1].exec(l[3])){r=(l[2]||" ")+Me[e][0];break}if(null==r)return void(t._isValid=!1)}if(!i&&null!=r)return void(t._isValid=!1);if(l[4]){if(!we.exec(l[4]))return void(t._isValid=!1);o="Z"}t._f=a+(r||"")+(o||""),Ae(t)}else t._isValid=!1}var Ce=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;function Pe(t){var e=parseInt(t,10);return e<=49?2e3+e:e<=999?1900+e:e}var Te={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Oe(t){var e,n,i,a,r,o,s,l=Ce.exec(t._i.replace(/\([^)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,""));if(l){var u=(e=l[4],n=l[3],i=l[2],a=l[5],r=l[6],o=l[7],s=[Pe(e),Nt.indexOf(n),parseInt(i,10),parseInt(a,10),parseInt(r,10)],o&&s.push(parseInt(o,10)),s);if(!function(t,e,n){return!t||Kt.indexOf(t)===new Date(e[0],e[1],e[2]).getDay()||(g(n).weekdayMismatch=!0,n._isValid=!1,!1)}(l[1],u,t))return;t._a=u,t._tzm=function(t,e,n){if(t)return Te[t];if(e)return 0;var i=parseInt(n,10),a=i%100;return(i-a)/100*60+a}(l[8],l[9],l[10]),t._d=jt.apply(null,t._a),t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),g(t).rfc2822=!0}else t._isValid=!1}function Ae(t){if(t._f!==a.ISO_8601)if(t._f!==a.RFC_2822){t._a=[],g(t).empty=!0;var e,n,i,r,o,s=""+t._i,l=s.length,u=0;for(i=G(t._f,t._locale).match(E)||[],e=0;e<i.length;e++)r=i[e],(n=(s.match(ht(r,t))||[])[0])&&((o=s.substr(0,s.indexOf(n))).length>0&&g(t).unusedInput.push(o),s=s.slice(s.indexOf(n)+n.length),u+=n.length),B[r]?(n?g(t).empty=!1:g(t).unusedTokens.push(r),pt(r,n,t)):t._strict&&!n&&g(t).unusedTokens.push(r);g(t).charsLeftOver=l-u,s.length>0&&g(t).unusedInput.push(s),t._a[xt]<=12&&!0===g(t).bigHour&&t._a[xt]>0&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[xt]=function(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?((i=t.isPM(n))&&e<12&&(e+=12),i||12!==e||(e=0),e):e}(t._locale,t._a[xt],t._meridiem),ye(t),ve(t)}else Oe(t);else De(t)}function Fe(t){var e=t._i,n=t._f;return t._locale=t._locale||pe(t._l),null===e||void 0===n&&""===e?p({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new x(ve(e)):(u(e)?t._d=e:r(n)?function(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;a<t._f.length;a++)r=0,e=b({},t),null!=t._useUTC&&(e._useUTC=t._useUTC),e._f=t._f[a],Ae(e),m(e)&&(r+=g(e).charsLeftOver,r+=10*g(e).unusedTokens.length,g(e).score=r,(null==i||r<i)&&(i=r,n=e));c(t,n||e)}(t):n?Ae(t):function(t){var e=t._i;s(e)?t._d=new Date(a.now()):u(e)?t._d=new Date(e.valueOf()):"string"==typeof e?function(t){var e=Se.exec(t._i);null===e?(De(t),!1===t._isValid&&(delete t._isValid,Oe(t),!1===t._isValid&&(delete t._isValid,a.createFromInputFallback(t)))):t._d=new Date(+e[1])}(t):r(e)?(t._a=d(e.slice(0),(function(t){return parseInt(t,10)})),ye(t)):o(e)?function(t){if(!t._d){var e=N(t._i);t._a=d([e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],(function(t){return t&&parseInt(t,10)})),ye(t)}}(t):l(e)?t._d=new Date(e):a.createFromInputFallback(t)}(t),m(t)||(t._d=null),t))}function Ie(t,e,n,i,a){var s,l={};return!0!==n&&!1!==n||(i=n,n=void 0),(o(t)&&function(t){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(t).length;var e;for(e in t)if(t.hasOwnProperty(e))return!1;return!0}(t)||r(t)&&0===t.length)&&(t=void 0),l._isAMomentObject=!0,l._useUTC=l._isUTC=a,l._l=n,l._i=t,l._f=e,l._strict=i,(s=new x(ve(Fe(l))))._nextDay&&(s.add(1,"d"),s._nextDay=void 0),s}function Le(t,e,n,i){return Ie(t,e,n,i,!1)}a.createFromInputFallback=D("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",(function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))})),a.ISO_8601=function(){},a.RFC_2822=function(){};var Re=D("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",(function(){var t=Le.apply(null,arguments);return this.isValid()&&t.isValid()?t<this?this:t:p()})),Ne=D("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",(function(){var t=Le.apply(null,arguments);return this.isValid()&&t.isValid()?t>this?this:t:p()}));function We(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Le();for(n=e[0],i=1;i<e.length;++i)e[i].isValid()&&!e[i][t](n)||(n=e[i]);return n}var Ye=["year","quarter","month","week","day","hour","minute","second","millisecond"];function ze(t){var e=N(t),n=e.year||0,i=e.quarter||0,a=e.month||0,r=e.week||e.isoWeek||0,o=e.day||0,s=e.hour||0,l=e.minute||0,u=e.second||0,d=e.millisecond||0;this._isValid=function(t){for(var e in t)if(-1===Pt.call(Ye,e)||null!=t[e]&&isNaN(t[e]))return!1;for(var n=!1,i=0;i<Ye.length;++i)if(t[Ye[i]]){if(n)return!1;parseFloat(t[Ye[i]])!==k(t[Ye[i]])&&(n=!0)}return!0}(e),this._milliseconds=+d+1e3*u+6e4*l+1e3*s*60*60,this._days=+o+7*r,this._months=+a+3*i+12*n,this._data={},this._locale=pe(),this._bubble()}function Ee(t){return t instanceof ze}function Ve(t){return t<0?-1*Math.round(-1*t):Math.round(t)}function He(t,e){j(t,0,0,(function(){var t=this.utcOffset(),n="+";return t<0&&(t=-t,n="-"),n+z(~~(t/60),2)+e+z(~~t%60,2)}))}He("Z",":"),He("ZZ",""),dt("Z",st),dt("ZZ",st),gt(["Z","ZZ"],(function(t,e,n){n._useUTC=!0,n._tzm=je(st,t)}));var Be=/([\+\-]|\d\d)/gi;function je(t,e){var n=(e||"").match(t);if(null===n)return null;var i=((n[n.length-1]||[])+"").match(Be)||["-",0,0],a=60*i[1]+k(i[2]);return 0===a?0:"+"===i[0]?a:-a}function Ue(t,e){var n,i;return e._isUTC?(n=e.clone(),i=(_(t)||u(t)?t.valueOf():Le(t).valueOf())-n.valueOf(),n._d.setTime(n._d.valueOf()+i),a.updateOffset(n,!1),n):Le(t).local()}function Ge(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function qe(){return!!this.isValid()&&this._isUTC&&0===this._offset}a.updateOffset=function(){};var Ze=/^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,$e=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Xe(t,e){var n,i,a,r,o,s,u=t,d=null;return Ee(t)?u={ms:t._milliseconds,d:t._days,M:t._months}:l(t)?(u={},e?u[e]=t:u.milliseconds=t):(d=Ze.exec(t))?(n="-"===d[1]?-1:1,u={y:0,d:k(d[yt])*n,h:k(d[xt])*n,m:k(d[_t])*n,s:k(d[wt])*n,ms:k(Ve(1e3*d[kt]))*n}):(d=$e.exec(t))?(n="-"===d[1]?-1:1,u={y:Ke(d[2],n),M:Ke(d[3],n),w:Ke(d[4],n),d:Ke(d[5],n),h:Ke(d[6],n),m:Ke(d[7],n),s:Ke(d[8],n)}):null==u?u={}:"object"==typeof u&&("from"in u||"to"in u)&&(r=Le(u.from),o=Le(u.to),a=r.isValid()&&o.isValid()?(o=Ue(o,r),r.isBefore(o)?s=Je(r,o):((s=Je(o,r)).milliseconds=-s.milliseconds,s.months=-s.months),s):{milliseconds:0,months:0},(u={}).ms=a.milliseconds,u.M=a.months),i=new ze(u),Ee(t)&&h(t,"_locale")&&(i._locale=t._locale),i}function Ke(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Je(t,e){var n={};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function Qe(t,e){return function(n,i){var a;return null===i||isNaN(+i)||(T(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),a=n,n=i,i=a),tn(this,Xe(n="string"==typeof n?+n:n,i),t),this}}function tn(t,e,n,i){var r=e._milliseconds,o=Ve(e._days),s=Ve(e._months);t.isValid()&&(i=null==i||i,s&&Yt(t,At(t,"Month")+s*n),o&&Ft(t,"Date",At(t,"Date")+o*n),r&&t._d.setTime(t._d.valueOf()+r*n),i&&a.updateOffset(t,o||s))}Xe.fn=ze.prototype,Xe.invalid=function(){return Xe(NaN)};var en=Qe(1,"add"),nn=Qe(-1,"subtract");function an(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),i=t.clone().add(n,"months");return-(n+(e-i<0?(e-i)/(i-t.clone().add(n-1,"months")):(e-i)/(t.clone().add(n+1,"months")-i)))||0}function rn(t){var e;return void 0===t?this._locale._abbr:(null!=(e=pe(t))&&(this._locale=e),this)}a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var on=D("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",(function(t){return void 0===t?this.localeData():this.locale(t)}));function sn(){return this._locale}var ln=1e3,un=60*ln,dn=60*un,hn=3506328*dn;function cn(t,e){return(t%e+e)%e}function fn(t,e,n){return t<100&&t>=0?new Date(t+400,e,n)-hn:new Date(t,e,n).valueOf()}function gn(t,e,n){return t<100&&t>=0?Date.UTC(t+400,e,n)-hn:Date.UTC(t,e,n)}function mn(t,e){j(0,[t,t.length],0,e)}function pn(t,e,n,i,a){var r;return null==t?qt(this,i,a).year:(e>(r=Zt(t,i,a))&&(e=r),vn.call(this,t,e,n,i,a))}function vn(t,e,n,i,a){var r=Gt(t,e,n,i,a),o=jt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}j(0,["gg",2],0,(function(){return this.weekYear()%100})),j(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),mn("gggg","weekYear"),mn("ggggg","weekYear"),mn("GGGG","isoWeekYear"),mn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),dt("G",rt),dt("g",rt),dt("GG",J,Z),dt("gg",J,Z),dt("GGGG",nt,X),dt("gggg",nt,X),dt("GGGGG",it,K),dt("ggggg",it,K),mt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,n,i){e[i.substr(0,2)]=k(t)})),mt(["gg","GG"],(function(t,e,n,i){e[i]=a.parseTwoDigitYear(t)})),j("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),dt("Q",q),gt("Q",(function(t,e){e[bt]=3*(k(t)-1)})),j("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),dt("D",J),dt("DD",J,Z),dt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),gt(["D","DD"],yt),gt("Do",(function(t,e){e[yt]=k(t.match(J)[0])}));var bn=Ot("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),dt("DDD",et),dt("DDDD",$),gt(["DDD","DDDD"],(function(t,e,n){n._dayOfYear=k(t)})),j("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),dt("m",J),dt("mm",J,Z),gt(["m","mm"],_t);var yn=Ot("Minutes",!1);j("s",["ss",2],0,"second"),L("second","s"),Y("second",15),dt("s",J),dt("ss",J,Z),gt(["s","ss"],wt);var xn,_n=Ot("Seconds",!1);for(j("S",0,0,(function(){return~~(this.millisecond()/100)})),j(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),j(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),j(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),j(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),j(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),j(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),L("millisecond","ms"),Y("millisecond",16),dt("S",et,q),dt("SS",et,Z),dt("SSS",et,$),xn="SSSS";xn.length<=9;xn+="S")dt(xn,at);function wn(t,e){e[kt]=k(1e3*("0."+t))}for(xn="S";xn.length<=9;xn+="S")gt(xn,wn);var kn=Ot("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var Mn=x.prototype;function Sn(t){return t}Mn.add=en,Mn.calendar=function(t,e){var n=t||Le(),i=Ue(n,this).startOf("day"),r=a.calendarFormat(this,i)||"sameElse",o=e&&(O(e[r])?e[r].call(this,n):e[r]);return this.format(o||this.localeData().calendar(r,this,Le(n)))},Mn.clone=function(){return new x(this)},Mn.diff=function(t,e,n){var i,a,r;if(!this.isValid())return NaN;if(!(i=Ue(t,this)).isValid())return NaN;switch(a=6e4*(i.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=an(this,i)/12;break;case"month":r=an(this,i);break;case"quarter":r=an(this,i)/3;break;case"second":r=(this-i)/1e3;break;case"minute":r=(this-i)/6e4;break;case"hour":r=(this-i)/36e5;break;case"day":r=(this-i-a)/864e5;break;case"week":r=(this-i-a)/6048e5;break;default:r=this-i}return n?r:w(r)},Mn.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=dn-cn(e+(this._isUTC?0:this.utcOffset()*un),dn)-1;break;case"minute":e=this._d.valueOf(),e+=un-cn(e,un)-1;break;case"second":e=this._d.valueOf(),e+=ln-cn(e,ln)-1}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.format=function(t){t||(t=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var e=U(this,t);return this.localeData().postformat(e)},Mn.from=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.fromNow=function(t){return this.from(Le(),t)},Mn.to=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.toNow=function(t){return this.to(Le(),t)},Mn.get=function(t){return O(this[t=R(t)])?this[t]():this},Mn.invalidAt=function(){return g(this).overflow},Mn.isAfter=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()<this.clone().startOf(e).valueOf())},Mn.isBefore=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()<n.valueOf():this.clone().endOf(e).valueOf()<n.valueOf())},Mn.isBetween=function(t,e,n,i){var a=_(t)?t:Le(t),r=_(e)?e:Le(e);return!!(this.isValid()&&a.isValid()&&r.isValid())&&("("===(i=i||"()")[0]?this.isAfter(a,n):!this.isBefore(a,n))&&(")"===i[1]?this.isBefore(r,n):!this.isAfter(r,n))},Mn.isSame=function(t,e){var n,i=_(t)?t:Le(t);return!(!this.isValid()||!i.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()===i.valueOf():(n=i.valueOf(),this.clone().startOf(e).valueOf()<=n&&n<=this.clone().endOf(e).valueOf()))},Mn.isSameOrAfter=function(t,e){return this.isSame(t,e)||this.isAfter(t,e)},Mn.isSameOrBefore=function(t,e){return this.isSame(t,e)||this.isBefore(t,e)},Mn.isValid=function(){return m(this)},Mn.lang=on,Mn.locale=rn,Mn.localeData=sn,Mn.max=Ne,Mn.min=Re,Mn.parsingFlags=function(){return c({},g(this))},Mn.set=function(t,e){if("object"==typeof t)for(var n=function(t){var e=[];for(var n in t)e.push({unit:n,priority:W[n]});return e.sort((function(t,e){return t.priority-e.priority})),e}(t=N(t)),i=0;i<n.length;i++)this[n[i].unit](t[n[i].unit]);else if(O(this[t=R(t)]))return this[t](e);return this},Mn.startOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year(),0,1);break;case"quarter":e=n(this.year(),this.month()-this.month()%3,1);break;case"month":e=n(this.year(),this.month(),1);break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":e=n(this.year(),this.month(),this.date());break;case"hour":e=this._d.valueOf(),e-=cn(e+(this._isUTC?0:this.utcOffset()*un),dn);break;case"minute":e=this._d.valueOf(),e-=cn(e,un);break;case"second":e=this._d.valueOf(),e-=cn(e,ln)}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.subtract=nn,Mn.toArray=function(){var t=this;return[t.year(),t.month(),t.date(),t.hour(),t.minute(),t.second(),t.millisecond()]},Mn.toObject=function(){var t=this;return{years:t.year(),months:t.month(),date:t.date(),hours:t.hours(),minutes:t.minutes(),seconds:t.seconds(),milliseconds:t.milliseconds()}},Mn.toDate=function(){return new Date(this.valueOf())},Mn.toISOString=function(t){if(!this.isValid())return null;var e=!0!==t,n=e?this.clone().utc():this;return n.year()<0||n.year()>9999?U(n,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(n+i+"-MM-DD[T]HH:mm:ss.SSS"+a)},Mn.toJSON=function(){return this.isValid()?this.toISOString():null},Mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Mn.unix=function(){return Math.floor(this.valueOf()/1e3)},Mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},Mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Mn.year=Tt,Mn.isLeapYear=function(){return Ct(this.year())},Mn.weekYear=function(t){return pn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Mn.isoWeekYear=function(t){return pn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},Mn.quarter=Mn.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},Mn.month=zt,Mn.daysInMonth=function(){return It(this.year(),this.month())},Mn.week=Mn.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},Mn.isoWeek=Mn.isoWeeks=function(t){var e=qt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},Mn.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},Mn.isoWeeksInYear=function(){return Zt(this.year(),1,4)},Mn.date=bn,Mn.day=Mn.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},Mn.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},Mn.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},Mn.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},Mn.hour=Mn.hours=le,Mn.minute=Mn.minutes=yn,Mn.second=Mn.seconds=_n,Mn.millisecond=Mn.milliseconds=kn,Mn.utcOffset=function(t,e,n){var i,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=je(st,t)))return this}else Math.abs(t)<16&&!n&&(t*=60);return!this._isUTC&&e&&(i=Ge(this)),this._offset=t,this._isUTC=!0,null!=i&&this.add(i,"m"),r!==t&&(!e||this._changeInProgress?tn(this,Xe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Ge(this)},Mn.utc=function(t){return this.utcOffset(0,t)},Mn.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ge(this),"m")),this},Mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=je(ot,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},Mn.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Le(t).utcOffset():0,(this.utcOffset()-t)%60==0)},Mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mn.isUtc=qe,Mn.isUTC=qe,Mn.zoneAbbr=function(){return this._isUTC?"UTC":""},Mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mn.dates=D("dates accessor is deprecated. Use date instead.",bn),Mn.months=D("months accessor is deprecated. Use month instead",zt),Mn.years=D("years accessor is deprecated. Use year instead",Tt),Mn.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()})),Mn.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),(t=Fe(t))._a){var e=t._isUTC?f(t._a):Le(t._a);this._isDSTShifted=this.isValid()&&M(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var Dn=F.prototype;function Cn(t,e,n,i){var a=pe(),r=f().set(i,e);return a[n](r,t)}function Pn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return Cn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=Cn(t,i,n,"month");return a}function Tn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(n=e=t,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a,r=pe(),o=t?r._week.dow:0;if(null!=n)return Cn(e,(n+o)%7,i,"day");var s=[];for(a=0;a<7;a++)s[a]=Cn(e,(a+o)%7,i,"day");return s}Dn.calendar=function(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return O(i)?i.call(e,n):i},Dn.longDateFormat=function(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,(function(t){return t.slice(1)})),this._longDateFormat[t])},Dn.invalidDate=function(){return this._invalidDate},Dn.ordinal=function(t){return this._ordinal.replace("%d",t)},Dn.preparse=Sn,Dn.postformat=Sn,Dn.relativeTime=function(t,e,n,i){var a=this._relativeTime[n];return O(a)?a(t,e,n,i):a.replace(/%d/i,t)},Dn.pastFuture=function(t,e){var n=this._relativeTime[t>0?"future":"past"];return O(n)?n(e):n.replace(/%s/i,e)},Dn.set=function(t){var e,n;for(n in t)O(e=t[n])?this[n]=e:this["_"+n]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Dn.months=function(t,e){return t?r(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Lt).test(e)?"format":"standalone"][t.month()]:r(this._months)?this._months:this._months.standalone},Dn.monthsShort=function(t,e){return t?r(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Lt.test(e)?"format":"standalone"][t.month()]:r(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Dn.monthsParse=function(t,e,n){var i,a,r;if(this._monthsParseExact)return Wt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;i<12;i++){if(a=f([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}},Dn.monthsRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=Vt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},Dn.monthsShortRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=Et),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},Dn.week=function(t){return qt(t,this._week.dow,this._week.doy).week},Dn.firstDayOfYear=function(){return this._week.doy},Dn.firstDayOfWeek=function(){return this._week.dow},Dn.weekdays=function(t,e){var n=r(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?$t(n,this._week.dow):t?n[t.day()]:n},Dn.weekdaysMin=function(t){return!0===t?$t(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},Dn.weekdaysShort=function(t){return!0===t?$t(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},Dn.weekdaysParse=function(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Qt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}},Dn.weekdaysRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},Dn.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Dn.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ne),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Dn.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},Dn.meridiem=function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},ge("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),a.lang=D("moment.lang is deprecated. Use moment.locale instead.",ge),a.langData=D("moment.langData is deprecated. Use moment.localeData instead.",pe);var On=Math.abs;function An(t,e,n,i){var a=Xe(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Fn(t){return t<0?Math.floor(t):Math.ceil(t)}function In(t){return 4800*t/146097}function Ln(t){return 146097*t/4800}function Rn(t){return function(){return this.as(t)}}var Nn=Rn("ms"),Wn=Rn("s"),Yn=Rn("m"),zn=Rn("h"),En=Rn("d"),Vn=Rn("w"),Hn=Rn("M"),Bn=Rn("Q"),jn=Rn("y");function Un(t){return function(){return this.isValid()?this._data[t]:NaN}}var Gn=Un("milliseconds"),qn=Un("seconds"),Zn=Un("minutes"),$n=Un("hours"),Xn=Un("days"),Kn=Un("months"),Jn=Un("years"),Qn=Math.round,ti={ss:44,s:45,m:45,h:22,d:26,M:11};function ei(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}var ni=Math.abs;function ii(t){return(t>0)-(t<0)||+t}function ai(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n=ni(this._milliseconds)/1e3,i=ni(this._days),a=ni(this._months);t=w(n/60),e=w(t/60),n%=60,t%=60;var r=w(a/12),o=a%=12,s=i,l=e,u=t,d=n?n.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=ii(this._months)!==ii(h)?"-":"",g=ii(this._days)!==ii(h)?"-":"",m=ii(this._milliseconds)!==ii(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var ri=ze.prototype;return ri.isValid=function(){return this._isValid},ri.abs=function(){var t=this._data;return this._milliseconds=On(this._milliseconds),this._days=On(this._days),this._months=On(this._months),t.milliseconds=On(t.milliseconds),t.seconds=On(t.seconds),t.minutes=On(t.minutes),t.hours=On(t.hours),t.months=On(t.months),t.years=On(t.years),this},ri.add=function(t,e){return An(this,t,e,1)},ri.subtract=function(t,e){return An(this,t,e,-1)},ri.as=function(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+i/864e5,n=this._months+In(e),t){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(e=this._days+Math.round(Ln(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}},ri.asMilliseconds=Nn,ri.asSeconds=Wn,ri.asMinutes=Yn,ri.asHours=zn,ri.asDays=En,ri.asWeeks=Vn,ri.asMonths=Hn,ri.asQuarters=Bn,ri.asYears=jn,ri.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},ri._bubble=function(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Fn(Ln(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=w(r/1e3),l.seconds=t%60,e=w(t/60),l.minutes=e%60,n=w(e/60),l.hours=n%24,o+=w(n/24),a=w(In(o)),s+=a,o-=Fn(Ln(a)),i=w(s/12),s%=12,l.days=o,l.months=s,l.years=i,this},ri.clone=function(){return Xe(this)},ri.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},ri.milliseconds=Gn,ri.seconds=qn,ri.minutes=Zn,ri.hours=$n,ri.days=Xn,ri.weeks=function(){return w(this.days()/7)},ri.months=Kn,ri.years=Jn,ri.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=function(t,e,n){var i=Xe(t).abs(),a=Qn(i.as("s")),r=Qn(i.as("m")),o=Qn(i.as("h")),s=Qn(i.as("d")),l=Qn(i.as("M")),u=Qn(i.as("y")),d=a<=ti.ss&&["s",a]||a<ti.s&&["ss",a]||r<=1&&["m"]||r<ti.m&&["mm",r]||o<=1&&["h"]||o<ti.h&&["hh",o]||s<=1&&["d"]||s<ti.d&&["dd",s]||l<=1&&["M"]||l<ti.M&&["MM",l]||u<=1&&["y"]||["yy",u];return d[2]=e,d[3]=+t>0,d[4]=n,ei.apply(null,d)}(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)},ri.toISOString=ai,ri.toString=ai,ri.toJSON=ai,ri.locale=rn,ri.localeData=sn,ri.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ai),ri.lang=on,j("X",0,0,"unix"),j("x",0,0,"valueOf"),dt("x",rt),dt("X",/[+-]?\d+(\.\d{1,3})?/),gt("X",(function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))})),gt("x",(function(t,e,n){n._d=new Date(k(t))})),a.version="2.24.0",n=Le,a.fn=Mn,a.min=function(){return We("isBefore",[].slice.call(arguments,0))},a.max=function(){return We("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=f,a.unix=function(t){return Le(1e3*t)},a.months=function(t,e){return Pn(t,e,"months")},a.isDate=u,a.locale=ge,a.invalid=p,a.duration=Xe,a.isMoment=_,a.weekdays=function(t,e,n){return Tn(t,e,n,"weekdays")},a.parseZone=function(){return Le.apply(null,arguments).parseZone()},a.localeData=pe,a.isDuration=Ee,a.monthsShort=function(t,e){return Pn(t,e,"monthsShort")},a.weekdaysMin=function(t,e,n){return Tn(t,e,n,"weekdaysMin")},a.defineLocale=me,a.updateLocale=function(t,e){if(null!=e){var n,i,a=ue;null!=(i=fe(t))&&(a=i._config),e=A(a,e),(n=new F(e)).parentLocale=de[t],de[t]=n,ge(t)}else null!=de[t]&&(null!=de[t].parentLocale?de[t]=de[t].parentLocale:null!=de[t]&&delete de[t]);return de[t]},a.locales=function(){return C(de)},a.weekdaysShort=function(t,e,n){return Tn(t,e,n,"weekdaysShort")},a.normalizeUnits=R,a.relativeTimeRounding=function(t){return void 0===t?Qn:"function"==typeof t&&(Qn=t,!0)},a.relativeTimeThreshold=function(t,e){return void 0!==ti[t]&&(void 0===e?ti[t]:(ti[t]=e,"s"===t&&(ti.ss=e-1),!0))},a.calendarFormat=function(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},a.prototype=Mn,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()})),mi={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};rn._date.override("function"==typeof gi?{_id:"moment",formats:function(){return mi},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=gi(t,e):t instanceof gi||(t=gi(t)),t.isValid()?t.valueOf():null},format:function(t,e){return gi(t).format(e)},add:function(t,e,n){return gi(t).add(e,n).valueOf()},diff:function(t,e,n){return gi(t).diff(gi(e),n)},startOf:function(t,e,n){return t=gi(t),"isoWeek"===e?t.isoWeekday(n).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return gi(t).endOf(e).valueOf()},_create:function(t){return gi(t)}}:{}),W._set("global",{plugins:{filler:{propagate:!0}}});var pi={dataset:function(t){var e=t.fill,n=t.chart,i=n.getDatasetMeta(e),a=i&&n.isDatasetVisible(e)&&i.dataset._children||[],r=a.length||0;return r?function(t,e){return e<r&&a[e]._view||null}:null},boundary:function(t){var e=t.boundary,n=e?e.x:null,i=e?e.y:null;return H.isArray(e)?function(t,n){return e[n]}:function(t){return{x:null===n?t.x:n,y:null===i?t.y:i}}}};function vi(t,e,n){var i,a=t._model||{},r=a.fill;if(void 0===r&&(r=!!a.backgroundColor),!1===r||null===r)return!1;if(!0===r)return"origin";if(i=parseFloat(r,10),isFinite(i)&&Math.floor(i)===i)return"-"!==r[0]&&"+"!==r[0]||(i=e+i),!(i===e||i<0||i>=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function bi(t){return(t.el._scale||{}).getPointPositionForValue?function(t){var e,n,i,a,r,o=t.el._scale,s=o.options,l=o.chart.data.labels.length,u=t.fill,d=[];if(!l)return null;for(e=s.ticks.reverse?o.max:o.min,n=s.ticks.reverse?o.min:o.max,i=o.getPointPositionForValue(0,e),a=0;a<l;++a)r="start"===u||"end"===u?o.getPointPositionForValue(a,"start"===u?e:n):o.getBasePosition(a),s.gridLines.circular&&(r.cx=i.x,r.cy=i.y,r.angle=o.getIndexAngle(a)-Math.PI/2),d.push(r);return d}(t):function(t){var e,n=t.el._model||{},i=t.el._scale||{},a=t.fill,r=null;if(isFinite(a))return null;if("start"===a?r=void 0===n.scaleBottom?i.bottom:n.scaleBottom:"end"===a?r=void 0===n.scaleTop?i.top:n.scaleTop:void 0!==n.scaleZero?r=n.scaleZero:i.getBasePixel&&(r=i.getBasePixel()),null!=r){if(void 0!==r.x&&void 0!==r.y)return r;if(H.isFinite(r))return{x:(e=i.isHorizontal())?r:null,y:e?null:r}}return null}(t)}function yi(t,e,n){var i,a=t[e].fill,r=[e];if(!n)return a;for(;!1!==a&&-1===r.indexOf(a);){if(!isFinite(a))return a;if(!(i=t[a]))return!1;if(i.visible)return a;r.push(a),a=i.fill}return!1}function xi(t){var e=t.fill,n="dataset";return!1===e?null:(isFinite(e)||(n="boundary"),pi[n](t))}function _i(t){return t&&!t.skip}function wi(t,e,n,i,a){var r,o,s,l;if(i&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r<i;++r)H.canvas.lineTo(t,e[r-1],e[r]);if(void 0===n[0].angle)for(t.lineTo(n[a-1].x,n[a-1].y),r=a-1;r>0;--r)H.canvas.lineTo(t,n[r],n[r-1],!0);else for(o=n[0].cx,s=n[0].cy,l=Math.sqrt(Math.pow(n[0].x-o,2)+Math.pow(n[0].y-s,2)),r=a-1;r>0;--r)t.arc(o,s,l,n[r].angle,n[r-1].angle,!0)}}function ki(t,e,n,i,a,r){var o,s,l,u,d,h,c,f,g=e.length,m=i.spanGaps,p=[],v=[],b=0,y=0;for(t.beginPath(),o=0,s=g;o<s;++o)d=n(u=e[l=o%g]._view,l,i),h=_i(u),c=_i(d),r&&void 0===f&&h&&(s=g+(f=o+1)),h&&c?(b=p.push(u),y=v.push(d)):b&&y&&(m?(h&&p.push(u),c&&v.push(d)):(wi(t,p,v,b,y),b=y=0,p=[],v=[]));wi(t,p,v,b,y),t.closePath(),t.fillStyle=a,t.fill()}var Mi={id:"filler",afterDatasetsUpdate:function(t,e){var n,i,a,r,o=(t.data.datasets||[]).length,s=e.propagate,l=[];for(i=0;i<o;++i)r=null,(a=(n=t.getDatasetMeta(i)).dataset)&&a._model&&a instanceof wt.Line&&(r={visible:t.isDatasetVisible(i),fill:vi(a,i,o),chart:t,el:a}),n.$filler=r,l.push(r);for(i=0;i<o;++i)(r=l[i])&&(r.fill=yi(l,i,s),r.boundary=bi(r),r.mapper=xi(r))},beforeDatasetsDraw:function(t){var e,n,i,a,r,o,s,l=t._getSortedVisibleDatasetMetas(),u=t.ctx;for(n=l.length-1;n>=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||W.global.defaultColor,o&&s&&r.length&&(H.canvas.clipArea(u,t.chartArea),ki(u,r,o,a,s,i._loop),H.canvas.unclipArea(u)))}},Si=H.rtl.getRtlAdapter,Di=H.noop,Ci=H.valueOrDefault;function Pi(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}W._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;e<n;e++)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=r[e].backgroundColor,r[e].label&&i.appendChild(document.createTextNode(r[e].label));return a.outerHTML}});var Ti=$.extend({initialize:function(t){H.extend(this,t),this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1},beforeUpdate:Di,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:Di,beforeSetDimensions:Di,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:Di,beforeBuildLabels:Di,buildLabels:function(){var t=this,e=t.options.labels||{},n=H.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(n=n.filter((function(n){return e.filter(n,t.chart.data)}))),t.options.reverse&&n.reverse(),t.legendItems=n},afterBuildLabels:Di,beforeFit:Di,fit:function(){var t=this,e=t.options,n=e.labels,i=e.display,a=t.ctx,r=H.options._parseFont(n),o=r.size,s=t.legendHitBoxes=[],l=t.minSize,u=t.isHorizontal();if(u?(l.width=t.maxWidth,l.height=i?10:0):(l.width=i?10:0,l.height=t.maxHeight),i){if(a.font=r.string,u){var d=t.lineWidths=[0],h=0;a.textAlign="left",a.textBaseline="middle",H.each(t.legendItems,(function(t,e){var i=Pi(n,o)+o/2+a.measureText(t.text).width;(0===e||d[d.length-1]+i+2*n.padding>l.width)&&(h+=o+n.padding,d[d.length-(e>0?0:1)]=0),s[e]={left:0,top:0,width:i,height:o},d[d.length-1]+=i+n.padding})),l.height+=h}else{var c=n.padding,f=t.columnWidths=[],g=t.columnHeights=[],m=n.padding,p=0,v=0;H.each(t.legendItems,(function(t,e){var i=Pi(n,o)+o/2+a.measureText(t.text).width;e>0&&v+o+2*c>l.height&&(m+=p+n.padding,f.push(p),g.push(v),p=0,v=0),p=Math.max(p,i),v+=o+c,s[e]={left:0,top:0,width:i,height:o}})),m+=p,f.push(p),g.push(v),l.width+=m}t.width=l.width,t.height=l.height}else t.width=l.width=t.height=l.height=0},afterFit:Di,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,n=e.labels,i=W.global,a=i.defaultColor,r=i.elements.line,o=t.height,s=t.columnHeights,l=t.width,u=t.lineWidths;if(e.display){var d,h=Si(e.rtl,t.left,t.minSize.width),c=t.ctx,f=Ci(n.fontColor,i.defaultFontColor),g=H.options._parseFont(n),m=g.size;c.textAlign=h.textAlign("left"),c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=g.string;var p=Pi(n,m),v=t.legendHitBoxes,b=function(t,i){switch(e.align){case"start":return n.padding;case"end":return t-i;default:return(t-i+n.padding)/2}},y=t.isHorizontal();d=y?{x:t.left+b(l,u[0]),y:t.top+n.padding,line:0}:{x:t.left+n.padding,y:t.top+b(o,s[0]),line:0},H.rtl.overrideTextDirection(t.ctx,e.textDirection);var x=m+n.padding;H.each(t.legendItems,(function(e,i){var f=c.measureText(e.text).width,g=p+m/2+f,_=d.x,w=d.y;h.setWidth(t.minSize.width),y?i>0&&_+g+n.padding>t.left+t.minSize.width&&(w=d.y+=x,d.line++,_=d.x=t.left+b(l,u[d.line])):i>0&&w+x>t.top+t.minSize.height&&(_=d.x=_+t.columnWidths[d.line]+n.padding,d.line++,w=d.y=t.top+b(o,s[d.line]));var k=h.x(_);!function(t,e,i){if(!(isNaN(p)||p<=0)){c.save();var o=Ci(i.lineWidth,r.borderWidth);if(c.fillStyle=Ci(i.fillStyle,a),c.lineCap=Ci(i.lineCap,r.borderCapStyle),c.lineDashOffset=Ci(i.lineDashOffset,r.borderDashOffset),c.lineJoin=Ci(i.lineJoin,r.borderJoinStyle),c.lineWidth=o,c.strokeStyle=Ci(i.strokeStyle,a),c.setLineDash&&c.setLineDash(Ci(i.lineDash,r.borderDash)),n&&n.usePointStyle){var s=p*Math.SQRT2/2,l=h.xPlus(t,p/2),u=e+m/2;H.canvas.drawPoint(c,i.pointStyle,s,l,u,i.rotation)}else c.fillRect(h.leftForLtr(t,p),e,p,m),0!==o&&c.strokeRect(h.leftForLtr(t,p),e,p,m);c.restore()}}(k,w,e),v[i].left=h.leftForLtr(k,v[i].width),v[i].top=w,function(t,e,n,i){var a=m/2,r=h.xPlus(t,p+a),o=e+a;c.fillText(n.text,r,o),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(r,o),c.lineTo(h.xPlus(r,i),o),c.stroke())}(k,w,e,f),y?d.x+=g+n.padding:d.y+=x})),H.rtl.restoreTextDirection(t.ctx,e.textDirection)}},_getLegendItemAt:function(t,e){var n,i,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,n=0;n<a.length;++n)if(t>=(i=a[n]).left&&t<=i.left+i.width&&e>=i.top&&e<=i.top+i.height)return r.legendItems[n];return null},handleEvent:function(t){var e,n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover&&!i.onLeave)return}else{if("click"!==a)return;if(!i.onClick)return}e=n._getLegendItemAt(t.x,t.y),"click"===a?e&&i.onClick&&i.onClick.call(n,t.native,e):(i.onLeave&&e!==n._hoveredItem&&(n._hoveredItem&&i.onLeave.call(n,t.native,n._hoveredItem),n._hoveredItem=e),i.onHover&&e&&i.onHover.call(n,t.native,e))}});function Oi(t,e){var n=new Ti({ctx:t.ctx,options:e,chart:t});me.configure(t,n,e),me.addBox(t,n),t.legend=n}var Ai={id:"legend",_element:Ti,beforeInit:function(t){var e=t.options.legend;e&&Oi(t,e)},beforeUpdate:function(t){var e=t.options.legend,n=t.legend;e?(H.mergeIf(e,W.global.legend),n?(me.configure(t,n,e),n.options=e):Oi(t,e)):n&&(me.removeBox(t,n),delete t.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}},Fi=H.noop;W._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Ii=$.extend({initialize:function(t){H.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:Fi,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:Fi,beforeSetDimensions:Fi,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:Fi,beforeBuildLabels:Fi,buildLabels:Fi,afterBuildLabels:Fi,beforeFit:Fi,fit:function(){var t,e=this,n=e.options,i=e.minSize={},a=e.isHorizontal();n.display?(t=(H.isArray(n.text)?n.text.length:1)*H.options._parseFont(n).lineHeight+2*n.padding,e.width=i.width=a?e.maxWidth:t,e.height=i.height=a?t:e.maxHeight):e.width=i.width=e.height=i.height=0},afterFit:Fi,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,n=t.options;if(n.display){var i,a,r,o=H.options._parseFont(n),s=o.lineHeight,l=s/2+n.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=H.valueOrDefault(n.fontColor,W.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,i=f-h):(a="left"===n.position?h+l:f-l,r=d+(c-d)/2,i=c-d,u=Math.PI*("left"===n.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=n.text;if(H.isArray(g))for(var m=0,p=0;p<g.length;++p)e.fillText(g[p],0,m,i),m+=s;else e.fillText(g,0,0,i);e.restore()}}});function Li(t,e){var n=new Ii({ctx:t.ctx,options:e,chart:t});me.configure(t,n,e),me.addBox(t,n),t.titleBlock=n}var Ri={},Ni=Mi,Wi=Ai,Yi={id:"title",_element:Ii,beforeInit:function(t){var e=t.options.title;e&&Li(t,e)},beforeUpdate:function(t){var e=t.options.title,n=t.titleBlock;e?(H.mergeIf(e,W.global.title),n?(me.configure(t,n,e),n.options=e):Li(t,e)):n&&(me.removeBox(t,n),delete t.titleBlock)}};for(var zi in Ri.filler=Ni,Ri.legend=Wi,Ri.title=Yi,en.helpers=H,function(){function t(t,e,n){var i;return"string"==typeof t?(i=parseInt(t,10),-1!==t.indexOf("%")&&(i=i/100*e.parentNode[n])):i=t,i}function e(t){return null!=t&&"none"!==t}function n(n,i,a){var r=document.defaultView,o=H._getParentNode(n),s=r.getComputedStyle(n)[i],l=r.getComputedStyle(o)[i],u=e(s),d=e(l),h=Number.POSITIVE_INFINITY;return u||d?Math.min(u?t(s,n,a):h,d?t(l,o,a):h):"none"}H.where=function(t,e){if(H.isArray(t)&&Array.prototype.filter)return t.filter(e);var n=[];return H.each(t,(function(t){e(t)&&n.push(t)})),n},H.findIndex=Array.prototype.findIndex?function(t,e,n){return t.findIndex(e,n)}:function(t,e,n){n=void 0===n?t:n;for(var i=0,a=t.length;i<a;++i)if(e.call(n,t[i],i,t))return i;return-1},H.findNextWhere=function(t,e,n){H.isNullOrUndef(n)&&(n=-1);for(var i=n+1;i<t.length;i++){var a=t[i];if(e(a))return a}},H.findPreviousWhere=function(t,e,n){H.isNullOrUndef(n)&&(n=t.length);for(var i=n-1;i>=0;i--){var a=t[i];if(e(a))return a}},H.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},H.almostEquals=function(t,e,n){return Math.abs(t-e)<n},H.almostWhole=function(t,e){var n=Math.round(t);return n-e<=t&&n+e>=t},H.max=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.max(t,e)}),Number.NEGATIVE_INFINITY)},H.min=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.min(t,e)}),Number.POSITIVE_INFINITY)},H.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},H.toRadians=function(t){return t*(Math.PI/180)},H.toDegrees=function(t){return t*(180/Math.PI)},H._decimalPlaces=function(t){if(H.isFinite(t)){for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}},H.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},H.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},H.aliasPixel=function(t){return t%2==0?0:.5},H._alignPixel=function(t,e,n){var i=t.currentDevicePixelRatio,a=n/2;return Math.round((e-a)*i)/i+a},H.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=i*(u=isNaN(u)?0:u),c=i*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},H.EPSILON=Number.EPSILON||1e-14,H.splineCurveMonotone=function(t){var e,n,i,a,r,o,s,l,u,d=(t||[]).map((function(t){return{model:t._model,deltaK:0,mK:0}})),h=d.length;for(e=0;e<h;++e)if(!(i=d[e]).model.skip){if(n=e>0?d[e-1]:null,(a=e<h-1?d[e+1]:null)&&!a.model.skip){var c=a.model.x-i.model.x;i.deltaK=0!==c?(a.model.y-i.model.y)/c:0}!n||n.model.skip?i.mK=i.deltaK:!a||a.model.skip?i.mK=n.deltaK:this.sign(n.deltaK)!==this.sign(i.deltaK)?i.mK=0:i.mK=(n.deltaK+i.deltaK)/2}for(e=0;e<h-1;++e)i=d[e],a=d[e+1],i.model.skip||a.model.skip||(H.almostEquals(i.deltaK,0,this.EPSILON)?i.mK=a.mK=0:(r=i.mK/i.deltaK,o=a.mK/i.deltaK,(l=Math.pow(r,2)+Math.pow(o,2))<=9||(s=3/Math.sqrt(l),i.mK=r*s*i.deltaK,a.mK=o*s*i.deltaK)));for(e=0;e<h;++e)(i=d[e]).model.skip||(n=e>0?d[e-1]:null,a=e<h-1?d[e+1]:null,n&&!n.model.skip&&(u=(i.model.x-n.model.x)/3,i.model.controlPointPreviousX=i.model.x-u,i.model.controlPointPreviousY=i.model.y-u*i.mK),a&&!a.model.skip&&(u=(a.model.x-i.model.x)/3,i.model.controlPointNextX=i.model.x+u,i.model.controlPointNextY=i.model.y+u*i.mK))},H.nextItem=function(t,e,n){return n?e>=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},H.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},H.niceNum=function(t,e){var n=Math.floor(H.log10(t)),i=t/Math.pow(10,n);return(e?i<1.5?1:i<3?2:i<7?5:10:i<=1?1:i<=2?2:i<=5?5:10)*Math.pow(10,n)},H.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},H.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(n=s[0].clientX,i=s[0].clientY):(n=a.clientX,i=a.clientY);var l=parseFloat(H.getStyle(r,"padding-left")),u=parseFloat(H.getStyle(r,"padding-top")),d=parseFloat(H.getStyle(r,"padding-right")),h=parseFloat(H.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:n=Math.round((n-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:i=Math.round((i-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},H.getConstraintWidth=function(t){return n(t,"max-width","clientWidth")},H.getConstraintHeight=function(t){return n(t,"max-height","clientHeight")},H._calculatePadding=function(t,e,n){return(e=H.getStyle(t,e)).indexOf("%")>-1?n*parseInt(e,10)/100:parseInt(e,10)},H._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},H.getMaximumWidth=function(t){var e=H._getParentNode(t);if(!e)return t.clientWidth;var n=e.clientWidth,i=n-H._calculatePadding(e,"padding-left",n)-H._calculatePadding(e,"padding-right",n),a=H.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},H.getMaximumHeight=function(t){var e=H._getParentNode(t);if(!e)return t.clientHeight;var n=e.clientHeight,i=n-H._calculatePadding(e,"padding-top",n)-H._calculatePadding(e,"padding-bottom",n),a=H.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},H.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},H.retinaScale=function(t,e){var n=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==n){var i=t.canvas,a=t.height,r=t.width;i.height=a*n,i.width=r*n,t.ctx.scale(n,n),i.style.height||i.style.width||(i.style.height=a+"px",i.style.width=r+"px")}},H.fontString=function(t,e,n){return e+" "+t+"px "+n},H.longestText=function(t,e,n,i){var a=(i=i||{}).data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var o,s,l,u,d,h=0,c=n.length;for(o=0;o<c;o++)if(null!=(u=n[o])&&!0!==H.isArray(u))h=H.measureText(t,a,r,h,u);else if(H.isArray(u))for(s=0,l=u.length;s<l;s++)null==(d=u[s])||H.isArray(d)||(h=H.measureText(t,a,r,h,d));var f=r.length/2;if(f>n.length){for(o=0;o<f;o++)delete a[r[o]];r.splice(0,f)}return h},H.measureText=function(t,e,n,i,a){var r=e[a];return r||(r=e[a]=t.measureText(a).width,n.push(a)),r>i&&(i=r),i},H.numberOfLabelLines=function(t){var e=1;return H.each(t,(function(t){H.isArray(t)&&t.length>e&&(e=t.length)})),e},H.color=k?function(t){return t instanceof CanvasGradient&&(t=W.global.defaultColor),k(t)}:function(t){return console.error("Color.js not found!"),t},H.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:H.color(t).saturate(.5).darken(.1).rgbString()}}(),en._adapters=rn,en.Animation=K,en.animationService=J,en.controllers=Jt,en.DatasetController=it,en.defaults=W,en.Element=$,en.elements=wt,en.Interaction=re,en.layouts=me,en.platform=Ie,en.plugins=Le,en.Scale=xn,en.scaleService=Re,en.Ticks=on,en.Tooltip=Ge,en.helpers.each(fi,(function(t,e){en.scaleService.registerScaleType(e,t,t._defaults)})),Ri)Ri.hasOwnProperty(zi)&&en.plugins.register(Ri[zi]);en.platform.initialize();var Ei=en;return"undefined"!=typeof window&&(window.Chart=en),en.Chart=en,en.Legend=Ri.legend._element,en.Title=Ri.title._element,en.pluginService=en.plugins,en.PluginBase=en.Element.extend({}),en.canvasHelpers=en.helpers.canvas,en.layoutService=en.layouts,en.LinearScaleBase=Dn,en.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],(function(t){en[t]=function(e,n){return new en(e,en.helpers.merge(n||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}})),Ei})); diff --git a/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js b/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js new file mode 100644 index 000000000..4c5be4c0f --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width",!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoundingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.duration=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r}); diff --git a/pikabu_ru/profile_rating__tracking/templates/index.html b/pikabu_ru/profile_rating__tracking/templates/index.html new file mode 100644 index 000000000..ca992355f --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/templates/index.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Pikabu profile rating + + + + + + + + + + + +
    +
    +
    + + + + + {% for item in items %} + + {% endfor %} + + + + {% for item in items %} + + {% endfor %} + + +
    DATE:{{ item.date.strftime('%d/%m/%Y') }}
    VALUE:{{ item.value }}
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/web.py b/pikabu_ru/profile_rating__tracking/web.py new file mode 100644 index 000000000..ad41cb934 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/web.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from flask import Flask, render_template +from db import ProfileRating + + +app = Flask(__name__) + + +@app.route("/") +def index(): + items = ProfileRating.select().order_by(ProfileRating.id.desc()) + return render_template("index.html", items=items) + + +if __name__ == "__main__": + app.debug = True + app.run(port=10017) diff --git a/pikabu_ru_page_interview_fullstack__question_19/main.py b/pikabu_ru_page_interview_fullstack__question_19/main.py deleted file mode 100644 index 650524aad..000000000 --- a/pikabu_ru_page_interview_fullstack__question_19/main.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://pikabu.ru/page/interview/fullstack/ -# 19. Секретный агент Пикабу передал зашифрованное изображение. Вам необходимо расшифровать изображение и вывести его -# на страницу средствами JS (без сторонних библиотек). Алгоритм дешифрования известен: -# * пиксели перебираются слева направо для каждой строки; -# * для каждого пикселя вычисляется параметр s += x + y * 80 (изначально s = 0 и для каждого следующего пикселя -# значение увеличивается на x + y * 80), где x - колонка пикселя, y - строка; -# * для канала красного и синего цвета необходимо добавить параметр s; -# * для канала зеленого цвета необходимо отнять параметр s следующим образом: green = (green - s) & 0xff; - - -# pip install Pillow -from PIL import Image - -image = Image.open('154800137443598227.png') -width, height = image.size -pixel = image.load() - -s = 0 - -for y in range(height): - for x in range(width): - s += x + y * 80 - - # RGBa, alpha-канал нас не интересует, только RGB - r, g, b, _ = pixel[x, y] - - r = (r + s) & 0xff - g = (g - s) & 0xff - b = (b + s) & 0xff - - pixel[x, y] = r, g, b - - -image.save('result.png') diff --git a/pil_pillow__examples/blur/main.py b/pil_pillow__examples/blur/main.py index ed8b73d7c..7ccb6bbd1 100644 --- a/pil_pillow__examples/blur/main.py +++ b/pil_pillow__examples/blur/main.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageFilter + image_file = "input.jpg" img = Image.open(image_file) img = img.filter(ImageFilter.GaussianBlur(2)) -img.save('output.png') +img.save("output.png") img.show() diff --git a/pil_pillow__examples/bytes_to_image/main.py b/pil_pillow__examples/bytes_to_image/main.py index 945ac776f..7b3192433 100644 --- a/pil_pillow__examples/bytes_to_image/main.py +++ b/pil_pillow__examples/bytes_to_image/main.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -with open('input.jpg', 'rb') as f: - data = f.read() - import io -data_io = io.BytesIO(data) # pip install Pillow from PIL import Image + + +with open("input.jpg", "rb") as f: + data = f.read() + +data_io = io.BytesIO(data) img = Image.open(data_io) print(img) diff --git a/pil_pillow__examples/crop.py b/pil_pillow__examples/crop.py index d1fdb421b..224192eff 100644 --- a/pil_pillow__examples/crop.py +++ b/pil_pillow__examples/crop.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow diff --git a/pil_pillow__examples/crop_groups.py b/pil_pillow__examples/crop_groups.py index f4dcd32af..d48bf3ece 100644 --- a/pil_pillow__examples/crop_groups.py +++ b/pil_pillow__examples/crop_groups.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path @@ -12,7 +12,7 @@ path = Path(...) -for file_name in path.glob('*.jpg'): +for file_name in path.glob("*.jpg"): img = Image.open(file_name) width, height = img.size diff --git a/pil_pillow__examples/darker_picture/darker_picture.py b/pil_pillow__examples/darker_picture/darker_picture.py index 10fce76e4..4292d4f6a 100644 --- a/pil_pillow__examples/darker_picture/darker_picture.py +++ b/pil_pillow__examples/darker_picture/darker_picture.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + image_file = "Collapse all.png" -img = Image.open(image_file).convert('RGBA') +img = Image.open(image_file).convert("RGBA") pixdata = img.load() @@ -16,6 +17,8 @@ for y in range(img.size[1]): for x in range(img.size[0]): if pixdata[x, y][:3] != (255, 255, 255): - pixdata[x, y] = tuple(map(lambda x: x - 50, pixdata[x, y][:3])) + pixdata[x, y][3:] + pixdata[x, y] = ( + tuple(map(lambda x: x - 50, pixdata[x, y][:3])) + pixdata[x, y][3:] + ) img.save("Collapse all black.png") diff --git a/pil_pillow__examples/draw watermark/main.py b/pil_pillow__examples/draw watermark/main.py index 70e747df3..30237ba53 100644 --- a/pil_pillow__examples/draw watermark/main.py +++ b/pil_pillow__examples/draw watermark/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -25,6 +25,6 @@ for i in range(0, width, width_text * 2): for j in range(0, height, height_text * 2): - drawer.text((i, j), text, font=font, fill=(0x00, 0xff, 0x00)) + drawer.text((i, j), text, font=font, fill=(0x00, 0xFF, 0x00)) image.show() diff --git a/pil_pillow__examples/draw_progress_on_image/main.py b/pil_pillow__examples/draw_progress_on_image/main.py index ed99db174..d6aea02da 100644 --- a/pil_pillow__examples/draw_progress_on_image/main.py +++ b/pil_pillow__examples/draw_progress_on_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -33,11 +33,11 @@ def draw_progress(image: Image, percent: int) -> Image: return image -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) - + for percent in (5, 15, 75, 100): img = draw_progress(image, percent) - img.save(f'output/image_{percent}%.png') + img.save(f"output/image_{percent}%.png") img.show() diff --git a/pil_pillow__examples/draw_times_cyrillic/main.py b/pil_pillow__examples/draw_times_cyrillic/main.py new file mode 100644 index 000000000..c9d42ccdf --- /dev/null +++ b/pil_pillow__examples/draw_times_cyrillic/main.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://ru.stackoverflow.com/q/1236348/201445 + + +from PIL import ImageFont, Image, ImageDraw + + +PADDING = 25 +LINE_SIZE = 2 +BOTTOM_PADDING = 150 +FONTS = "fonts" + + +def set_text(template, texts): + font_upper = ImageFont.truetype(f"{FONTS}/times.ttf", 64, encoding="utf-8") + font_lower = ImageFont.truetype(f"{FONTS}/times.ttf", 46, encoding="utf-8") + + draw_upper_text = ImageDraw.Draw(template) + upper_text_width, _ = draw_upper_text.textsize(texts[0], font_upper) + draw_upper_text.text( + ( + (template.width - upper_text_width) / 2, + template.height - BOTTOM_PADDING + 10, + ), + texts[0], + fill="white", + font=font_upper, + ) + + draw_lower_text = ImageDraw.Draw(template) + lower_text_width, _ = draw_lower_text.textsize(texts[1], font_lower) + draw_lower_text.text( + ( + (template.width - lower_text_width) / 2, + template.height - BOTTOM_PADDING + (PADDING * 4) - 10, + ), + texts[1], + fill="white", + font=font_lower, + ) + return template + + +if __name__ == "__main__": + img = Image.new("RGB", (400, 300), "black") + + set_text(img, ["ПРИВЕТ", "HELLO WORLD!"]) + + img.show() diff --git a/pil_pillow__examples/draw_times_cyrillic/times.ttf b/pil_pillow__examples/draw_times_cyrillic/times.ttf new file mode 100644 index 000000000..eaf5e1164 Binary files /dev/null and b/pil_pillow__examples/draw_times_cyrillic/times.ttf differ diff --git a/pil_pillow__examples/fast_average_color/README.md b/pil_pillow__examples/fast_average_color/README.md new file mode 100644 index 000000000..dc33ec4bd --- /dev/null +++ b/pil_pillow__examples/fast_average_color/README.md @@ -0,0 +1,6 @@ +| input/ | output/ | +| --- | --- | +| ![input/2.jpg](input/2.jpg) | ![output/2.jpg](output/2.jpg) | +| ![input/11.jpg](input/11.jpg) | ![output/11.jpg](output/11.jpg) | +| ![input/14.jpg](input/14.jpg) | ![output/14.jpg](output/14.jpg) | +| ![input/22.jpg](input/22.jpg) | ![output/22.jpg](output/22.jpg) | diff --git a/pil_pillow__examples/fast_average_color/input/11.jpg b/pil_pillow__examples/fast_average_color/input/11.jpg new file mode 100644 index 000000000..833116052 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/input/11.jpg differ diff --git a/pil_pillow__examples/fast_average_color/input/14.jpg b/pil_pillow__examples/fast_average_color/input/14.jpg new file mode 100644 index 000000000..581e56cdd Binary files /dev/null and b/pil_pillow__examples/fast_average_color/input/14.jpg differ diff --git a/pil_pillow__examples/fast_average_color/input/2.jpg b/pil_pillow__examples/fast_average_color/input/2.jpg new file mode 100644 index 000000000..886eab18d Binary files /dev/null and b/pil_pillow__examples/fast_average_color/input/2.jpg differ diff --git a/pil_pillow__examples/fast_average_color/input/22.jpg b/pil_pillow__examples/fast_average_color/input/22.jpg new file mode 100644 index 000000000..22b14b740 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/input/22.jpg differ diff --git a/pil_pillow__examples/fast_average_color/main.py b/pil_pillow__examples/fast_average_color/main.py new file mode 100644 index 000000000..0fbeb875f --- /dev/null +++ b/pil_pillow__examples/fast_average_color/main.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import math +from pathlib import Path + +from PIL import Image + + +DIR = Path(__file__).resolve().parent + + +def get_mode_correction(image: Image) -> str: + if image.mode not in ["RGB", "RGBA"]: + return "RGB" + return image.mode + + +# SOURCE: https://github.com/fast-average-color/fast-average-color/blob/15561aa55a36702f0b1af3d0ef611a205f8d56b5/src/algorithm/sqrt.js#L3 +def sqrt_algorithm(image: Image) -> tuple[int, int, int, int]: + mode = get_mode_correction(image) + if mode != image.mode: + image = image.convert("RGB") + + red_total = 0 + green_total = 0 + blue_total = 0 + alpha_total = 0 + count = 0 + + pixel = image.load() + + for i in range(image.width): + for j in range(image.height): + color = pixel[i, j] + if len(color) == 4: + red, green, blue, alpha = color + else: + [red, green, blue], alpha = color, 255 + + red_total += red * red * alpha + green_total += green * green * alpha + blue_total += blue * blue * alpha + alpha_total += alpha + + count += 1 + + return ( + round(math.sqrt(red_total / alpha_total)), + round(math.sqrt(green_total / alpha_total)), + round(math.sqrt(blue_total / alpha_total)), + round(alpha_total / count), + ) + + +def draw_example(image: Image, margin=70) -> Image: + rgba = sqrt_algorithm(image) + + width = image.width + margin + margin + height = image.height + margin + margin + + mode = get_mode_correction(image) + + image_output = Image.new(mode, (width, height), rgba) + image_output.paste(image, (margin, margin)) + + return image_output + + +if __name__ == "__main__": + for file_name in DIR.glob("input/*.*"): + image = Image.open(file_name) + image_output = draw_example(image) + # image_output.show() + image_output.save(DIR / "output" / file_name.name) diff --git a/pil_pillow__examples/fast_average_color/output/11.jpg b/pil_pillow__examples/fast_average_color/output/11.jpg new file mode 100644 index 000000000..8ac813a80 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/output/11.jpg differ diff --git a/pil_pillow__examples/fast_average_color/output/14.jpg b/pil_pillow__examples/fast_average_color/output/14.jpg new file mode 100644 index 000000000..6eca789d6 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/output/14.jpg differ diff --git a/pil_pillow__examples/fast_average_color/output/2.jpg b/pil_pillow__examples/fast_average_color/output/2.jpg new file mode 100644 index 000000000..9ac9a3800 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/output/2.jpg differ diff --git a/pil_pillow__examples/fast_average_color/output/22.jpg b/pil_pillow__examples/fast_average_color/output/22.jpg new file mode 100644 index 000000000..b6f3a3999 Binary files /dev/null and b/pil_pillow__examples/fast_average_color/output/22.jpg differ diff --git a/pil_pillow__examples/fill_0_and_1/main.py b/pil_pillow__examples/fill_0_and_1/main.py new file mode 100644 index 000000000..c72f716dc --- /dev/null +++ b/pil_pillow__examples/fill_0_and_1/main.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import itertools + +# pip install Pillow +from PIL import Image, ImageDraw, ImageFont + + +img = Image.new("RGB", (500, 300), (255, 0, 0)) +width, height = img.size + +drawer = ImageDraw.Draw(img) +font = ImageFont.truetype("arial.ttf", 25) + +text_0, text_1 = "0", "1" +color_0, color_1 = (0, 0, 0), (255, 255, 255) +it = itertools.cycle([ + (text_0, color_0), + (text_1, color_1), +]) + +width_text_0, height_text_0 = font.getsize(text_0) +width_text_1, height_text_1 = font.getsize(text_1) + +max_width_text = max(width_text_0, width_text_1) +max_height_text = max(height_text_0, height_text_1) + +for i in range(0, width // max_width_text): + for j in range(0, height // max_height_text): + x, y = i * max_width_text, j * max_height_text + text, color = next(it) + + drawer.text((x, y), text, font=font, fill=color) + + # NOTE: aligned column + # next(it) + +img.save("output.png") +img.show() diff --git a/pil_pillow__examples/fill_0_and_1/output.png b/pil_pillow__examples/fill_0_and_1/output.png new file mode 100644 index 000000000..f243b9293 Binary files /dev/null and b/pil_pillow__examples/fill_0_and_1/output.png differ diff --git a/pil_pillow__examples/flip/flip.py b/pil_pillow__examples/flip/flip.py index 9c38ef45d..7aba1a3a5 100644 --- a/pil_pillow__examples/flip/flip.py +++ b/pil_pillow__examples/flip/flip.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageOps -img = Image.open('input.png') + + +img = Image.open("input.png") img = ImageOps.flip(img) img.show() diff --git a/pil_pillow__examples/for_cycle__pixels__array/main.py b/pil_pillow__examples/for_cycle__pixels__array/main.py index ed7f5d2a9..70a89eb4b 100644 --- a/pil_pillow__examples/for_cycle__pixels__array/main.py +++ b/pil_pillow__examples/for_cycle__pixels__array/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -21,7 +21,7 @@ def get_pixel_array(img, rgb_hex=False): r, g, b = img.getpixel((x, y)) if rgb_hex: - value = '{:02X}{:02X}{:02X}'.format(r, g, b) + value = f"{r:02X}{g:02X}{b:02X}" row.append(value) else: row.append((r, g, b)) @@ -29,16 +29,16 @@ def get_pixel_array(img, rgb_hex=False): return pixels -if __name__ == '__main__': +if __name__ == "__main__": img = Image.open("input.jpg") print(img) print() pixels = get_pixel_array(img) - print('Rows: {}, cols: {}'.format(len(pixels), len(pixels[0]))) + print(f"Rows: {len(pixels)}, cols: {len(pixels[0])}") print([pixels[0][i] for i in range(2)]) # [(7, 7, 7), (23, 23, 23)] print() pixels = get_pixel_array(img, rgb_hex=True) - print('Rows: {}, cols: {}'.format(len(pixels), len(pixels[0]))) + print(f"Rows: {len(pixels)}, cols: {len(pixels[0])}") print([pixels[0][i] for i in range(2)]) # ['070707', '171717'] diff --git a/pil_pillow__examples/get_image_size/main.py b/pil_pillow__examples/get_image_size/main.py index 96e07cda7..9851cc596 100644 --- a/pil_pillow__examples/get_image_size/main.py +++ b/pil_pillow__examples/get_image_size/main.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + img = Image.open("input.jpg") print(img) print(img.size) diff --git a/pil_pillow__examples/get_viewers.py b/pil_pillow__examples/get_viewers.py new file mode 100644 index 000000000..7646fd6b8 --- /dev/null +++ b/pil_pillow__examples/get_viewers.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PIL.ImageShow import _viewers + + +print(f"Viewers: {len(_viewers)}") +for viewer in _viewers: + print(viewer.get_command("123.png")) + +""" +Viewers: 1 +start "Pillow" /WAIT "123.png" && ping -n 2 127.0.0.1 >NUL && del /f "123.png" +""" diff --git a/pil_pillow__examples/gray/main.py b/pil_pillow__examples/gray/main.py index a954414bb..c29a90b06 100644 --- a/pil_pillow__examples/gray/main.py +++ b/pil_pillow__examples/gray/main.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageOps + image_file = "input.jpg" image = Image.open(image_file) image_gray = ImageOps.grayscale(image) -image_gray.save('image_gray.png') +image_gray.save("image_gray.png") image_gray.show() diff --git a/pil_pillow__examples/image_to_bytes/main.py b/pil_pillow__examples/image_to_bytes/main.py index c3504430f..b1f70bfcf 100644 --- a/pil_pillow__examples/image_to_bytes/main.py +++ b/pil_pillow__examples/image_to_bytes/main.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import io + # pip install Pillow from PIL import Image -import io img = Image.open("input.jpg") diff --git a/pil_pillow__examples/img_to_grid/main.py b/pil_pillow__examples/img_to_grid/main.py index c5e2fcf09..9a0d01864 100644 --- a/pil_pillow__examples/img_to_grid/main.py +++ b/pil_pillow__examples/img_to_grid/main.py @@ -1,18 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + + image = Image.open("input.jpg") width, height = image.size ROW_COUNT = 3 COLUMN_COUNT = 5 -grid_image = Image.new("RGB", (width * COLUMN_COUNT, height * ROW_COUNT), (255, 255, 255)) +grid_image = Image.new( + "RGB", + (width * COLUMN_COUNT, height * ROW_COUNT), + (255, 255, 255), +) for row in range(ROW_COUNT): y = row * width @@ -22,4 +28,4 @@ grid_image.paste(image, (x, y)) grid_image.show() -grid_image.save('output.jpg') +grid_image.save("output.jpg") diff --git a/pil_pillow__examples/invert/main.py b/pil_pillow__examples/invert/main.py index fc9481503..c3c409f00 100644 --- a/pil_pillow__examples/invert/main.py +++ b/pil_pillow__examples/invert/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Инвертирование цвета картинки""" @@ -12,21 +12,21 @@ def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) image_invert = invert(image) - image_invert.save('image_invert.png') + image_invert.save("image_invert.png") image_invert.show() diff --git a/pil_pillow__examples/invert_gray/main.py b/pil_pillow__examples/invert_gray/main.py index 3ee1191b2..6ddb9c4f1 100644 --- a/pil_pillow__examples/invert_gray/main.py +++ b/pil_pillow__examples/invert_gray/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -9,22 +9,22 @@ def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) image_invert = invert(image) image_invert_gray = ImageOps.grayscale(image_invert) - image_invert_gray.save('image_invert_gray.png') + image_invert_gray.save("image_invert_gray.png") image_invert_gray.show() diff --git "a/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" "b/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" index b9d9ff20b..6abde7363 100644 --- "a/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" +++ "b/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image -img = Image.open('input.jpg') -img.save('output.jpg', format='JPEG', quality=1) + + +img = Image.open("input.jpg") +img.save("output.jpg", format="JPEG", quality=1) diff --git a/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py b/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py index a52ba908c..1c1e97c64 100644 --- a/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py +++ b/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + from PyQt5 import Qt + image_file = "input.jpg" image = Image.open(image_file) diff --git a/pil_pillow__examples/pixelate_image/main.py b/pil_pillow__examples/pixelate_image/main.py index 14d7bdf2d..ea9fa95e6 100644 --- a/pil_pillow__examples/pixelate_image/main.py +++ b/pil_pillow__examples/pixelate_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://gist.github.com/danyshaanan/6754465 @@ -17,8 +17,12 @@ def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -26,25 +30,25 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image -if __name__ == '__main__': - image = Image.open('image/input.jpg').convert('RGB') +if __name__ == "__main__": + image = Image.open("image/input.jpg").convert("RGB") # image.show() image_pixelate = pixelate(image, draw_margin=False) - image_pixelate.save('image/output_no_margin.jpg') + image_pixelate.save("image/output_no_margin.jpg") # image_pixelate.show() image_pixelate = pixelate(image) - image_pixelate.save('image/output.jpg') + image_pixelate.save("image/output.jpg") # image_pixelate.show() for size in (16, 32, 48): image_pixelate = pixelate(image, pixel_size=size) - image_pixelate.save('image/output_{}.jpg'.format(size)) + image_pixelate.save(f"image/output_{size}.jpg") # image_pixelate.show() diff --git a/pil_pillow__examples/png2jpg/main.py b/pil_pillow__examples/png2jpg/main.py index 9cfc0673a..d0acd602d 100644 --- a/pil_pillow__examples/png2jpg/main.py +++ b/pil_pillow__examples/png2jpg/main.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from PIL import Image file_name = "input.png" -new_file_name = file_name[:-3] + 'jpg' +new_file_name = file_name[:-3] + "jpg" -from PIL import Image img = Image.open(file_name) - img.save(new_file_name, optimize=True, quality=100) diff --git a/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py b/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py index a3a1fe53e..48e8cd089 100644 --- a/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py +++ b/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -10,6 +10,7 @@ # pip install pyqt5 from PyQt5.Qt import QImage, QPixmap, QApplication + image_file = "input.jpg" app = QApplication([]) diff --git a/pil_pillow__examples/replace_rgb_by_alpha/input.png b/pil_pillow__examples/replace_rgb_by_alpha/input.png new file mode 100644 index 000000000..be7aeaccf Binary files /dev/null and b/pil_pillow__examples/replace_rgb_by_alpha/input.png differ diff --git a/pil_pillow__examples/replace_rgb_by_alpha/main.py b/pil_pillow__examples/replace_rgb_by_alpha/main.py new file mode 100644 index 000000000..a9ff418d5 --- /dev/null +++ b/pil_pillow__examples/replace_rgb_by_alpha/main.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install Pillow +from PIL import Image + + +image_file = "input.png" +img = Image.open(image_file).convert("RGBA") + +pixdata = img.load() + +for y in range(img.size[1]): + for x in range(img.size[0]): + alpha = pixdata[x, y][3] + if alpha: + pixdata[x, y] = (255, 255, 255, alpha) + +img.save("output.png") diff --git a/pil_pillow__examples/replace_rgb_by_alpha/output.png b/pil_pillow__examples/replace_rgb_by_alpha/output.png new file mode 100644 index 000000000..ccdb7c42e Binary files /dev/null and b/pil_pillow__examples/replace_rgb_by_alpha/output.png differ diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg new file mode 100644 index 000000000..2b7999a0f Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg differ diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py new file mode 100644 index 000000000..52e049336 --- /dev/null +++ b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install Pillow +from PIL import Image +from PIL.Image import Resampling + + +def _resize_img( + img: Image, + width: int, + height: int, + resample: Resampling | None = None, +) -> Image: + img_resized = img.resize(size=(width, height), resample=resample) + img_resized.format = img.format # NOTE: resize reset format + return img_resized + + +def resize_height_img( + img: Image, + height: int, + resample: Resampling | None = None, +) -> Image: + """Resize by height, keep ratio.""" + return _resize_img(img, img.width * height // img.height, height, resample) + + +def resize_width_img( + img: Image, + width: int, + resample: Resampling | None = None, +) -> Image: + """Resize by width, keep ratio.""" + return _resize_img(img, width, img.height * width // img.width, resample) + + +if __name__ == "__main__": + file_name = "input.jpg" + img = Image.open(file_name) + print(img.size) + # (660, 438) + + resample = Resampling.LANCZOS + + img_height_300 = resize_height_img(img, height=300, resample=resample) + img_height_300.save("output_height=300.jpg") + print(img_height_300.size) + # (452, 300) + + img_width_300 = resize_width_img(img, width=300, resample=resample) + img_width_300.save("output_width=300.jpg") + print(img_width_300.size) + # (300, 199) diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg new file mode 100644 index 000000000..fb4a062a2 Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg differ diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg new file mode 100644 index 000000000..3381d2a90 Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg differ diff --git a/pil_pillow__examples/rounded_rectangle.py b/pil_pillow__examples/rounded_rectangle.py index ad836f2c7..57784bfa4 100644 --- a/pil_pillow__examples/rounded_rectangle.py +++ b/pil_pillow__examples/rounded_rectangle.py @@ -1,38 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/szabgab/slides/blob/914d15bb55cb0e335afcb72b6af0fa2aa9ed356f/python/examples/pil/draw_rectangle_with_rounded_corners.py -# TODO: append alpha +from PIL import Image, ImageDraw -from PIL import Image, ImageDraw +Color = float | tuple[int, ...] | str -def round_corner(radius, fill): +def round_corner(radius: int, fill: Color) -> Image: """Draw a round corner""" - corner = Image.new('RGB', (radius, radius), (0, 0, 0, 0)) + corner = Image.new("RGBA", (radius, radius), (0, 0, 0, 0)) + draw = ImageDraw.Draw(corner) - draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill=fill) + draw.pieslice((0, 0, radius * 2, radius * 2), start=180, end=270, fill=fill) + return corner -def round_rectangle(size, radius, fill): +def round_rectangle(width: int, height: int, radius: int, fill: Color) -> Image: """Draw a rounded rectangle""" - width, height = size - rectangle = Image.new('RGBA', size, fill) + + rectangle = Image.new("RGBA", (width, height), fill) corner = round_corner(radius, fill) + rectangle.paste(corner, (0, 0)) - rectangle.paste(corner.rotate(90), (0, height - radius)) # Rotate the corner and paste it + + # Rotate the corner and paste it + rectangle.paste(corner.rotate(90), (0, height - radius)) + rectangle.paste(corner.rotate(180), (width - radius, height - radius)) rectangle.paste(corner.rotate(270), (width - radius, 0)) + return rectangle -img = round_rectangle((500, 500), 50, "yellow") -img.show() -img.save('img.png') +if __name__ == "__main__": + img = round_rectangle(width=500, height=500, radius=50, fill="yellow") + img.show() + img.save("img.png") diff --git a/pil_pillow__examples/set_transparent_pixels/main.py b/pil_pillow__examples/set_transparent_pixels/main.py new file mode 100644 index 000000000..491a8f6a2 --- /dev/null +++ b/pil_pillow__examples/set_transparent_pixels/main.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PIL import Image + + +file_name = "../for_cycle__pixels__array/input.jpg" + +img = Image.open(file_name).convert("RGBA") +pixels = img.load() +width, height = img.size + +for i in range(width): + for j in range(height): + if (j + i) % 2 == 0: + pixels[i, j] = 0, 0, 0, 0 + +img.save("output.png") diff --git a/pil_pillow__examples/simple_draw__hello_world/main.py b/pil_pillow__examples/simple_draw__hello_world/main.py index 26a9a4d30..ba888f3d7 100644 --- a/pil_pillow__examples/simple_draw__hello_world/main.py +++ b/pil_pillow__examples/simple_draw__hello_world/main.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageDraw, ImageFont + image = Image.open("images.jpg") font = ImageFont.truetype("arial.ttf", 25) drawer = ImageDraw.Draw(image) -drawer.text((50, 100), "Hello World!\nПривет мир!", font=font, fill='black') +drawer.text((50, 100), "Hello World!\nПривет мир!", font=font, fill="black") -image.save('new_img.jpg') +image.save("new_img.jpg") image.show() diff --git a/pil_pillow__examples/square_to_circle__avatar/main.py b/pil_pillow__examples/square_to_circle__avatar/main.py index 796e98f89..7ad249805 100644 --- a/pil_pillow__examples/square_to_circle__avatar/main.py +++ b/pil_pillow__examples/square_to_circle__avatar/main.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageDraw -img = Image.open('input.jpg') + +img = Image.open("input.jpg") bigsize = img.size[0] * 3, img.size[1] * 3 -mask = Image.new('L', bigsize, 0) +mask = Image.new("L", bigsize, 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0) + bigsize, fill=255) mask = mask.resize(img.size, Image.ANTIALIAS) img.putalpha(mask) -img.save('output.png') +img.save("output.png") diff --git a/pil_pillow__examples/text_to_pixels/main.py b/pil_pillow__examples/text_to_pixels/main.py index 6f3309b45..959090869 100644 --- a/pil_pillow__examples/text_to_pixels/main.py +++ b/pil_pillow__examples/text_to_pixels/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageDraw @@ -46,7 +46,7 @@ img_size = s break -assert img_size != -1, "Неудалось подобрать размер изображения" +assert img_size != -1, "Не удалось подобрать размер изображения" image = Image.new("RGBA", (img_size, img_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) @@ -61,4 +61,4 @@ color = colors[pos] draw.point((x, y), fill=(color, 255, 255)) -image.save('result.png') +image.save("result.png") diff --git a/pil_pillow__examples/thumbnails/main.py b/pil_pillow__examples/thumbnails/main.py index ba5910aeb..668ba6756 100644 --- a/pil_pillow__examples/thumbnails/main.py +++ b/pil_pillow__examples/thumbnails/main.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + image_file = "input.jpg" img = Image.open(image_file) size = 128, 128 img.thumbnail(size) -img.save('output_thumbnail.png') +img.save("output_thumbnail.png") img.show() diff --git a/pipette/main.py b/pipette/main.py index addd5d231..ad58972e9 100644 --- a/pipette/main.py +++ b/pipette/main.py @@ -1,23 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://habrahabr.ru/post/346146/ +import time + # pip install pyautogui import pyautogui + from graphics import GraphWin, Circle, Point, Entry, color_rgb -import time -win = GraphWin("pipetka", 200, 200, autoflush=True) # создаем графическую форму размером 200х200 и элементы на ней + +# Создаем графическую форму размером 200х200 и элементы на ней +win = GraphWin( + "pipetka", 200, 200, autoflush=True +) x, y = pyautogui.position() # получаем в x, y координаты мыши r, g, b = pyautogui.pixel(x, y) # получаем в r, g, b цвет ColorDot = Circle(Point(100, 100), 25) # создаем точку, отображающую цвет -ColorDot.setFill(color_rgb(r, g, b)) # устанавливает ей заливку из ранее полученных цветов +# Устанавливает ей заливку из ранее полученных цветов +ColorDot.setFill( + color_rgb(r, g, b) +) ColorDot.draw(win) # рисуем на форме win RGBtext = Entry(Point(win.getWidth() / 2, 25), 10) # создаем RGB вывод @@ -37,7 +46,7 @@ ColorDot.setFill(color_rgb(r, g, b)) # Обновляем цвет RGBtext.setText(pyautogui.pixel(x, y)) # Обновляем RGB RGBstring.setText(color_rgb(r, g, b)) # Обновляем web цвет - Coordstring.setText('{} x {}'.format(x, y)) # Обновляем координаты + Coordstring.setText(f"{x} x {y}") # Обновляем координаты if win.isClosed(): break diff --git a/pixelate_image_gui/main.py b/pixelate_image_gui/main.py index 718acb70f..cd121acd3 100644 --- a/pixelate_image_gui/main.py +++ b/pixelate_image_gui/main.py @@ -1,20 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os -from PyQt5 import Qt +import sys +import traceback + from PIL import Image +from PyQt5 import Qt + # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1c1ad83de7c0f0bd02d9926fe141da4ba5b92720/pil_example/pixelate_image/main.py def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -22,36 +30,34 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + Qt.QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -WINDOW_TITLE = 'Pixelate' +WINDOW_TITLE = "Pixelate" class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle(WINDOW_TITLE) - self.last_load_path = '.' + self.last_load_path = "." self.file_name = None self.image_source = None self.image_result = None @@ -59,20 +65,20 @@ def __init__(self): self.lb_image_view = Qt.QLabel() self.lb_image_view.setAlignment(Qt.Qt.AlignCenter) - self.pb_load_image = Qt.QPushButton('Load') + self.pb_load_image = Qt.QPushButton("Load") self.pb_load_image.clicked.connect(self.load) - self.pb_save_as = Qt.QPushButton('Save as...') + self.pb_save_as = Qt.QPushButton("Save as...") self.pb_save_as.clicked.connect(self.save_as) - self.rb_original = Qt.QRadioButton('Original') + self.rb_original = Qt.QRadioButton("Original") self.rb_original.clicked.connect(self._do_pixelate) - self.rb_result = Qt.QRadioButton('Result') + self.rb_result = Qt.QRadioButton("Result") self.rb_result.setChecked(True) self.rb_result.clicked.connect(self._do_pixelate) - self.cb_draw_margin = Qt.QCheckBox('Draw margin') + self.cb_draw_margin = Qt.QCheckBox("Draw margin") self.cb_draw_margin.setChecked(True) self.cb_draw_margin.clicked.connect(self._do_pixelate) @@ -81,7 +87,9 @@ def __init__(self): self.sl_pixel_size.setValue(9) self.sb_pixel_size = Qt.QSpinBox() - self.sb_pixel_size.setRange(self.sl_pixel_size.minimum(), self.sl_pixel_size.maximum()) + self.sb_pixel_size.setRange( + self.sl_pixel_size.minimum(), self.sl_pixel_size.maximum() + ) self.sb_pixel_size.setValue(self.sl_pixel_size.value()) self.sl_pixel_size.sliderMoved.connect(self.sb_pixel_size.setValue) @@ -101,7 +109,7 @@ def __init__(self): layout_button.addWidget(self.cb_draw_margin) layout_button_2 = Qt.QHBoxLayout() - layout_button_2.addWidget(Qt.QLabel('Pixel size:')) + layout_button_2.addWidget(Qt.QLabel("Pixel size:")) layout_button_2.addWidget(self.sl_pixel_size) layout_button_2.addWidget(self.sb_pixel_size) @@ -117,30 +125,34 @@ def __init__(self): self._do_pixelate() - def load(self): + def load(self) -> None: image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" - self.file_name = Qt.QFileDialog.getOpenFileName(self, "Load image", self.last_load_path, image_filters)[0] + self.file_name = Qt.QFileDialog.getOpenFileName( + self, "Load image", self.last_load_path, image_filters + )[0] if not self.file_name: return self.last_load_path = Qt.QFileInfo(self.file_name).absolutePath() - self.image_source = Image.open(self.file_name).convert('RGB') + self.image_source = Image.open(self.file_name).convert("RGB") self._do_pixelate() - def save_as(self): + def save_as(self) -> None: image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" # C:\Images\img.png -> C:\Images\img_pixelate.png - file_name = '_pixelate'.join(os.path.splitext(self.file_name)) + file_name = "_pixelate".join(os.path.splitext(self.file_name)) - file_name = Qt.QFileDialog.getSaveFileName(self, "Save as image", file_name, image_filters)[0] + file_name = Qt.QFileDialog.getSaveFileName( + self, "Save as image", file_name, image_filters + )[0] if not file_name: return self.image_result.save(file_name) - def _do_pixelate(self): + def _do_pixelate(self) -> None: # Виджеты будут доступны только если стоит флаг на Result и картинка загружена ok = self.rb_result.isChecked() and self.image_source is not None self.pb_save_as.setEnabled(ok) @@ -157,7 +169,7 @@ def _do_pixelate(self): self.show_result() - def show_result(self): + def show_result(self) -> None: image_result = self.image_result if self.rb_result.isChecked() else self.image_source if not image_result: return @@ -165,20 +177,22 @@ def show_result(self): image_result = image_result.toqpixmap() size = self.lb_image_view.size() - pixmap = image_result.scaled(size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation) + pixmap = image_result.scaled( + size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation + ) self.lb_image_view.setPixmap(pixmap) height, width = self.image_source.size - title = WINDOW_TITLE + '. {}x{} ({}x{}). {}'.format(width, height, size.width(), size.height(), self.file_name) + title = f"{WINDOW_TITLE}. {width}x{height} ({size.width()}x{size.height()}). {self.file_name}" self.setWindowTitle(title) - def resizeEvent(self, e): + def resizeEvent(self, e) -> None: super().resizeEvent(e) self.show_result() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/play mp3/pyglet_example.py b/play mp3/pyglet_example.py index 48c44a09e..7ee7eeb93 100644 --- a/play mp3/pyglet_example.py +++ b/play mp3/pyglet_example.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import pyglet # # SIMPLE EXAMPLE @@ -11,10 +15,8 @@ # pyglet.app.run() -def play(file_name): - import pyglet - import os - dll_file_name = os.path.join(os.path.dirname(__file__), 'avbin') +def play(file_name) -> None: + dll_file_name = os.path.join(os.path.dirname(__file__), "avbin") pyglet.lib.load_library(dll_file_name) player = pyglet.media.Player() @@ -23,7 +25,7 @@ def play(file_name): player.play() - def update(dt): + def update(dt) -> None: if not player.playing: # Отпишем функцию, иначе при повторном вызове, иначе # будет двойной вызов при следующем воспроизведении @@ -35,5 +37,5 @@ def update(dt): pyglet.app.run() -if __name__ == '__main__': - play(r'speak.mp3') +if __name__ == "__main__": + play(r"speak.mp3") diff --git a/play_audio/play.py b/play_audio/play.py index 4bed644be..7fc5567e8 100644 --- a/play_audio/play.py +++ b/play_audio/play.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import sys +import os +import sys +import play_audio + + +if __name__ == "__main__": if len(sys.argv) > 1: - import play_audio play_audio.play(sys.argv[1]) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] audio_file_name'.format(file_name)) + print(f"usage: {file_name} [-h] audio_file_name") diff --git a/play_audio/play_audio.py b/play_audio/play_audio.py index 3211fcf9f..a6df2a2f7 100644 --- a/play_audio/play_audio.py +++ b/play_audio/play_audio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -21,7 +21,7 @@ Phonon.createPath(audio, output) -def play(file_name): +def play(file_name) -> None: audio.setCurrentSource(Phonon.MediaSource(file_name)) audio.play() diff --git a/play_mp3_with_mp3play_module/play.py b/play_mp3_with_mp3play_module/play.py index ce2835749..10ac62ca3 100644 --- a/play_mp3_with_mp3play_module/play.py +++ b/play_mp3_with_mp3play_module/play.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time + # https://pypi.python.org/pypi/mp3play/ +import mp3play -def play(filename): - import mp3play +def play(filename) -> None: clip = mp3play.load(filename) clip.play() - import time time.sleep(min(30, clip.seconds())) clip.stop() -if __name__ == '__main__': +if __name__ == "__main__": # TODO: не работает: print 'Error %s for "%s": %s' % (str(err), txt, buf) # ^SyntaxError: invalid syntax - play(r'speak.mp3') + play(r"speak.mp3") diff --git a/play_sound__pyqt/main.py b/play_sound__pyqt/main.py index 8c3899bb5..8229d0c1d 100644 --- a/play_sound__pyqt/main.py +++ b/play_sound__pyqt/main.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + +from multiprocessing import Process + from PyQt5.QtCore import QCoreApplication, QUrl, QTimer from PyQt5.QtMultimedia import QMediaPlaylist, QMediaContent, QMediaPlayer -import os def play(file_name: str): @@ -24,7 +27,7 @@ def play(file_name: str): player.setPlaylist(playlist) player.play() - def tick(): + def tick() -> None: if player.state() == QMediaPlayer.StoppedState: app.quit() @@ -35,24 +38,25 @@ def tick(): app.exec() -def play_async(file_name: str): +def play_async(file_name: str) -> None: # from threading import Thread # thread = Thread(target=play, args=(file_name,)) # thread.start() - from multiprocessing import Process process = Process(target=play, args=(file_name,)) process.start() -if __name__ == '__main__': - file_name = r'..\speak\play_mp3\speak_male.mp3' - +if __name__ == "__main__": + file_name = r"..\speak\play_mp3\speak_male.mp3" play(file_name) import time + play_async(file_name) time.sleep(0.2) + play_async(file_name) time.sleep(0.2) + play_async(file_name) diff --git a/play_sound__pyqt/play.py b/play_sound__pyqt/play.py index dddc6af95..75e100d02 100644 --- a/play_sound__pyqt/play.py +++ b/play_sound__pyqt/play.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import sys +import os +import sys +from main import play + + +if __name__ == "__main__": if len(sys.argv) > 1: - from main import play play(sys.argv[1]) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] audio_file_name'.format(file_name)) + print(f"usage: {file_name} [-h] audio_file_name") diff --git a/play_url_mp3/play_url_mp3.py b/play_url_mp3/play_url_mp3.py index cd49e75fe..8a27d6685 100644 --- a/play_url_mp3/play_url_mp3.py +++ b/play_url_mp3/play_url_mp3.py @@ -1,35 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.request import urlretrieve -from urllib.parse import urlparse - import os import sys +from urllib.request import urlretrieve +from urllib.parse import urlparse + from PySide.QtGui import * from PySide.phonon import Phonon -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) audio = Phonon.MediaObject() - audio.setCurrentSource(Phonon.MediaSource('')) + audio.setCurrentSource(Phonon.MediaSource("")) output = Phonon.AudioOutput(Phonon.MusicCategory) Phonon.createPath(audio, output) widget = QWidget() - widget.setWindowTitle('Simple url audio player') + widget.setWindowTitle("Simple url audio player") line_edit_url = QLineEdit() - line_edit_url.setText('https://www.dropbox.com/sh/a4bwzhn0s584u73/AACgDRii1rtCmoL3mXEwlXDga/CallYourName.mp3?dl=1') - + line_edit_url.setText( + "https://www.dropbox.com/sh/a4bwzhn0s584u73/AACgDRii1rtCmoL3mXEwlXDga/CallYourName.mp3?dl=1" + ) - def play(): + def play() -> None: url = line_edit_url.text() # Выдираем из url имя файла @@ -40,17 +41,15 @@ def play(): audio.setCurrentSource(Phonon.MediaSource(file_name)) audio.play() - - play_url = QPushButton('Play') + play_url = QPushButton("Play") play_url.clicked.connect(play) layout = QHBoxLayout() - layout.addWidget(QLabel('Url audio:')) + layout.addWidget(QLabel("Url audio:")) layout.addWidget(line_edit_url) layout.addWidget(play_url) widget.setLayout(layout) widget.show() - sys.exit(app.exec_()) diff --git a/play_url_mp3/setup.py b/play_url_mp3/setup.py index ef4a9cdf4..4b92e25f0 100644 --- a/play_url_mp3/setup.py +++ b/play_url_mp3/setup.py @@ -9,12 +9,11 @@ from cx_Freeze import setup, Executable -executables = [ - Executable('play_url_mp3.py') -] +executables = [Executable("play_url_mp3.py")] -setup(name='play_url_mp3', - version='0.1', - description='play_url_mp3.py', - executables=executables - ) +setup( + name="play_url_mp3", + version="0.1", + description="play_url_mp3.py", + executables=executables, +) diff --git a/playwright__examples/README.md b/playwright__examples/README.md new file mode 100644 index 000000000..b22502b53 --- /dev/null +++ b/playwright__examples/README.md @@ -0,0 +1,15 @@ +Install: +``` +pip install playwright==1.52.0 +``` + +Download browsers: +``` +playwright install +``` + +Download browsers (proxy): +``` +set HTTPS_PROXY=https://: +playwright install +``` diff --git a/playwright__examples/__init__.py b/playwright__examples/__init__.py new file mode 100644 index 000000000..f6a8eac64 --- /dev/null +++ b/playwright__examples/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + diff --git a/playwright__examples/get_cookies.py b/playwright__examples/get_cookies.py new file mode 100644 index 000000000..9af43b8ba --- /dev/null +++ b/playwright__examples/get_cookies.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = browser_type.launch() + page = browser.new_page() + + print(browser_type.name.upper()) + + print("Cookies:", page.context.cookies()) + + for k, v in [ + ("name", 123), + ("foo", "bar"), + ("browser", browser_type.name), + ]: + page.goto(f"https://httpbin.org/cookies/set/{k}/{v}") + time.sleep(1) + + rs = page.goto("https://httpbin.org/cookies") + print("From API:", rs.json()) + + cookies = page.context.cookies() + print(f"Cookies ({len(cookies)}):") + for cookie in cookies: + print(f" {cookie}") + + print() + + browser.close() +""" +CHROMIUM +Cookies: [] +From API: {'cookies': {'browser': 'chromium', 'foo': 'bar', 'name': '123'}} +Cookies (3): + {'name': 'name', 'value': '123', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + {'name': 'foo', 'value': 'bar', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + {'name': 'browser', 'value': 'chromium', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + +FIREFOX +Cookies: [] +From API: {'cookies': {'browser': 'firefox', 'foo': 'bar', 'name': '123'}} +Cookies (3): + {'name': 'name', 'value': '123', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + {'name': 'foo', 'value': 'bar', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + {'name': 'browser', 'value': 'firefox', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + +WEBKIT +Cookies: [] +From API: {'cookies': {}} +Cookies (0): + +""" diff --git a/playwright__examples/get_page_text.py b/playwright__examples/get_page_text.py new file mode 100644 index 000000000..a7e103d4a --- /dev/null +++ b/playwright__examples/get_page_text.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + browser = p.webkit.launch() + page = browser.new_page() + + rs = page.goto("https://httpbin.org/") + print(rs.text()) + + browser.close() diff --git a/playwright__examples/get_screenshot.py b/playwright__examples/get_screenshot.py new file mode 100644 index 000000000..edd891a28 --- /dev/null +++ b/playwright__examples/get_screenshot.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from playwright.sync_api import sync_playwright + +DIR: Path = Path(__file__).resolve().parent +DIR_SCREENSHOTS: Path = DIR / "screenshots" +DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name) + + browser = browser_type.launch() + page = browser.new_page() + + page.goto("https://github.com/gil9red") + + img_date: bytes = page.screenshot() + print(f"img_date: {img_date[:50]}...") + + path_page: Path = DIR_SCREENSHOTS / f"{browser_type.name}_page.png" + page.screenshot(path=path_page) + print(f"path_page: {path_page}") + + path_full_page: Path = DIR_SCREENSHOTS / f"{browser_type.name}_full_page.png" + page.screenshot(path=path_full_page, full_page=True) + print(f"path_full_page: {path_full_page}") + + print() diff --git a/playwright__examples/get_user_agents.py b/playwright__examples/get_user_agents.py new file mode 100644 index 000000000..2b657fb5b --- /dev/null +++ b/playwright__examples/get_user_agents.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = browser_type.launch() + page = browser.new_page() + + print(browser_type.name.upper()) + print("Local:", page.evaluate("navigator.userAgent")) + + context = browser.new_context(user_agent="Abc 123") + print("Set local:", context.new_page().evaluate("navigator.userAgent")) + + rs = page.goto("https://httpbin.org/user-agent") + print("Remote:", rs.json()["user-agent"]) + + print() + + browser.close() +""" +CHROMIUM +Local: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.6778.33 Safari/537.36 +Set local: Abc 123 +Remote: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.6778.33 Safari/537.36 + +FIREFOX +Local: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0 +Set local: Abc 123 +Remote: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0 + +WEBKIT +Local: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15 +Set local: Abc 123 +Remote: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15 +""" diff --git a/playwright__examples/human_automation/__init__.py b/playwright__examples/human_automation/__init__.py new file mode 100644 index 000000000..3c994a1db --- /dev/null +++ b/playwright__examples/human_automation/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/playwright__examples/human_automation/examples/__init__.py b/playwright__examples/human_automation/examples/__init__.py new file mode 100644 index 000000000..3c994a1db --- /dev/null +++ b/playwright__examples/human_automation/examples/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/playwright__examples/human_automation/examples/github.com_gil9red.py b/playwright__examples/human_automation/examples/github.com_gil9red.py new file mode 100644 index 000000000..dfb200a0f --- /dev/null +++ b/playwright__examples/human_automation/examples/github.com_gil9red.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# playwright>=1.61.0 +from playwright.sync_api import sync_playwright + +from human_automation.human_automation import HumanAutomation + +URL: str = "https://github.com/gil9red" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + part_url: str = "tab=repositories" + + def click_on_repositories() -> None: + link_locator = page.locator(f'nav a[href *= "{part_url}"]').first + human_automation.click(link_locator) + + human_automation.ensure_change_url( + action=click_on_repositories, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + part_url: str = "/SimplePyScripts" + + def find_repository() -> None: + find_selector = 'input[placeholder="Find a repository…"]' + find_locator = page.locator(find_selector) + human_automation.type_text(find_locator, "SimplePyScripts") + + link_selector = ( + f'#user-repositories-list a[itemprop *= "name"][href *= "{part_url}"]' + ) + locator = page.locator(link_selector).first + + human_automation.click(locator) + + human_automation.ensure_change_url( + action=find_repository, + is_ok_url=lambda url: part_url in url, + ) + + human_automation.wait(300, 600) + print("\n") + + human_automation.move_mouse_to_center() + + # Step 1: Go /playwright__examples + part_url: str = "/playwright__examples" + + def click_on_step_1() -> None: + dir_locator = page.get_by_role("link", name=part_url[1:]).first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_1, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 2: Go /playwright__examples/screenshots + part_url: str = f"{part_url}/screenshots" + + def click_on_step_2() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_2, + is_ok_url=lambda url: url.endswith(part_url), + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 3: Go /playwright__examples/screenshots/stealth + part_url: str = f"{part_url}/stealth" + + def click_on_step_3() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_3, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 4: Go /playwright__examples/screenshots/stealth/stealth_firefox_page.png + part_url: str = f"{part_url}/stealth_firefox_page.png" + + def click_on_step_4() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_4, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Download /playwright__examples/screenshots/stealth/stealth_firefox_page.png + img_locator = page.locator(f'img[src *= "{part_url}"]').first + print(img_locator) + + img_url: str = img_locator.get_attribute("src") + full_url: str = page.evaluate(f"new URL('{img_url}', window.location.href).href") + print(full_url) + + response = page.request.get(full_url) + img_bytes: bytes = response.body() + print(f"img_bytes ({len(img_bytes)} bytes): {img_bytes[:50]}...") + + page.wait_for_timeout(2_000) diff --git a/playwright__examples/human_automation/examples/store_steampowered_com.py b/playwright__examples/human_automation/examples/store_steampowered_com.py new file mode 100644 index 000000000..f5c093484 --- /dev/null +++ b/playwright__examples/human_automation/examples/store_steampowered_com.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# playwright>=1.61.0 +from playwright.sync_api import sync_playwright + +from human_automation.human_automation import HumanAutomation + +URL: str = "https://store.steampowered.com/" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + categories_locator = page.get_by_label("Меню магазина").locator( + "button > div:has-text('Категории')" + ) + print(categories_locator) + human_automation.click(categories_locator) + + # View all tags + all_tags_locator = page.locator("a:has-text('Просмотреть все метки')") + print(all_tags_locator) + human_automation.click(all_tags_locator) + + # Free To Play + free_to_play_locator = page.get_by_role("button", name="Бесплатная игра") + print(categories_locator) + human_automation.click(free_to_play_locator) + + go_to_free_to_play_locator = page.get_by_text("Просмотреть все") + print(go_to_free_to_play_locator) + human_automation.click(go_to_free_to_play_locator) + + filters_locator = page.locator("//div[text()='Фильтры']/..").first + print(filters_locator) + human_automation.move_to(filters_locator) + + genres_frame_locator = filters_locator.locator("//div[text()='Жанры']/../..").first + print(genres_frame_locator) + human_automation.move_to(genres_frame_locator) + + genres_locator = genres_frame_locator.locator("//div[text()='Жанры']") + print(genres_locator) + human_automation.click(genres_locator) + + genres_all_locator = genres_frame_locator.locator("//div[text()='Показать больше']") + print(genres_all_locator) + human_automation.click(genres_all_locator) + + genre_shooter_locator = genres_frame_locator.locator("//div/a[text()='Шутер']") + print(genre_shooter_locator) + human_automation.click(genre_shooter_locator) + + sub_genres_frame_locator = filters_locator.locator( + "//div[text()='Поджанры']/../.." + ).first + print(sub_genres_frame_locator) + human_automation.move_to(sub_genres_frame_locator) + + genres_locator = sub_genres_frame_locator.locator("//div[text()='Поджанры']") + print(genres_locator) + human_automation.click(genres_locator) + + sub_genres_all_locator = sub_genres_frame_locator.locator( + "//div[text()='Показать больше']" + ) + print(sub_genres_all_locator) + human_automation.click(sub_genres_all_locator) + + fps_locator = sub_genres_frame_locator.locator( + "//div/a[text()='Шутер от первого лица']" + ) + print(fps_locator) + human_automation.click(fps_locator) + + human_automation.click(page.get_by_role("button", name="Показать больше")) + + human_automation.move_to( + page.locator("a[href *= '/app/'][role='button']:has(img[alt]):visible").first + ) + human_automation.wait(2_000, 4_000) + + for locator in page.locator( + "a[href *= '/app/'][role='button']:has(img[alt]):visible" + ).all(): + print(locator) + + name: str = locator.locator("img[alt]").get_attribute("alt") + URL: str = locator.get_attribute("href") + print(f"{name!r}: {URL}") + + human_automation.move_to(locator) + + print() + + page.wait_for_timeout(5_000) diff --git a/playwright__examples/human_automation/examples/translate_google_ru.py b/playwright__examples/human_automation/examples/translate_google_ru.py new file mode 100644 index 000000000..f3ad0c2bb --- /dev/null +++ b/playwright__examples/human_automation/examples/translate_google_ru.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from textwrap import dedent + +# playwright>=1.61.0 +from playwright.sync_api import Page, sync_playwright + +from human_automation.human_automation import HumanAutomation + + +def get_text(page: Page, lang: str) -> str: + return "\n".join( + span.text_content().strip() + for span in page.locator(f'span[lang="{lang}"] > span > span').all() + ) + + +URL: str = "https://translate.google.ru/?sl=ru&tl=en&op=translate" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + input_locator = page.locator('textarea[aria-label="Исходный текст"]') + + human_automation.type_text( + input_locator, + text=dedent(""" + Съешь же ещё этих мягких + французских булок, + да выпей же чаю + """).strip(), + ) + + page.wait_for_timeout(2_000) + + text: str = get_text(page, lang="en") + print(f"Text ({len(text)}):\n{text}") + + page.wait_for_timeout(2_000) + + human_automation.click('button[aria-label *= "Обратный перевод"]:visible') + + page.wait_for_timeout(2_000) + + human_automation.type_text( + input_locator, + text=dedent(""" + The quick brown fox jumps + over the lazy dog + """).strip(), + ) + + page.wait_for_timeout(2_000) + + text: str = get_text(page, lang="ru") + print(f"Text ({len(text)}):\n{text}") + + page.wait_for_timeout(5_000) diff --git a/playwright__examples/human_automation/human_automation.py b/playwright__examples/human_automation/human_automation.py new file mode 100644 index 000000000..65da28946 --- /dev/null +++ b/playwright__examples/human_automation/human_automation.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import random +import re +import string + +from typing import Callable, TypedDict + +# playwright>=1.61.0 +from playwright.sync_api import Page, Locator, TimeoutError + +# playwright-stealth>=2.0.3 +from playwright_stealth import Stealth + +# python-ghost-cursor==0.1.1 +from python_ghost_cursor.playwright_sync import create_cursor +from python_ghost_cursor.playwright_sync._spoof import GhostCursor + +type Point = TypedDict("Point", {"x": int, "y": int}) + + +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +stealth = Stealth() + + +def get_center_page(page: Page) -> Point: + # Get the size of the current browser window + viewport: dict[str, int] | None = page.viewport_size + + base_x: int + base_y: int + if viewport: + base_x = viewport["width"] // 2 + base_y = viewport["height"] // 2 + else: + base_x, base_y = 640, 360 + + offset_x: int = random.randint(-40, 40) + offset_y: int = random.randint(-40, 40) + + target_x: int = base_x + offset_x + target_y: int = base_y + offset_y + + return {"x": target_x, "y": target_y} + + +def inject_cursor(page: Page) -> None: + page.add_init_script(""" + (() => { + const initCursor = () => { + const box = document.createElement('div'); + box.id = 'playwright-fake-cursor'; + box.style.position = 'fixed'; + box.style.top = '0'; + box.style.left = '0'; + box.style.width = '14px'; + box.style.height = '14px'; + box.style.background = 'rgba(255, 0, 0, 0.7)'; + box.style.border = '2px solid white'; + box.style.borderRadius = '50%'; + box.style.pointerEvents = 'none'; + box.style.zIndex = '999999'; + box.style.transform = 'translate(-50%, -50%)'; + document.body.appendChild(box); + + document.addEventListener('mousemove', (e) => { + box.style.left = e.clientX + 'px'; + box.style.top = e.clientY + 'px'; + }); + }; + + // If the DOM has already been loaded (unlikely for init_script, but useful for safety) + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCursor); + } else { + initCursor(); + } + })(); + """) + + +TYPO_CHAR_SETS: dict[str, str] = { + "eng_lower": string.ascii_lowercase, + "eng_upper": string.ascii_uppercase, + "rus_lower": "абвгдеёжзийклмнопрстуфхцчшщъыьэюя", + "rus_upper": "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ", + "digits": string.digits, + "punctuation": string.punctuation, +} + +WAIT_MS: tuple[int, int] = 100, 250 + + +class HumanAutomation: + page: Page + cursor: GhostCursor + typo_char_sets: dict[str, str] + + def __init__( + self, + page: Page, + typo_char_sets: dict[str, str] | None = None, + ) -> None: + if typo_char_sets is None: + typo_char_sets = TYPO_CHAR_SETS + + self.page = page + self.cursor = create_cursor(page, start=get_center_page(page)) + self.typo_char_sets = typo_char_sets + + stealth.apply_stealth_sync(page) + inject_cursor(page) + + def wait(self, min_time_ms: int, max_time_ms: int) -> None: + time_ms: float = random.randint(min_time_ms, max_time_ms) + self.page.wait_for_timeout(time_ms) + + def move_to( + self, + locator: Locator | str, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + if isinstance(locator, str): + locator = self.page.locator(locator) + + log.info(f"Moving to: {locator}") + + self.scroll_to_element(locator) + + self.cursor.move(locator) + self.wait(*wait_ms) + + def click( + self, + locator: Locator | str, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + if isinstance(locator, str): + locator = self.page.locator(locator) + + log.info(f"Clicking to: {locator}") + + # NOTE: move_to is not suitable here - the cursor moves twice + # It internally calls cursor.move, which moves the cursor + # click internally calls cursor.click, which also calls cursor.move + self.scroll_to_element(locator) + + # NOTE: If this is a link, removing the 'target' attribute will prevent opening in a new tab + locator.evaluate("el => el.removeAttribute('target')") + + self.cursor.click(locator) + self.wait(*wait_ms) + + def scroll_to_element( + self, + locator: Locator | str, + margin: int = 100, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + """ + Simulates human scrolling. + If the element is fully visible on the screen, no scrolling occurs. + If the element is hidden (at the top or bottom), it will smoothly scroll closer to the center. + """ + + if isinstance(locator, str): + locator = self.page.locator(locator) + + log.info(f"Scrolling to: {locator}") + + while True: + box: dict[str, float] | None + try: + box = locator.bounding_box(timeout=100) + except TimeoutError: + box = None + + if not box: + # If the element is not in the DOM at all, scroll down randomly + self.page.mouse.wheel(0, random.randint(250, 450)) + self.wait(*wait_ms) + continue + + viewport_height = self.page.viewport_size["height"] + viewport_height: int = self.page.viewport_size["height"] + + # Find the boundaries of the element relative to the screen + element_top = box["y"] + element_height = box["height"] + element_bottom = element_top + element_height + + is_top_visible = element_top >= margin + is_bottom_within_viewport = element_bottom <= (viewport_height - margin) + is_too_large = element_height > (viewport_height - (margin * 2)) + is_bottom_visible = is_bottom_within_viewport or is_too_large + if is_top_visible and is_bottom_visible: # The element is perfectly visible + break + + # If the element is not on the screen, calculate the distance to center for scrolling + element_center_y = element_top + (element_height / 2) + screen_center_y = viewport_height / 2 + distance_to_center = element_center_y - screen_center_y + + # If we are already very close to the center, stop + if abs(distance_to_center) < random.randint(40, 80): + break + + scroll_step: int + if distance_to_center > 0: + # Scroll down (target below screen) + scroll_step = random.randint(150, 350) + else: + # Scroll up (target above screen) + scroll_step = random.randint(-350, -150) + + self.page.mouse.wheel(0, scroll_step) + self.wait(*wait_ms) + + print("return") + self.wait(*wait_ms) + + def _typo_effect( + self, + char: str, + typo_chance: float, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + # Logic for random typo (don't break spaces) + if char == " " or random.random() >= typo_chance: + return + + pool = "" + for s_set in self.typo_char_sets.values(): + if char in s_set: + pool = s_set + break + + # If the symbol is rare and is not in the pools, we create a default list (letters + numbers) + if not pool: + pool: str = self.typo_char_sets["eng_lower"] + self.typo_char_sets["digits"] + + valid_choices: list[str] = [c for c in pool if c != char] + if not valid_choices: + log.warning(f"No valid choices found for character {char!r}.") + return + + wrong_char: str = random.choice(valid_choices) + + self.page.keyboard.type(wrong_char) + self.wait(60, 140) + self.wait(180, 350) # Pause for recognizing the error + self.page.keyboard.press("Backspace") + self.wait(*wait_ms) + + def type_text( + self, + locator: Locator | str, + text: str, + typo_chance: float = 0.07, + max_attempts: int = 3, + strict_validation: bool = False, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> bool: + """ + Types text like a human: with unique pauses, random typos, + deletion of them through Backspace and final validation of the input value. + """ + + if isinstance(locator, str): + locator = self.page.locator(locator) + + log.info(f"Typing text ({len(text)}): {text!r}, locator: {locator}") + + for attempt in range(1, max_attempts + 1): + # Click on the field to bring focus + self.click(locator) + + # Realistically clear the field through keyboard emulation of Ctrl+A -> Backspace + self.page.keyboard.down("Control") + self.page.keyboard.press("a") + self.page.keyboard.up("Control") + self.page.keyboard.press("Backspace") + self.wait(*wait_ms) + + # Typing character by character + for char in text: + self._typo_effect(char, typo_chance=typo_chance, wait_ms=wait_ms) + + # Type correct character + self.page.keyboard.type(char) + + # Typing rhythm (delay between keys) + min_time_ms: int = 50 + max_time_ms: int = 160 + # 4% chance that the person thought about a word in the middle of it + if random.random() < 0.04: + min_time_ms += 350 + max_time_ms += 700 + self.wait(min_time_ms, max_time_ms) + + # Validate the actually typed text in the field + actual_text: str = locator.input_value() or "" + + is_valid: bool + if strict_validation: + is_valid = actual_text == text + else: + + def _remove_all_spaces(text: str) -> str: + return re.sub(r"\s+", "", text) + + is_valid = _remove_all_spaces(actual_text) == _remove_all_spaces(text) + + if is_valid: + return True + + log.warning(f"Expected text {text!r}, but got {actual_text!r}.") + + self.wait(400, 800) + + raise RuntimeError( + f"Failed to correctly type {text!r} after {max_attempts} attempts." + ) + + def move_mouse_to_center(self) -> None: + """ + Smoothly moves the mouse cursor to a random area around the center of the screen. + Takes: page (a Playwright page object) + """ + + # Move the cursor along a human-like trajectory + point: Point = get_center_page(self.page) + self.cursor.move_to(point) + + # Simulate a small pause after movement (200-600 ms) + self.wait(200, 600) + + def ensure_change_url( + self, + action: Callable[[], None], + is_ok_url: Callable[[str], bool], + max_attempts: int = 3, + ) -> None: + prev_url: str = self.page.url + log.info(f"Current URL: {prev_url!r}") + + attempt: int = 0 + while True: + # If the URL substring is found and the URL has changed + if is_ok_url(self.page.url) and prev_url != self.page.url: + break + + try: + action() + + log.info("Waiting for url load") + self.page.wait_for_url( + is_ok_url, timeout=5_000, wait_until="domcontentloaded" + ) + log.info(f"Page changed! New URL: {self.page.url!r}") + + break + + except Exception: + log.exception("Error:") + log.info("Trying to move the cursor and retry") + attempt += 1 + self.cursor.move_to( + { + "x": 10 + random.randint(-10, 10), + "y": 500 + random.randint(-100, 100), + } + ) + if attempt == max_attempts: + log.info(f"Loading {prev_url!r}") + attempt = 0 + self.page.goto(prev_url) + self.wait(4_000, 5_000) diff --git a/playwright__examples/human_automation/screenshots/github.com_gil9red.gif b/playwright__examples/human_automation/screenshots/github.com_gil9red.gif new file mode 100644 index 000000000..14df4e414 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/github.com_gil9red.gif differ diff --git a/playwright__examples/human_automation/screenshots/store_steampowered_com.gif b/playwright__examples/human_automation/screenshots/store_steampowered_com.gif new file mode 100644 index 000000000..eaa969fa6 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/store_steampowered_com.gif differ diff --git a/playwright__examples/human_automation/screenshots/translate_google_ru.gif b/playwright__examples/human_automation/screenshots/translate_google_ru.gif new file mode 100644 index 000000000..7d25d9d64 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/translate_google_ru.gif differ diff --git a/playwright__examples/parse_with_bs4.py b/playwright__examples/parse_with_bs4.py new file mode 100644 index 000000000..4e21a8a67 --- /dev/null +++ b/playwright__examples/parse_with_bs4.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from bs4 import BeautifulSoup +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + browser = p.firefox.launch() + page = browser.new_page() + + rs = page.goto("https://httpbin.org/") + text = rs.text() + browser.close() + + soup = BeautifulSoup(text, "html.parser") + print(soup.select_one("a.github-corner")["href"]) + print(soup.select_one(".description").get_text(strip=True, separator="\n")) + """ + https://github.com/requests/httpbin + A simple HTTP Request & Response Service. + Run locally: + $ docker run -p 80:80 kennethreitz/httpbin + """ diff --git a/playwright__examples/print_executable_path.py b/playwright__examples/print_executable_path.py new file mode 100644 index 000000000..fb81bd4be --- /dev/null +++ b/playwright__examples/print_executable_path.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name, browser_type.executable_path) +r""" +chromium C:\Users\ipetrash\AppData\Local\ms-playwright\chromium-1148\chrome-win\chrome.exe +firefox C:\Users\ipetrash\AppData\Local\ms-playwright\firefox-1466\firefox\firefox.exe +webkit C:\Users\ipetrash\AppData\Local\ms-playwright\webkit-2104\Playwright.exe +""" diff --git a/playwright__examples/screenshots/chromium_full_page.png b/playwright__examples/screenshots/chromium_full_page.png new file mode 100644 index 000000000..547400daa Binary files /dev/null and b/playwright__examples/screenshots/chromium_full_page.png differ diff --git a/playwright__examples/screenshots/chromium_page.png b/playwright__examples/screenshots/chromium_page.png new file mode 100644 index 000000000..632b26e62 Binary files /dev/null and b/playwright__examples/screenshots/chromium_page.png differ diff --git a/playwright__examples/screenshots/firefox_full_page.png b/playwright__examples/screenshots/firefox_full_page.png new file mode 100644 index 000000000..5a2ffe6b0 Binary files /dev/null and b/playwright__examples/screenshots/firefox_full_page.png differ diff --git a/playwright__examples/screenshots/firefox_page.png b/playwright__examples/screenshots/firefox_page.png new file mode 100644 index 000000000..611bc0d79 Binary files /dev/null and b/playwright__examples/screenshots/firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_chromium_page.png b/playwright__examples/screenshots/stealth/normal_chromium_page.png new file mode 100644 index 000000000..0e2e6c809 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_chromium_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_firefox_page.png b/playwright__examples/screenshots/stealth/normal_firefox_page.png new file mode 100644 index 000000000..f25c69aa1 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_webkit_page.png b/playwright__examples/screenshots/stealth/normal_webkit_page.png new file mode 100644 index 000000000..53f89ea02 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_webkit_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_chromium_page.png b/playwright__examples/screenshots/stealth/stealth_chromium_page.png new file mode 100644 index 000000000..36d09711d Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_chromium_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_firefox_page.png b/playwright__examples/screenshots/stealth/stealth_firefox_page.png new file mode 100644 index 000000000..976fe5b9b Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_webkit_page.png b/playwright__examples/screenshots/stealth/stealth_webkit_page.png new file mode 100644 index 000000000..0c92edfc2 Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_webkit_page.png differ diff --git a/playwright__examples/screenshots/webkit_full_page.png b/playwright__examples/screenshots/webkit_full_page.png new file mode 100644 index 000000000..5cd17dee8 Binary files /dev/null and b/playwright__examples/screenshots/webkit_full_page.png differ diff --git a/playwright__examples/screenshots/webkit_page.png b/playwright__examples/screenshots/webkit_page.png new file mode 100644 index 000000000..68cc4d083 Binary files /dev/null and b/playwright__examples/screenshots/webkit_page.png differ diff --git a/playwright__examples/stealth.py b/playwright__examples/stealth.py new file mode 100644 index 000000000..395e6ba4b --- /dev/null +++ b/playwright__examples/stealth.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from playwright.sync_api import sync_playwright +from playwright_stealth import Stealth + +DIR: Path = Path(__file__).resolve().parent +DIR_SCREENSHOTS: Path = DIR / "screenshots" / "stealth" +DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) + +URL: str = "https://bot.sannysoft.com/" + +stealth = Stealth() + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name) + + browser = browser_type.launch() + page = browser.new_page() + + page.goto(URL) + print("navigator.webdriver (default):", page.evaluate("navigator.webdriver")) + + normal_screenshot_path: Path = ( + DIR_SCREENSHOTS / f"normal_{browser_type.name}_page.png" + ) + page.screenshot(path=normal_screenshot_path) + + stealth.apply_stealth_sync(page) + page.goto(URL) + print("navigator.webdriver (stealth):", page.evaluate("navigator.webdriver")) + + stealth_screenshot_path: Path = ( + DIR_SCREENSHOTS / f"stealth_{browser_type.name}_page.png" + ) + page.screenshot(path=stealth_screenshot_path) + + browser.close() # NOTE: Close in loop + print() + +""" +chromium +navigator.webdriver (default): True +navigator.webdriver (stealth): False + +firefox +navigator.webdriver (default): True +navigator.webdriver (stealth): False + +webkit +navigator.webdriver (default): True +navigator.webdriver (stealth): False +""" diff --git a/plural_days.py b/plural_days.py new file mode 100644 index 000000000..66dbba1f9 --- /dev/null +++ b/plural_days.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +def plural_days(n: int) -> str: + days = ["день", "дня", "дней"] + + if n % 10 == 1 and n % 100 != 11: + p = 0 + elif 2 <= n % 10 <= 4 and (n % 100 < 10 or n % 100 >= 20): + p = 1 + else: + p = 2 + + return f"{n} {days[p]}" + + +if __name__ == "__main__": + for i in range(100 + 1): + print(plural_days(i)) diff --git a/png2svg.py b/png2svg.py index 2613b31ba..3b5745afc 100644 --- a/png2svg.py +++ b/png2svg.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path import sys - try: from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QPainter, QImage @@ -26,23 +25,21 @@ from PySide.QtSvg import QSvgGenerator -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) - file_name = r'C:\Users\ipetrash\Desktop\explorer.png' + file_name = r"C:\Users\ipetrash\Desktop\explorer.png" im = QImage() im.load(file_name) w, h = im.size().width(), im.size().height() gen = QSvgGenerator() - gen.setFileName(os.path.splitext(file_name)[0] + '.svg') + gen.setFileName(os.path.splitext(file_name)[0] + ".svg") gen.setSize(im.size()) gen.setViewBox(QRect(0, 0, w, h)) painter = QPainter() painter.begin(gen) - painter.drawImage(QPointF(0, 0), - im, - QRectF(0, 0, w, h)) + painter.drawImage(QPointF(0, 0), im, QRectF(0, 0, w, h)) painter.end() diff --git a/pretty_html/README.md b/pretty_html/README.md new file mode 100644 index 000000000..7eaecb47b --- /dev/null +++ b/pretty_html/README.md @@ -0,0 +1,8 @@ +pretty html +=========== + +RU: Получение html текста с отступами. + + +#### +![](screenshot.png) diff --git a/pretty_html/pretty_html.py b/pretty_html/pretty_html.py new file mode 100644 index 000000000..e2207e771 --- /dev/null +++ b/pretty_html/pretty_html.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from bs4 import BeautifulSoup + + +# SOURCE: https://stackoverflow.com/a/15513483/5909792 +orig_prettify = BeautifulSoup.prettify +r = re.compile(r"^(\s*)", re.MULTILINE) + + +def prettify(self, encoding=None, formatter="minimal", indent_width=4): + return r.sub(r"\1" * indent_width, orig_prettify(self, encoding, formatter)) + + +BeautifulSoup.prettify = prettify + + +def process_html_string(text: str) -> str: + """ + Функция из текста выдирает строку с html. Она должна начинаться на < и заканчиваться > + + """ + + start = text.index("<") + end = text.rindex(">") + return text[start : end + 1] + + +def pretty_html(text: str) -> str: + text = process_html_string(text) + root = BeautifulSoup(text, "html.parser") + return root.prettify() + + +if __name__ == "__main__": + text = "

    " + print(pretty_html(text)) diff --git a/pretty_html/pretty_html_gui.py b/pretty_html/pretty_html_gui.py new file mode 100644 index 000000000..97317a60a --- /dev/null +++ b/pretty_html/pretty_html_gui.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import traceback + +from os.path import split as path_split + +try: + from PyQt5.QtWidgets import ( + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, + ) + from PyQt5.QtCore import Qt + +except: + try: + from PyQt4.QtGui import ( + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, + ) + from PyQt4.QtCore import Qt + + except: + from PySide.QtGui import ( + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, + ) + from PySide.QtCore import Qt + +from pretty_html import pretty_html + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print(text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(path_split(__file__)[1]) + + layout = QVBoxLayout() + + self.text_edit_input = QPlainTextEdit() + self.text_edit_output = QPlainTextEdit() + + self.label_error = QLabel() + self.label_error.setStyleSheet("QLabel { color : red; }") + self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + self.button_detail_error = QPushButton("...") + self.button_detail_error.setFixedSize(20, 20) + self.button_detail_error.setToolTip("Detail error") + self.button_detail_error.hide() + + self.last_error_message = None + self.last_detail_error_message = None + + self.button_detail_error.clicked.connect(self.show_detail_error_message) + self.text_edit_input.textChanged.connect(self.input_text_changed) + + splitter = QSplitter() + splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + splitter.addWidget(self.text_edit_input) + splitter.addWidget(self.text_edit_output) + + layout.addWidget(splitter) + + layout_error = QHBoxLayout() + layout_error.addWidget(self.label_error) + layout_error.addWidget(self.button_detail_error) + + layout.addLayout(layout_error) + + self.setLayout(layout) + + def input_text_changed(self) -> None: + self.label_error.clear() + self.button_detail_error.hide() + + self.last_error_message = None + self.last_detail_error_message = None + + try: + text = self.text_edit_input.toPlainText() + text = pretty_html(text) + self.text_edit_output.setPlainText(text) + + except Exception as e: + # Выводим ошибку в консоль + traceback.print_exc() + + # Сохраняем в переменную + tb = traceback.format_exc() + + self.last_error_message = str(e) + self.last_detail_error_message = str(tb) + self.button_detail_error.show() + + self.label_error.setText("Error: " + self.last_error_message) + + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message + + mb = QErrorMessage() + mb.setWindowTitle("Error") + # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, + # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности + # выбрать тип текста, то делаем такой хак. + mb.findChild(QTextEdit).setPlainText(message) + + mb.exec_() + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.resize(650, 500) + mw.show() + + mw.text_edit_input.setPlainText( + "Example" + "

    This is an example of a simple HTML page with one paragraph.

    " + "" + ) + + app.exec_() diff --git a/pretty_html/screenshot.png b/pretty_html/screenshot.png new file mode 100644 index 000000000..35e2be139 Binary files /dev/null and b/pretty_html/screenshot.png differ diff --git a/pretty_money_format.py b/pretty_money_format.py index 380068b12..da8159729 100644 --- a/pretty_money_format.py +++ b/pretty_money_format.py @@ -1,28 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re def pretty_money_format(money) -> str: money = str(money) + money = re.sub(r"[^.,\d]", "", money) - import re - money = re.sub(r'[^.,\d]', '', money) - - if money.count('.') + money.count(',') > 1: - raise Exception('Invalid money format: "{}".'.format(money)) + if money.count(".") + money.count(",") > 1: + raise Exception(f'Invalid money format: "{money}".') - money_sep = '' - if money.count('.'): - money_sep = '.' - elif money.count(','): - money_sep = ',' + money_sep = "" + if money.count("."): + money_sep = "." + elif money.count(","): + money_sep = "," if money_sep: major, minor = money.split(money_sep) else: - major, minor = money, '' + major, minor = money, "" if len(major) > 3: new_major = list() @@ -36,29 +37,29 @@ def pretty_money_format(money) -> str: if i > 0 and i % 3 == 0: new_major.append(" ") - major = ''.join(reversed(new_major)) + major = "".join(reversed(new_major)) money = major + money_sep + minor return money.strip() -if __name__ == '__main__': - assert pretty_money_format(1000) == '1 000' - assert pretty_money_format(1000.) == '1 000.0' - assert pretty_money_format(1000000) == '1 000 000' - assert pretty_money_format(10000.56) == '10 000.56' - assert pretty_money_format(1000000.0) == '1 000 000.0' - assert pretty_money_format(100000) == '100 000' - - assert pretty_money_format('1000') == '1 000' - assert pretty_money_format('1000.') == '1 000.' - assert pretty_money_format('1000000') == '1 000 000' - assert pretty_money_format('10000.50') == '10 000.50' - assert pretty_money_format('1000000.0') == '1 000 000.0' - assert pretty_money_format('100000') == '100 000' - - assert pretty_money_format('10 00') == '1 000' - assert pretty_money_format('100 00 00') == '1 000 000' - assert pretty_money_format('1 0000.50$') == '10 000.50' - assert pretty_money_format('100 00 00.0') == '1 000 000.0' - assert pretty_money_format('100!000#') == '100 000' +if __name__ == "__main__": + assert pretty_money_format(1000) == "1 000" + assert pretty_money_format(1000.0) == "1 000.0" + assert pretty_money_format(1000000) == "1 000 000" + assert pretty_money_format(10000.56) == "10 000.56" + assert pretty_money_format(1000000.0) == "1 000 000.0" + assert pretty_money_format(100000) == "100 000" + + assert pretty_money_format("1000") == "1 000" + assert pretty_money_format("1000.") == "1 000." + assert pretty_money_format("1000000") == "1 000 000" + assert pretty_money_format("10000.50") == "10 000.50" + assert pretty_money_format("1000000.0") == "1 000 000.0" + assert pretty_money_format("100000") == "100 000" + + assert pretty_money_format("10 00") == "1 000" + assert pretty_money_format("100 00 00") == "1 000 000" + assert pretty_money_format("1 0000.50$") == "10 000.50" + assert pretty_money_format("100 00 00.0") == "1 000 000.0" + assert pretty_money_format("100!000#") == "100 000" diff --git a/pretty_xml/pretty_xml.py b/pretty_xml/pretty_xml.py index 21a00e7a2..76f734fe2 100644 --- a/pretty_xml/pretty_xml.py +++ b/pretty_xml/pretty_xml.py @@ -1,42 +1,67 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def process_xml_string(xml_string): +from xml.dom.minidom import parseString +from lxml import etree + + +def process_xml_string(xml_string: str) -> str: """ Функция из текста выдирает строку с xml -- она должна начинаться на < и заканчиваться > """ - start = xml_string.index('<') - end = xml_string.rindex('>') - return xml_string[start:end + 1] + start = xml_string.index("<") + end = xml_string.rindex(">") + return xml_string[start : end + 1] -def pretty_xml_minidom(xml_string, ind=2): +def pretty_xml_minidom(xml_string: str, indent: int = 4) -> str: """Функция принимает строку xml и выводит xml с отступами.""" - from xml.dom.minidom import parseString + xml_string: str = process_xml_string(xml_string) - xml_string = process_xml_string(xml_string) - xml_utf8 = parseString(xml_string).toprettyxml(indent=' ' * ind, encoding='utf-8') - return xml_utf8.decode('utf-8') + xml_utf8 = parseString(xml_string).toprettyxml( + indent=" " * indent, + encoding="utf-8", + ) + return xml_utf8.decode("utf-8") -def pretty_xml_lxml(xml_string): +def pretty_xml_lxml(xml_string: str) -> str: """Функция принимает строку xml и выводит xml с отступами.""" - from lxml import etree + xml_string: str = process_xml_string(xml_string) + + root = etree.fromstring( + text=xml_string, + parser=etree.XMLParser( + remove_blank_text=True, + # NOTE: Для обработки больших XML + huge_tree=True, + ), + ) - xml_string = process_xml_string(xml_string) - root = etree.fromstring(xml_string) - return etree.tostring(root, pretty_print=True, encoding='utf-8').decode('utf-8') + return etree.tostring( + root, + pretty_print=True, + encoding="utf-8", + ).decode("utf-8") -if __name__ == '__main__': - xml = '' +if __name__ == "__main__": + xml = """ + Простой хлеб + МукаДрожжиТёплая вода + + """ + + # TODO: Плохо работает print(pretty_xml_minidom(xml)) + print(pretty_xml_lxml(xml)) diff --git a/pretty_xml/pretty_xml_gui.py b/pretty_xml/pretty_xml_gui.py index 864b6817e..9ca28ee36 100644 --- a/pretty_xml/pretty_xml_gui.py +++ b/pretty_xml/pretty_xml_gui.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from os.path import split as path_split +import sys import traceback +from pathlib import Path + # TODO: избавиться от `import *` try: @@ -23,31 +25,28 @@ from PySide.QtGui import * from PySide.QtCore import Qt +from pretty_xml import pretty_xml_lxml as to_pretty_xml -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -from pretty_xml import pretty_xml_minidom as to_pretty_xml - - class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle(path_split(__file__)[1]) + self.setWindowTitle(Path(__file__).name) - layout = QVBoxLayout() + layout = QVBoxLayout(self) self.text_edit_input = QPlainTextEdit() self.text_edit_output = QPlainTextEdit() @@ -57,13 +56,13 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message = "" + self.last_detail_error_message = "" self.button_detail_error.clicked.connect(self.show_detail_error_message) self.text_edit_input.textChanged.connect(self.input_text_changed) @@ -81,14 +80,12 @@ def __init__(self): layout.addLayout(layout_error) - self.setLayout(layout) - - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message = "" + self.last_detail_error_message = "" try: text = self.text_edit_input.toPlainText() @@ -106,13 +103,13 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -121,11 +118,20 @@ def show_detail_error_message(self): mb.exec_() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() mw.resize(650, 500) mw.show() + mw.text_edit_input.setPlainText( + """ + Простой хлеб + МукаДрожжиТёплая вода + + """ + ) + app.exec_() diff --git a/prevent_multiple_instances__mutex.py b/prevent_multiple_instances__mutex.py new file mode 100644 index 000000000..c8bb31791 --- /dev/null +++ b/prevent_multiple_instances__mutex.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time + +import win32api +import win32event +import winerror + + +# Prevent multiple instances +mutex = win32event.CreateMutex(None, 1, "MY_MUTEX_PYTHON") +if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: + print("Already running!") + mutex = None + sys.exit(0) + +while True: + time.sleep(99999) diff --git a/prime_numbers.py b/prime_numbers.py index 45789a853..c7b162426 100644 --- a/prime_numbers.py +++ b/prime_numbers.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/3035188/5909792 -def get_prime_numbers(n: int) -> list: - """ Returns a list of primes < n """ +def get_prime_numbers(n: int) -> list[int]: + """Returns a list of primes < n""" + sieve = [True] * n - for i in range(3, int(n**0.5)+1, 2): + for i in range(3, int(n**0.5) + 1, 2): if sieve[i]: - sieve[i*i::2*i] = [False] * ((n-i*i-1)//(2*i)+1) + sieve[i * i : : 2 * i] = [False] * ((n - i * i - 1) // (2 * i) + 1) return [2] + [i for i in range(3, n, 2) if sieve[i]] -if __name__ == '__main__': +if __name__ == "__main__": assert get_prime_numbers(10) == [2, 3, 5, 7] - assert get_prime_numbers(100) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, - 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] + assert get_prime_numbers(100) == [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 + ] print(sum(get_prime_numbers(2_000_000))) assert sum(get_prime_numbers(2_000_000)) == 142913828922 diff --git a/print_System_Info.py b/print_System_Info.py index fd7eb83ed..2ddd03ac5 100644 --- a/print_System_Info.py +++ b/print_System_Info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Pure-L0G1C/FleX/blob/da8f30f9204a65df57063ed74b3e79a2a79a7bfc/payload/modules/sysinfo.py @@ -12,11 +12,11 @@ system_info = { - 'System': system(), - 'Release': release(), - 'Version': version(), - 'Machine': machine(), - 'Username': getuser(), - 'Architecture': architecture()[0] + "System": system(), + "Release": release(), + "Version": version(), + "Machine": machine(), + "Username": getuser(), + "Architecture": architecture()[0], } print(system_info) diff --git a/print__hprof_or_big_size_file.py b/print__hprof_or_big_size_file.py index ba593407f..13ac6acd4 100644 --- a/print__hprof_or_big_size_file.py +++ b/print__hprof_or_big_size_file.py @@ -1,34 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from multiprocessing.dummy import Pool as ThreadPool from os import walk from os.path import join, getsize from timeit import default_timer -from multiprocessing.dummy import Pool as ThreadPool - - -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - return "%3.1f %s" % (num, 'TB') +from human_byte_size import sizeof_fmt -DIRS = [r'C:\DEV__TX', r'C:\DEV__OPTT', r'C:\DEV__RADIX'] +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] def is_hprof_and_more_1GB(file_name: str) -> bool: - return '.hprof' in file_name or getsize(file_name) >= 1_000_000_000 # 1 GB + return ".hprof" in file_name and getsize(file_name) >= 1_000_000_000 # 1 GB def find_files_by_dir(root_dir: str, match_func) -> list: - print(f'Start check: {root_dir!r}') + print(f"Start check: {root_dir!r}") items = [] t = default_timer() @@ -42,7 +34,7 @@ def find_files_by_dir(root_dir: str, match_func) -> list: print(text) items.append(text) - print(f'Finish check: {root_dir!r}. Elapsed: {default_timer() - t:.3f} secs') + print(f"Finish check: {root_dir!r}. Elapsed: {default_timer() - t:.3f} secs") return items @@ -51,15 +43,17 @@ def find_files_by_dirs(dirs: list, match_func=is_hprof_and_more_1GB) -> list: items = [] with ThreadPool() as pool: - for result in pool.map(lambda root_dir: find_files_by_dir(root_dir, match_func), dirs): + for result in pool.map( + lambda root_dir: find_files_by_dir(root_dir, match_func), dirs + ): items += result return items -if __name__ == '__main__': +if __name__ == "__main__": items = find_files_by_dirs(DIRS) print() - print(f'Result ({len(items)}):') - print('\n'.join(items)) + print(f"Result ({len(items)}):") + print("\n".join(items)) diff --git a/print__hprof_or_big_size_file__notify_with_MessageBox.py b/print__hprof_or_big_size_file__notify_with_MessageBox.py index 16955b958..a07b65b56 100644 --- a/print__hprof_or_big_size_file__notify_with_MessageBox.py +++ b/print__hprof_or_big_size_file__notify_with_MessageBox.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import re import time + from pathlib import Path -import re from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtCore import Qt from print__hprof_or_big_size_file import find_files_by_dirs, DIRS -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) while True: @@ -21,10 +23,13 @@ if not result: continue - text = f'Files .hprof ({len(result)}):\n' + '\n'.join(result) + text = f"Files .hprof ({len(result)}):\n" + "\n".join(result) - msg_box = QMessageBox(QMessageBox.Information, 'Found .hprof!', text) - remove_all_files_button = msg_box.addButton("Remove all files", QMessageBox.DestructiveRole) + msg_box = QMessageBox(QMessageBox.Information, "Found .hprof!", text) + msg_box.setTextInteractionFlags(Qt.TextSelectableByMouse) + remove_all_files_button = msg_box.addButton( + "Remove all files", QMessageBox.DestructiveRole + ) msg_box.addButton(QMessageBox.Ok) msg_box.exec() @@ -38,6 +43,8 @@ try: Path(file_name).unlink() except Exception as e: - QMessageBox.warning(None, "Warning", f"Error while removed {file_name!r}: {e}") + QMessageBox.warning( + None, "Warning", f"Error while removed {file_name!r}: {e}" + ) time.sleep(5 * 60 * 60) diff --git a/print__win__netstat/main.py b/print__win__netstat/main.py index 197ca5f78..89f572b5a 100644 --- a/print__win__netstat/main.py +++ b/print__win__netstat/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Чтобы узнать кто какие занял/прослушивает порты @@ -11,7 +11,7 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/c24fb90f1c6b792ebd77e2f916ef5ec7d741c494/ascii_table__simple_pretty__rjust.py -def print_pretty_table(data, cell_sep=' | '): +def print_pretty_table(data, cell_sep=" | "): rows = len(data) cols = len(data[0]) @@ -32,27 +32,27 @@ def print_pretty_table(data, cell_sep=' | '): # Append header separate if not header_ok: - print(' + '.join('-' * width for width in col_width)) + print(" + ".join("-" * width for width in col_width)) header_ok = True def get_raw_data() -> str: - cmd = 'netstat -ano -p tcp' + cmd = "netstat -ano -p tcp" return check_output(cmd, universal_newlines=True).strip() -def get_data() -> (list, list): +def get_data() -> tuple[list, list]: text = get_raw_data() - lines = text.split('\n')[3:] + lines = text.split("\n")[3:] - headers = ['Proto', 'Local Address', 'Foreign Address', 'State', 'PID'] + headers = ["Proto", "Local Address", "Foreign Address", "State", "PID"] return headers, [line.split() for line in lines] -if __name__ == '__main__': +if __name__ == "__main__": print(get_raw_data()) - print('\n\n') + print("\n\n") headers, rows = get_data() diff --git a/print__win__wmi/print__baseboard__info.py b/print__win__wmi/print__baseboard__info.py index 29309a6c5..3788cb4ca 100644 --- a/print__win__wmi/print__baseboard__info.py +++ b/print__win__wmi/print__baseboard__info.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from subprocess import check_output import xml.etree.ElementTree as ET +from subprocess import check_output def get_baseboard_info() -> dict: # Motherboard model - cmd = 'wmic baseboard get product, Manufacturer, version, serialnumber /format:RAWXML' + cmd = "wmic baseboard get product, Manufacturer, version, serialnumber /format:RAWXML" text = check_output(cmd) root = ET.fromstring(text) result = dict() - for x in root.iter('PROPERTY'): - key = x.attrib['NAME'] - result[key] = x.find('VALUE').text + for x in root.iter("PROPERTY"): + key = x.attrib["NAME"] + result[key] = x.find("VALUE").text return result -if __name__ == '__main__': +if __name__ == "__main__": info = get_baseboard_info() print(info) - print(info['Product']) + print(info["Product"]) diff --git "a/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" "b/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" index b5f03b99a..6f6965d78 100644 --- "a/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" +++ "b/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'Г Е Н И А Л Ь Н О Е фото ' +text = "Г Е Н И А Л Ь Н О Е фото " length = len(text) for start in range(length + 1): - new_text = '' + new_text = "" for i in range(length): j = (start + i) % length new_text += text[j] - if new_text[0] != ' ': + if new_text[0] != " ": print(new_text) # Г Е Н И А Л Ь Н О Е фото diff --git a/print_ascii_art.py b/print_ascii_art.py index 9bd0e15dd..c5f0ca33c 100644 --- a/print_ascii_art.py +++ b/print_ascii_art.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -art = '''\ +art = """\ ..sSs$$$$$$b. .$$$$$$$$$$$$$$$. .$$$$$$$$$$$$$$$$$$$$$b. @@ -68,6 +68,6 @@ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ dp $ -''' +""" print(art) diff --git a/print_attr_from_object.py b/print_attr_from_object.py index 7815dfae0..fae7fd4ba 100644 --- a/print_attr_from_object.py +++ b/print_attr_from_object.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" obj = 1 -print(list(filter(lambda x: not x.startswith('_'), dir(obj)))) -for i in list(filter(lambda x: not x.startswith('_'), dir(obj))): - print(i, ':', getattr(obj, i)) +print(list(filter(lambda x: not x.startswith("_"), dir(obj)))) +for i in list(filter(lambda x: not x.startswith("_"), dir(obj))): + print(i, ":", getattr(obj, i)) print() diff --git a/print_class_name.py b/print_class_name.py index 9400ec27b..945a046c6 100644 --- a/print_class_name.py +++ b/print_class_name.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class A: pass + a = A() print(A.__name__) # A print(a.__class__.__name__) # A class B: - def __init__(self): + def __init__(self) -> None: print(self.__class__.__name__) # B + B() diff --git a/print_connections__radixware_explorer.py b/print_connections__radixware_explorer.py deleted file mode 100644 index e5547bc68..000000000 --- a/print_connections__radixware_explorer.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -FILE_NAME = r'%APPDATA%\radixware.org\explorer\connections.xml' - -import os -ABS_FILE_NAME = os.path.expandvars(FILE_NAME) - - -with open(ABS_FILE_NAME, 'rb') as f: - from bs4 import BeautifulSoup - root = BeautifulSoup(f.read(), 'html.parser') - - -def get_tag_text(tag): - return '' if tag is None else tag.text - - -headers = ['NAME', 'ID', 'COMMENT', 'USERNAME', 'STATIONNAME', 'INITIALADDRESS', 'LANGUAGE', - 'COUNTRY', 'EXPLORERROOTID', 'TRACELEVEL'] -rows = [] - -for connection in root.select('connection'): - rows.append([ - connection['name'], - connection['id'], - get_tag_text(connection.comment), - get_tag_text(connection.username), - get_tag_text(connection.stationname), - get_tag_text(connection.initialaddress), - get_tag_text(connection.language), - get_tag_text(connection.country), - get_tag_text(connection.explorerrootid), - get_tag_text(connection.tracelevel), - ]) - - -# pip install tabulate -from tabulate import tabulate -print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/print_exif/main.py b/print_exif/main.py index 9beb97b26..b45e4113a 100644 --- a/print_exif/main.py +++ b/print_exif/main.py @@ -1,25 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import base64 + +# pip install exifread +import exifread def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags - # pip install exifread - import exifread tags = exifread.process_file(file_object_or_file_name) tags_by_value = dict() if not tags: - print('Not tags') + print("Not tags") return tags_by_value - print('Tags ({}):'.format(len(tags))) + print(f"Tags ({len(tags)}):") for tag, value in tags.items(): # Process value @@ -28,9 +32,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -42,18 +46,17 @@ def get_exif_tags(file_object_or_file_name, as_category=True): except: # Example tag JPEGThumbnail if type(value) == bytes: - import base64 value = base64.b64encode(value).decode() - print(' "{}": {}'.format(tag, value)) + print(f' "{tag}": {value}') if not as_category: tags_by_value[tag] = value else: # Fill categories_by_tag - if ' ' in tag: - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -68,10 +71,11 @@ def get_exif_tags(file_object_or_file_name, as_category=True): return tags_by_value -if __name__ == '__main__': +if __name__ == "__main__": import json - print('TAGS_BY_VALUE:') - f = open('exif_this.jpg', mode='rb') + + print("TAGS_BY_VALUE:") + f = open("exif_this.jpg", mode="rb") tags_by_value = get_exif_tags(f, as_category=False) print(json.dumps(tags_by_value, sort_keys=True, ensure_ascii=False, indent=4)) # @@ -91,9 +95,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): # "Image XPTitle": "print_exif" # } - print('\n\n') - print('CATEGORIES_BY_TAG:') - f = open('exif_this.jpg', mode='rb') + print("\n\n") + print("CATEGORIES_BY_TAG:") + f = open("exif_this.jpg", mode="rb") categories_by_tag = get_exif_tags(f) print(json.dumps(categories_by_tag, sort_keys=True, ensure_ascii=False, indent=4)) # diff --git a/print_list_subdirectories_size.py b/print_list_subdirectories_size.py index d30c710fa..f513ccac2 100644 --- a/print_list_subdirectories_size.py +++ b/print_list_subdirectories_size.py @@ -1,31 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') +__author__ = "ipetrash" import os from os.path import getsize, join +from human_byte_size import sizeof_fmt + -# Словарь нужен чтобы помнить размер папки. Когда итератор дойдет до родительской папки +# Словарь нужен, чтобы помнить размер папки. Когда итератор дойдет до родительской папки # в словаре уже будут размер вложенных папок dir_sizes = dict() -for root, dirs, files in os.walk('.', topdown=False): +for root, dirs, files in os.walk(".", topdown=False): size = sum(getsize(join(root, f)) for f in files) size += sum(dir_sizes[join(root, d)] for d in dirs) dir_sizes[root] = size for path, total_size in sorted(dir_sizes.items(), key=lambda x: x[0]): - print('{} : {} ({})'.format(path, sizeof_fmt(total_size), total_size)) + print(f"{path} : {sizeof_fmt(total_size)} ({total_size})") diff --git a/print_paths_from_LNKs_of_directory_by_specific_Disc.py b/print_paths_from_LNKs_of_directory_by_specific_Disc.py index fce17a400..7dd065fdf 100644 --- a/print_paths_from_LNKs_of_directory_by_specific_Disc.py +++ b/print_paths_from_LNKs_of_directory_by_specific_Disc.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from glob import iglob import os +from glob import iglob # pip install winshell import winshell -path_desktop_lnk = os.path.expanduser(r'~\Desktop\Пройти\*.lnk') +path_desktop_lnk = os.path.expanduser(r"~\Desktop\Пройти\*.lnk") need_disc = "E:" paths = [] diff --git a/print_sizes_of_directory_LNKs_desktop.py b/print_sizes_of_directory_LNKs_desktop.py index 9c2c2e8c9..e0f98f865 100644 --- a/print_sizes_of_directory_LNKs_desktop.py +++ b/print_sizes_of_directory_LNKs_desktop.py @@ -1,45 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + from collections import defaultdict from glob import iglob -import os # pip install winshell import winshell - -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') +from human_byte_size import sizeof_fmt # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/b6ac435ee171e48ed35044e8e61e199de641a6e7/get_dir_total_size__using_glob.py -def get_dir_total_size(dir_name: str) -> (int, str): +def get_dir_total_size(dir_name: str) -> tuple[int, str]: total_size = 0 - for file_name in iglob(dir_name + '/**/*', recursive=True): + for file_name in iglob(dir_name + "/**/*", recursive=True): try: if os.path.isfile(file_name): total_size += os.path.getsize(file_name) except Exception as e: - print('File: "{}", error: "{}"'.format(file_name, e)) + print(f'File: "{file_name}", error: "{e}"') return total_size, sizeof_fmt(total_size) disc_by_number = defaultdict(int) -path_desktop_lnk = os.path.expanduser(r'~\Desktop\Пройдено\*.lnk') +path_desktop_lnk = os.path.expanduser(r"~\Desktop\Пройдено\*.lnk") paths = [] @@ -47,7 +40,7 @@ def get_dir_total_size(dir_name: str) -> (int, str): shortcut = winshell.shortcut(file_name) path = shortcut.path - if path.endswith('.exe') and os.path.isfile(path): + if path.endswith(".exe") and os.path.isfile(path): path = os.path.dirname(path) paths.append(path) @@ -61,7 +54,7 @@ def get_dir_total_size(dir_name: str) -> (int, str): for file_name in paths: size, size_str = get_dir_total_size(file_name) - print('{:<15} {:10} {}'.format(size, size_str, file_name)) + print(f"{size:<15} {size_str:10} {file_name}") total_size += size disc = file_name[0] @@ -71,22 +64,25 @@ def get_dir_total_size(dir_name: str) -> (int, str): disc_by_total_items[disc].append((size, size_str, file_name)) print() -print('Total size:', total_size, sizeof_fmt(total_size)) +print(f"Total size: {total_size} {sizeof_fmt(total_size)}") for disc in sorted(total_size_by_disc): size = total_size_by_disc[disc] - print(' {} {:<15} {}'.format(disc, size, sizeof_fmt(size))) + print(f" {disc} {size:<15} {sizeof_fmt(size)}") print() -print('Top all:') -for size, size_str, file_name in sorted(total_items, key=lambda x: x[0], reverse=True)[:5]: - print(' {:<15} bytes {:10} {}'.format(size, size_str, file_name)) +print("Top all:") +items = sorted(total_items, key=lambda x: x[0], reverse=True) +for size, size_str, file_name in items[:5]: + print(f" {size:<15} bytes {size_str:10} {file_name}") print() for disc, total_items in disc_by_total_items.items(): - print('Top of {}:'.format(disc)) - for size, size_str, file_name in sorted(total_items, key=lambda x: x[0], reverse=True)[:3]: - print(' {:<15} bytes {:10} {}'.format(size, size_str, file_name)) + print(f"Top of {disc}:") + for size, size_str, file_name in sorted( + total_items, key=lambda x: x[0], reverse=True + )[:3]: + print(f" {size:<15} bytes {size_str:10} {file_name}") print() diff --git a/print_sql_db_tables__using_sqlalchemy.py b/print_sql_db_tables__using_sqlalchemy.py index 0e44c469c..31a1e9930 100644 --- a/print_sql_db_tables__using_sqlalchemy.py +++ b/print_sql_db_tables__using_sqlalchemy.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import os - DIR = os.path.dirname(__file__) - DB_FILE_NAME = 'sqlite:///' + os.path.join(DIR, 'database') +import os - # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) - from sqlalchemy import create_engine - engine = create_engine( - DB_FILE_NAME, - # echo=True, - pool_recycle=7200 - ) +from sqlalchemy import create_engine +from sqlalchemy.engine import reflection - from sqlalchemy.engine import reflection - inspector = reflection.Inspector.from_engine(engine) - table_names = inspector.get_table_names() - for table in table_names: - columns = [column_info['name'] for column_info in inspector.get_columns(table)] - print('{}: {}'.format(table, ', '.join(columns))) +DIR = os.path.dirname(__file__) +DB_FILE_NAME = "sqlite:///" + os.path.join(DIR, "database") - for row in engine.execute("select * from {}".format(table)): - print(row.values()) +# Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) +engine = create_engine( + DB_FILE_NAME, + # echo=True, + pool_recycle=7200, +) + +inspector = reflection.Inspector.from_engine(engine) + +table_names = inspector.get_table_names() +for table in table_names: + columns = [column_info["name"] for column_info in inspector.get_columns(table)] + print(f"{table}: {', '.join(columns)}") + + for row in engine.execute(f"select * from {table}"): + print(row.values()) diff --git a/print_triangle.py b/print_triangle.py deleted file mode 100644 index 35bb0561d..000000000 --- a/print_triangle.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def print_triangle(n): - """ - n = 5 - - * - *** - ***** - ******* -********* - - """ - - starts = 1 - - for i in range(1, n + 1): - fill = ' ' * (n - i) - print(fill + '*' * starts + fill) - starts += 2 - - -if __name__ == '__main__': - print_triangle(5) diff --git a/profile__example.py b/profile__example.py index d932e9203..22b38634e 100644 --- a/profile__example.py +++ b/profile__example.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# LOOK: 1000 0.016 0.000 0.016 0.000 profile__example.py:14(_has) +import profile -def foo(number=100): - def _inc_i(): +def foo(number=100) -> None: + def _inc_i() -> None: nonlocal i i += 1 - def _has(value, items): + def _has(value, items) -> bool: return value in items - items = ['1234567890' for _ in range(number)] + items = ["1234567890" for _ in range(number)] print(items) i = 0 for i in range(number): _inc_i() - _has('5', items) + _has("5", items) print(i) -import profile -profile.run(r'foo(1000)') +profile.run(r"foo(1000)") +# LOOK: 1000 0.016 0.000 0.016 0.000 profile__example.py:14(_has) diff --git a/programmer_day.py b/programmer_day.py index 9601fe536..6a6785d1f 100644 --- a/programmer_day.py +++ b/programmer_day.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from datetime import datetime, timedelta -day_of_year = int(datetime.today().strftime('%j')) -print('day_of_year:', day_of_year) +from datetime import date, timedelta -last_programmer_day = 256 - day_of_year -print('last_programmer_day:', last_programmer_day) -if last_programmer_day >= 0: - programmer_day = (datetime.today() + timedelta(days=last_programmer_day)) - print('\nday of programmer_day:', programmer_day.strftime('%j')) - print('programmer_day:', programmer_day) +day_of_year: int = int(date.today().strftime("%j")) +print(f"Current day of year: {day_of_year}") + +last_programmer_day: int = 256 - day_of_year +print(f"Until Programmer's day: {last_programmer_day}") +if last_programmer_day >= 0: + programmer_day = date.today() + timedelta(days=last_programmer_day) + print() + print(f"Day of programmer day: {programmer_day:%j}") + print(f"Programmer day: {programmer_day}") else: - print('Has already passed') + print("Has already passed") diff --git a/proxy.py__examples/ca_certificate.py b/proxy.py__examples/ca_certificate.py new file mode 100644 index 000000000..878b916fb --- /dev/null +++ b/proxy.py__examples/ca_certificate.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# NOTE: Analog https://github.com/abhinavsingh/proxy.py/blob/754d5a35d28347716dfaefea91418826148ed903/Makefile#L60 + + +import os +from config import CA_KEY_FILE_PATH, CA_CERT_FILE_PATH, CA_SIGNING_KEY_FILE_PATH + + +def main() -> None: + # Generate CA key + os.system( + f"python -m proxy.common.pki gen_private_key --private-key-path {CA_KEY_FILE_PATH}" + ) + os.system( + f"python -m proxy.common.pki remove_passphrase --private-key-path {CA_KEY_FILE_PATH}" + ) + + # Generate CA certificate + os.system( + f"python -m proxy.common.pki gen_public_key --private-key-path {CA_KEY_FILE_PATH} " + f"--public-key-path {CA_CERT_FILE_PATH}" + ) + + # Generate key that will be used to generate domain certificates on the fly + # Generated certificates are then signed with CA certificate / key generated above + os.system( + f"python -m proxy.common.pki gen_private_key --private-key-path {CA_SIGNING_KEY_FILE_PATH}" + ) + os.system( + f"python -m proxy.common.pki remove_passphrase --private-key-path {CA_SIGNING_KEY_FILE_PATH}" + ) + + +if __name__ == "__main__": + main() diff --git a/proxy.py__examples/config.py b/proxy.py__examples/config.py new file mode 100644 index 000000000..682255cb5 --- /dev/null +++ b/proxy.py__examples/config.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# TODO: support pathlib +CA_KEY_FILE_PATH = "ca-key.pem" +CA_CERT_FILE_PATH = "ca-cert.pem" +CA_SIGNING_KEY_FILE_PATH = "ca-signing-key.pem" diff --git a/proxy.py__examples/hello_world.py b/proxy.py__examples/hello_world.py new file mode 100644 index 000000000..0a1ecc404 --- /dev/null +++ b/proxy.py__examples/hello_world.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/abhinavsingh/proxy.py + + +import ipaddress + +# pip install --upgrade proxy.py +import proxy +from proxy.common.utils import bytes_ +from proxy.http.parser import HttpParser +from proxy.plugin import FilterByUpstreamHostPlugin +from proxy.http.proxy import HttpProxyBasePlugin + + +# NOTE: Analog https://github.com/gil9red/SimplePyScripts/blob/3d8afdec07c2cfbc94fcb3fd792721e1e8565a42/http.server__examples/simple_http_proxy_server/main.py#L18 +class AddHeadersPlugin(HttpProxyBasePlugin): + def before_upstream_connection(self, request: HttpParser) -> HttpParser | None: + return request + + def handle_client_request(self, request: HttpParser) -> HttpParser | None: + # NOTE: Add custom header + request.add_header(b"x-my-proxy", b"hell yeah!") + request.add_header(b"x-my-client-ip", bytes_(self.client.addr[0])) + + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass + + +def main(*args, **kwargs) -> None: + proxy.main( + *args, + hostname=ipaddress.ip_address("127.0.0.1"), + port=33333, + plugins=[ + "proxy.plugin.CacheResponsesPlugin", # Adding plugin v1 + FilterByUpstreamHostPlugin, # Adding plugin v2 + AddHeadersPlugin, # Adding custom plugin + ], + **kwargs + ) + + +if __name__ == "__main__": + main() diff --git a/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py b/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py new file mode 100644 index 000000000..65c26694f --- /dev/null +++ b/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/abhinavsingh/proxy.py#tls-interception + +# NOTE: When error "ssl.SSLError: [X509: KEY_VALUES_MISMATCH] key values mismatch (_ssl.c:3845)" +# try remove all files from "C:\Users\\.proxy\certificates" + + +import os.path + +import ca_certificate + +from config import CA_KEY_FILE_PATH, CA_CERT_FILE_PATH, CA_SIGNING_KEY_FILE_PATH +from hello_world import main + + +if not os.path.exists(CA_CERT_FILE_PATH): + ca_certificate.main() + + +if __name__ == "__main__": + main( + ca_key_file=CA_KEY_FILE_PATH, + ca_cert_file=CA_CERT_FILE_PATH, + ca_signing_key_file=CA_SIGNING_KEY_FILE_PATH, + ) diff --git a/proxy.py__examples/test.py b/proxy.py__examples/test.py new file mode 100644 index 000000000..cb39e7c5f --- /dev/null +++ b/proxy.py__examples/test.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os.path + +import requests + +import ca_certificate +from config import CA_CERT_FILE_PATH + + +if not os.path.exists(CA_CERT_FILE_PATH): + ca_certificate.main() + + +DEBUG_LOG = False + + +proxies = { + "http": "localhost:33333", + "https": "localhost:33333", +} + +# NOTE: When error "ssl.SSLError: [X509: KEY_VALUES_MISMATCH] key values mismatch (_ssl.c:3845)" +# try remove all files from "C:\Users\\.proxy\certificates" +for url in ["http://httpbin.org/headers", "https://httpbin.org/headers"]: + print(url) + + # NOTE: The "verify" parameter is needed for HTTPS interception + # You can specify "verify=False", but there will be warnings + rs = requests.get(url, proxies=proxies, verify=CA_CERT_FILE_PATH) + + DEBUG_LOG and print(rs) + DEBUG_LOG and print(rs.url) + DEBUG_LOG and print(rs.headers) + DEBUG_LOG and print(rs.content) + + for header, value in rs.json()["headers"].items(): + if header.lower().startswith("x-my"): + print(f"{header}: {value!r}") + + print() + +""" +http://httpbin.org/headers +X-My-Client-Ip: '127.0.0.1' +X-My-Proxy: 'hell yeah!' + +https://httpbin.org/headers +X-My-Client-Ip: '127.0.0.1' +X-My-Proxy: 'hell yeah!' +""" diff --git a/proxy/proxy_requests__upgraded.py b/proxy/proxy_requests__upgraded.py deleted file mode 100644 index 2e1d2ba2e..000000000 --- a/proxy/proxy_requests__upgraded.py +++ /dev/null @@ -1,104 +0,0 @@ -import random -from re import findall - -import requests - -# rootVIII -# pycodestyle validated -# 2018-2020 - -# SOURCE: https://github.com/rootVIII/proxy_requests/blob/c8a4ebce4a9b9774b4cf7baa605dce3bbf879832/proxy_requests.py -# gil9red -# 2020 - - -DEBUG_LOG = False - - -class ProxyRequests: - EXPECTED_ERRORS = [ - 'ConnectTimeout', - 'ProxyError', - 'SSLError', - 'ReadTimeout', - 'ConnectionError', - 'ConnectTimeoutError' - ] - EMPTY_WARN = 'Proxy Pool has been emptied' - - def __init__(self, url: str, timeout=5): - self.url = url - self.proxies = [] - self.used_proxy = None - self.timeout = timeout - self._acquire_sockets() - - def _acquire_sockets(self): - rs = requests.get('https://www.sslproxies.org/') - matches = findall(r"\d+\.\d+\.\d+\.\d+\d+", rs.text) - revised = [m.replace('', '') for m in matches] - self.proxies = [s[:-5].replace('', ':') for s in revised] - random.shuffle(self.proxies) - - DEBUG_LOG and print(f'[+] Total proxies: {len(self.proxies)}') - - def _pop_random_proxy(self) -> str: - if not self.proxies: - raise Exception(self.EMPTY_WARN) - - return self.proxies.pop() - - def _is_err(self, err): - if type(err).__name__ not in self.EXPECTED_ERRORS: - raise err - - def request(self, method: str, **kwargs) -> requests.Response: - i = 0 - - while True: - i += 1 - - current_proxy = self._pop_random_proxy() - proxies = { - 'http': 'http://' + current_proxy, - 'https': 'https://' + current_proxy - } - self.used_proxy = current_proxy - - try: - DEBUG_LOG and print(f'[+] Attempt #{i}, proxy: {current_proxy}') - - rs = requests.request( - method, - self.url, - proxies=proxies, - timeout=self.timeout, - **kwargs, - ) - - return rs - - except Exception as e: - DEBUG_LOG and print(f'[#] Fail with {type(e).__name__}! Attempt #{i}, proxy: {current_proxy}') - self._is_err(e) - - def get(self, params=None, **kwargs) -> requests.Response: - return self.request('get', params=params, **kwargs) - - def post(self, params=None, **kwargs) -> requests.Response: - return self.request('post', params=params, **kwargs) - - def options(self, params=None, **kwargs) -> requests.Response: - return self.request('options', params=params, **kwargs) - - def head(self, params=None, **kwargs) -> requests.Response: - return self.request('head', params=params, **kwargs) - - def put(self, params=None, **kwargs) -> requests.Response: - return self.request('put', params=params, **kwargs) - - def patch(self, params=None, **kwargs) -> requests.Response: - return self.request('patch', params=params, **kwargs) - - def delete(self, params=None, **kwargs) -> requests.Response: - return self.request('delete', params=params, **kwargs) diff --git a/psutil_example/cpu_info.py b/psutil_example/cpu_info.py index ff4921e88..4c734f2a0 100644 --- a/psutil_example/cpu_info.py +++ b/psutil_example/cpu_info.py @@ -1,32 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil -print('CPU info:', psutil.cpu_times()) + +print("CPU info:", psutil.cpu_times()) print() for _ in range(3): info = psutil.cpu_percent(interval=1) - print('CPU percent (interval=1, percpu=False):', info) + print("CPU percent (interval=1, percpu=False):", info) print() for _ in range(3): info = psutil.cpu_percent(interval=1, percpu=True) - print('CPU percent (interval=1, percpu=True):', info) + print("CPU percent (interval=1, percpu=True):", info) print() for _ in range(3): info = psutil.cpu_times_percent(interval=1, percpu=False) - print('CPU times percent (interval=1, percpu=False):', info) + print("CPU times percent (interval=1, percpu=False):", info) print() -print('Logical CPUs:', psutil.cpu_count()) -print('Physical CPUs:', psutil.cpu_count(logical=False)) +print("Logical CPUs:", psutil.cpu_count()) +print("Physical CPUs:", psutil.cpu_count(logical=False)) print() -print('CPU stats:', psutil.cpu_stats()) -print('CPU freq:', psutil.cpu_freq()) +print("CPU stats:", psutil.cpu_stats()) +print("CPU freq:", psutil.cpu_freq()) diff --git a/psutil_example/disc_used_DIFF/common.py b/psutil_example/disc_used_DIFF/common.py index 05e254563..2be79686a 100644 --- a/psutil_example/disc_used_DIFF/common.py +++ b/psutil_example/disc_used_DIFF/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -13,31 +13,31 @@ # Absolute file name -FILE_NAME_SNAPSHOT = str(pathlib.Path(__file__).resolve().parent / 'snapshot.json') +FILE_NAME_SNAPSHOT = str(pathlib.Path(__file__).resolve().parent / "snapshot.json") def sizeof_fmt(num: Union[int, float], with_sign=False) -> str: if with_sign: - sign = '+' if num >= 0 else '-' + sign = "+" if num >= 0 else "-" else: - sign = '' + sign = "" if num == 0: - sign = '' + sign = "" num = abs(num) - for x in ['bytes', 'KB', 'MB', 'GB']: + for x in ["bytes", "KB", "MB", "GB"]: if num < 1024.0: return "%s%3.1f %s" % (sign, num, x) num /= 1024.0 - return "%s%3.1f %s" % (sign, num, 'TB') + return "%s%3.1f %s" % (sign, num, "TB") def get_disc_list() -> List[str]: - return [disk.device for disk in psutil.disk_partitions() if 'fixed' in disk.opts] + return [disk.device for disk in psutil.disk_partitions() if "fixed" in disk.opts] def get_human_sizes(items: List[int], with_sign) -> List[str]: @@ -58,18 +58,18 @@ def get_disc_total_used(disc_list: List[str]) -> str: return sizeof_fmt(sum(psutil.disk_usage(disk).used for disk in disc_list)) -def write_snapshot_raw_disc_used(disc_list: List[str] = None): - with open(FILE_NAME_SNAPSHOT, 'w', encoding='utf-8') as f: +def write_snapshot_raw_disc_used(disc_list: List[str] = None) -> None: + with open(FILE_NAME_SNAPSHOT, "w", encoding="utf-8") as f: items = get_disc_used(disc_list, only_raw=True) - print('Write snapshot with:', items) + print("Write snapshot with:", items) json.dump(items, f) def load_snapshot_raw_disc_used() -> List[int]: - with open(FILE_NAME_SNAPSHOT, encoding='utf-8') as f: + with open(FILE_NAME_SNAPSHOT, encoding="utf-8") as f: items = json.load(f) - print('Read snapshot:', items) + print("Read snapshot:", items) return items diff --git a/psutil_example/disc_used_DIFF/create_disc_snapshot.py b/psutil_example/disc_used_DIFF/create_disc_snapshot.py index 4d662ae9c..c483e1a6b 100644 --- a/psutil_example/disc_used_DIFF/create_disc_snapshot.py +++ b/psutil_example/disc_used_DIFF/create_disc_snapshot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import write_snapshot_raw_disc_used diff --git a/psutil_example/disc_used_DIFF/main.py b/psutil_example/disc_used_DIFF/main.py index eb7d460fe..9a54e6967 100644 --- a/psutil_example/disc_used_DIFF/main.py +++ b/psutil_example/disc_used_DIFF/main.py @@ -1,26 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install tabulate from tabulate import tabulate -from common import get_disc_list, get_disc_used, get_human_sizes, get_row_for, load_snapshot_raw_disc_used +from common import ( + get_disc_list, + get_disc_used, + get_human_sizes, + get_row_for, + load_snapshot_raw_disc_used, +) disc_list = get_disc_list() disc_list_raw = get_disc_used(disc_list, only_raw=True) disc_list_snapshot_raw = load_snapshot_raw_disc_used() -disc_list_diff_raw = [b-a for a, b in zip(disc_list_snapshot_raw, disc_list_raw)] +disc_list_diff_raw = [b - a for a, b in zip(disc_list_snapshot_raw, disc_list_raw)] -headers = [''] + disc_list + ['TOTAL USED'] +headers = [""] + disc_list + ["TOTAL USED"] rows = [ - get_row_for('', disc_list_snapshot_raw), - get_row_for('', disc_list_raw), - get_row_for('', disc_list_diff_raw, with_sign=True), + get_row_for("", disc_list_snapshot_raw), + get_row_for("", disc_list_raw), + get_row_for("", disc_list_diff_raw, with_sign=True), ] print() diff --git a/psutil_example/disk_info.py b/psutil_example/disk_info.py index d2ed61682..ca824ff52 100644 --- a/psutil_example/disk_info.py +++ b/psutil_example/disk_info.py @@ -1,56 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') - +# pip install humanize +from humanize import naturalsize as sizeof_fmt # pip install psutil import psutil -print('Disk partitions:') +# pip install tabulate +from tabulate import tabulate + + +print("Disk partitions:") for disk in psutil.disk_partitions(): - print(' {}'.format(disk)) + print(f" {disk}") print() -print('Disk usage:') -for disk in filter(lambda x: 'fixed' in x.opts, psutil.disk_partitions()): +print("Disk usage:") +for disk in filter(lambda x: "fixed" in x.opts, psutil.disk_partitions()): info = psutil.disk_usage(disk.device) - print(' {} {}'.format(disk.device, info)) - print(' {} free of {}'.format(sizeof_fmt(info.free), sizeof_fmt(info.total))) + print(f" {disk.device} {info}") + print(f" {sizeof_fmt(info.free)} free of {sizeof_fmt(info.total)}") print() print() -print('Disk io (input/output) total sum counters:') -print(' {}'.format(psutil.disk_io_counters())) +print("Disk io (input/output) total sum counters:") +print(f" {psutil.disk_io_counters()}") physical_drive_by_info = list(psutil.disk_io_counters(True).items()) print() -print('Physical drive io (input/output) counters ({}):'.format(len(physical_drive_by_info))) -# -# for drive, info in physical_drive_by_info: -# print(' {}: {}'.format(drive, info)) -# # -# # OR: -# # -headers = ('drive',) + physical_drive_by_info[0][1]._fields +print(f"Physical drive io (input/output) counters ({len(physical_drive_by_info)}):") + +headers = ("drive",) + physical_drive_by_info[0][1]._fields headers = [header.upper() for header in headers] rows = [(drive,) + tuple(info) for drive, info in physical_drive_by_info] - -# pip install tabulate -from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) -print() -print() diff --git a/psutil_example/get_battery_status.py b/psutil_example/get_battery_status.py index ea2fd6797..5116944d0 100644 --- a/psutil_example/get_battery_status.py +++ b/psutil_example/get_battery_status.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil + + battery = psutil.sensors_battery() print(battery.percent) diff --git a/psutil_example/get_win_services.py b/psutil_example/get_win_services.py new file mode 100644 index 000000000..26361ecb1 --- /dev/null +++ b/psutil_example/get_win_services.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install psutil +import psutil +from psutil._pswindows import WindowsService + + +def get_win_services() -> list[WindowsService]: + return list(psutil.win_service_iter()) + + +if __name__ == "__main__": + win_service_list = get_win_services() + print(f"Win service list ({len(win_service_list)}):") + + for service in win_service_list: + title = f"{service.name()!r} ({service.display_name()})" + path = ( + f"Pid={service.pid()}, name={service.name()!r}, display_name={service.display_name()!r}, " + f"status={service.status()!r}, start_type={service.start_type()!r}" + ) + print("Title:", title) + print("Path:", path) + print("Status:", service.status()) + print("binpath:", service.binpath()) + print() diff --git a/psutil_example/hello_world.py b/psutil_example/hello_world.py index 6a1f82e01..01a6e6232 100644 --- a/psutil_example/hello_world.py +++ b/psutil_example/hello_world.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil + + psutil.test() diff --git a/psutil_example/kill_process_by_name.py b/psutil_example/kill_process_by_name.py index e06ffad92..889c8f93a 100644 --- a/psutil_example/kill_process_by_name.py +++ b/psutil_example/kill_process_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -9,5 +9,5 @@ for process in psutil.process_iter(): - if process.name() == 'calc.exe': + if process.name() == "calc.exe": process.kill() diff --git a/psutil_example/loop_kill_process_by_name.py b/psutil_example/loop_kill_process_by_name.py index d5e5c27f4..d23f99ea2 100644 --- a/psutil_example/loop_kill_process_by_name.py +++ b/psutil_example/loop_kill_process_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -12,7 +12,7 @@ while True: for process in psutil.process_iter(): - if process.name() == 'calc.exe': + if process.name() == "calc.exe": process.kill() # Wait 5 secs diff --git a/psutil_example/memory_info.py b/psutil_example/memory_info.py index f3579be09..ac1744368 100644 --- a/psutil_example/memory_info.py +++ b/psutil_example/memory_info.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil -print('System memory:', psutil.virtual_memory()) -print('System swap memory:', psutil.swap_memory()) + + +print("System memory:", psutil.virtual_memory()) +print("System swap memory:", psutil.swap_memory()) diff --git a/psutil_example/network_info/net_connections.py b/psutil_example/network_info/net_connections.py index 86e7535fc..bdc25cd6d 100644 --- a/psutil_example/network_info/net_connections.py +++ b/psutil_example/network_info/net_connections.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_connections = psutil.net_connections() -print('Net connections ({}):'.format(len(net_connections))) +print(f"Net connections ({len(net_connections)}):") # Sort by pid net_connections.sort(key=lambda x: x.pid) @@ -23,7 +27,4 @@ headers = [header.upper() for header in headers] rows = [tuple(info) for info in net_connections] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid", showindex=True)) diff --git a/psutil_example/network_info/net_interface_by_address_list.py b/psutil_example/network_info/net_interface_by_address_list.py index bf797234a..356ae34d2 100644 --- a/psutil_example/network_info/net_interface_by_address_list.py +++ b/psutil_example/network_info/net_interface_by_address_list.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_interface_by_address_list = list(psutil.net_if_addrs().items()) -print('Net interface by address list ({}):'.format(len(net_interface_by_address_list))) +print(f"Net interface by address list ({len(net_interface_by_address_list)}):") # Sort by name interface net_interface_by_address_list.sort(key=lambda x: x[0]) @@ -23,15 +27,13 @@ # # OR: # # if net_interface_by_address_list: - headers = ('name',) + net_interface_by_address_list[0][1][0]._fields + headers = ("name",) + net_interface_by_address_list[0][1][0]._fields headers = [header.upper() for header in headers] rows = [] for name, address_list in net_interface_by_address_list: for i, address in enumerate(address_list): - rows.append((name if i == 0 else '',) + tuple(address)) + rows.append((name if i == 0 else "",) + tuple(address)) - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/network_info/net_interface_list.py b/psutil_example/network_info/net_interface_list.py index 561c964ac..88f2f7b75 100644 --- a/psutil_example/network_info/net_interface_list.py +++ b/psutil_example/network_info/net_interface_list.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_interface_list = list(psutil.net_io_counters(pernic=True).items()) -print('Net interface list ({}):'.format(len(net_interface_list))) +print(f"Net interface list ({len(net_interface_list)}):") # Sort by name interface net_interface_list.sort(key=lambda x: x[0]) @@ -20,11 +24,8 @@ # # if net_interface_list: info_fields = net_interface_list[0][1]._fields - headers = ('name',) + info_fields + headers = ("name",) + info_fields headers = [header.upper() for header in headers] rows = [(interface,) + tuple(info) for interface, info in net_interface_list] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/network_info/network_interface_card_by_info.py b/psutil_example/network_info/network_interface_card_by_info.py index de795cefd..c4b0b8c73 100644 --- a/psutil_example/network_info/network_interface_card_by_info.py +++ b/psutil_example/network_info/network_interface_card_by_info.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + network_interface_card_by_info = list(psutil.net_if_stats().items()) -print('Network interface card by info ({}):'.format(len(network_interface_card_by_info))) +print(f"Network interface card by info ({len(network_interface_card_by_info)}):") # Sort by name interface network_interface_card_by_info.sort(key=lambda x: x[0]) @@ -20,11 +24,8 @@ # # if network_interface_card_by_info: - headers = ('name',) + network_interface_card_by_info[0][1]._fields + headers = ("name",) + network_interface_card_by_info[0][1]._fields headers = [header.upper() for header in headers] rows = [(name,) + tuple(info) for name, info in network_interface_card_by_info] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/on_new_process_handler.py b/psutil_example/on_new_process_handler.py index 5d6be6934..b2092c618 100644 --- a/psutil_example/on_new_process_handler.py +++ b/psutil_example/on_new_process_handler.py @@ -1,37 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from threading import Thread from time import sleep -from typing import Callable, List +from typing import Callable # pip install psutil import psutil -ListenerFunc = Callable[[List], None] +ListenerFunc = Callable[[list], None] class OnNewProcessHandler(Thread): - def __init__(self, timeout_secs=1.0): + def __init__(self, timeout_secs=1.0) -> None: super().__init__() self._process = dict() - self._listeners: List[ListenerFunc] = [] + self._listeners: list[ListenerFunc] = [] self._timeout_secs = timeout_secs - def add_listener(self, listener: ListenerFunc): + def add_listener(self, listener: ListenerFunc) -> None: if listener not in self._listeners: self._listeners.append(listener) - def remove_listener(self, listener: ListenerFunc): + def remove_listener(self, listener: ListenerFunc) -> None: if listener in self._listeners: self._listeners.remove(listener) - def remove_all_listener(self): + def remove_all_listener(self) -> None: self._listeners.clear() @staticmethod @@ -40,15 +40,15 @@ def get_process() -> dict: for proc in psutil.process_iter(): try: - pinfo = proc.as_dict(attrs=['pid', 'name', 'exe']) - items[pinfo['pid']] = pinfo + pinfo = proc.as_dict(attrs=["pid", "name", "exe"]) + items[pinfo["pid"]] = pinfo except psutil.NoSuchProcess: continue return items - def run(self): + def run(self) -> None: while True: current_process = self.get_process() @@ -69,18 +69,18 @@ def run(self): sleep(self._timeout_secs) -if __name__ == '__main__': - def print_listener(process_list): +if __name__ == "__main__": + + def print_listener(process_list) -> None: result = [] for p in process_list: result.append(f"{p['name']} (pid={p['pid']})") - print('-' * 10 + '\n' + '\n'.join(result) + '\n' + '-' * 10) - + print("-" * 10 + "\n" + "\n".join(result) + "\n" + "-" * 10) handler = OnNewProcessHandler() handler.add_listener(print_listener) handler.start() sleep(2) - handler.add_listener(lambda x: print(f'New process: {len(x)}')) + handler.add_listener(lambda x: print(f"New process: {len(x)}")) diff --git a/psutil_example/other_system_info.py b/psutil_example/other_system_info.py index 6105dc634..0df75294a 100644 --- a/psutil_example/other_system_info.py +++ b/psutil_example/other_system_info.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime + # pip install psutil import psutil -print('Users:', psutil.users()) + + +print("Users:", psutil.users()) print() boot_time = psutil.boot_time() -print('System boot time (timestamp):', boot_time) +print("System boot time (timestamp):", boot_time) -from datetime import datetime -print('System boot time:', datetime.fromtimestamp(boot_time)) +print("System boot time:", datetime.fromtimestamp(boot_time)) diff --git a/psutil_example/print_all_webservers/get_info_html.py b/psutil_example/print_all_webservers/get_info_html.py new file mode 100644 index 000000000..476373f37 --- /dev/null +++ b/psutil_example/print_all_webservers/get_info_html.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import tempfile + +from dataclasses import dataclass + +# pip install psutil==6.1.0 +import psutil + + +@dataclass +class Process: + pid: int + name: str + path: str + cwd: str + ports: str + + +TITLE = "Список запущенных серверов" + + +def _get_processes() -> list[Process]: + processes = [] + for proc in psutil.process_iter(): + try: + connections = [ + c + for c in proc.net_connections() + if c.status == psutil.CONN_LISTEN and c.laddr + ] + if not connections: + continue + + ports = sorted(set(c.laddr.port for c in connections)) + processes.append( + Process( + pid=proc.pid, + name=proc.name(), + path=" ".join(os.path.normpath(x) for x in proc.cmdline()), + cwd=proc.cwd(), + ports=", ".join(map(str, ports)), + ) + ) + + except psutil.AccessDenied: + pass + + processes.sort(key=lambda p: p.name) + + return processes + + +def get_info_html() -> str: + template = """ + + + + + {{ title }} + + + + + + + + + + + + + + + + + + {{ table_rows }} + +
    {{ title }}
    #PIDНазваниеПорт(ы)ПутьCwd
    + + + """ + + table_rows: list[str] = [ + f""" + + {i} + {p.pid} + {p.name} +
    {p.ports}
    +
    {p.path}
    +
    {p.cwd}
    + + """ + for i, p in enumerate(_get_processes(), 1) + ] + + return template.replace( + "{{ title }}", TITLE + ).replace( + "{{ table_rows }}", "\n".join(table_rows) + ) + + +def open_html_file() -> None: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + prefix="webservers-", + suffix=".html", + delete=False, + ) as fp: + fp.write(get_info_html()) + path: str = fp.name + + os.startfile(path) + + +if __name__ == "__main__": + open_html_file() diff --git a/psutil_example/print_all_webservers/web.py b/psutil_example/print_all_webservers/web.py index 114811c60..0687ee765 100644 --- a/psutil_example/print_all_webservers/web.py +++ b/psutil_example/print_all_webservers/web.py @@ -1,130 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from http.server import BaseHTTPRequestHandler, HTTPServer import os -from urllib.parse import urlsplit - -# pip install psutil -import psutil - - -TITLE = "Список запущенных серверов" -HEADERS = ["#", "PID", "Название", "Порт(ы)", "Путь"] - -def _get_processes() -> list: - processes = [] - for proc in psutil.process_iter(): - try: - connections = [c for c in proc.connections() if c.status == psutil.CONN_LISTEN and c.laddr] - if not connections: - continue - - ports = sorted(set(c.laddr.port for c in connections)) - processes.append({ - 'pid': proc.pid, - 'name': proc.name(), - 'path': ' '.join(os.path.normpath(x) for x in proc.cmdline()), - 'ports': ', '.join(map(str, ports)), - }) - - except psutil.AccessDenied: - pass - - processes.sort(key=lambda p: int(''.join(c for c in p['ports'] if c.isdigit()))) +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlsplit - return processes +from get_info_html import get_info_html class HttpProcessor(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: o = urlsplit(self.path) # Only index - if o.path != '/': + if o.path != "/": self.send_error(404) return - table_rows = [] - for i, p in enumerate(_get_processes(), 1): - table_rows.append(f''' - - {i} - {p['pid']} - {p['name']} - {p['ports']} - {p['path']} - - ''') - - text = """ - - - - - {{ title }} - - - - - - - - - - - - {{ headers }} - - {{ table_rows }} - -
    {{ title }}
    - - - """ \ - .replace('{{ title }}', TITLE) \ - .replace('{{ headers }}', ''.join(f'{x}' for x in HEADERS)) \ - .replace('{{ table_rows }}', ''.join(table_rows)) + text = get_info_html() self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Connection', 'close') + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Connection", "close") self.end_headers() - self.wfile.write(text.encode('utf-8')) + self.wfile.write(text.encode("utf-8")) -def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080): - print(f'HTTP server running on http://127.0.0.1:{port}') +def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080) -> None: + print(f"HTTP server running on http://127.0.0.1:{port}") - server_address = ('', port) + server_address = ("", port) httpd = server_class(server_address, handler_class) httpd.serve_forever() -if __name__ == '__main__': - run(port=10013) +if __name__ == "__main__": + run( + port=int(os.environ.get("PORT", 50012)), + ) diff --git a/psutil_example/print_current_python_webservers/web.py b/psutil_example/print_current_python_webservers/web.py index f9d176cb0..9b36cc0db 100644 --- a/psutil_example/print_current_python_webservers/web.py +++ b/psutil_example/print_current_python_webservers/web.py @@ -1,70 +1,78 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from http.server import BaseHTTPRequestHandler, HTTPServer import os + +from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlsplit -# pip install psutil +# pip install psutil==6.1.0 import psutil -NEED_PROCESS = ['python.exe', 'pythonw.exe'] +NEED_PROCESS = ["python.exe", "pythonw.exe"] TITLE = "Список запущенных серверов на python" HEADERS = ["#", "PID", "Порт(ы)", "Путь"] -def _get_processes() -> list: +def _port_to_tag_a(port: int) -> str: + return f'{port}' + + +def _get_processes() -> list[dict[str, str | int]]: processes = [] for proc in psutil.process_iter(): if not proc.is_running() or proc.name().lower() not in NEED_PROCESS: continue - connections = [c for c in proc.connections() if c.status == psutil.CONN_LISTEN and c.laddr] + connections = [ + c for c in proc.net_connections() if c.status == psutil.CONN_LISTEN and c.laddr + ] if not connections: continue ports = sorted(set(c.laddr.port for c in connections)) - processes.append({ - 'pid': proc.pid, - 'name': proc.name(), - 'path': os.path.normpath(proc.cmdline()[1]), - 'ports': ', '.join(map(_port_to_tag_a, ports)), - }) + processes.append( + { + "pid": proc.pid, + "name": proc.name(), + "path": " ".join(proc.cmdline()[1:]), + "ports": ", ".join(map(_port_to_tag_a, ports)), + } + ) - processes.sort(key=lambda p: int(''.join(c for c in p['ports'] if c.isdigit()))) + processes.sort(key=lambda p: int("".join(c for c in p["ports"] if c.isdigit()))) return processes -def _port_to_tag_a(port: int) -> str: - return f'{port}' - - class HttpProcessor(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: o = urlsplit(self.path) # Only index - if o.path != '/': + if o.path != "/": self.send_error(404) return table_rows = [] for i, p in enumerate(_get_processes(), 1): - table_rows.append(f''' + table_rows.append( + f""" {i} {p['pid']} {p['ports']} {p['path']} - ''') + """ + ) - text = """ + text = ( + """ @@ -108,26 +116,28 @@ def do_GET(self): - """ \ - .replace('{{ title }}', TITLE) \ - .replace('{{ headers }}', ''.join(f'{x}' for x in HEADERS)) \ - .replace('{{ table_rows }}', ''.join(table_rows)) + """.replace( + "{{ title }}", TITLE + ) + .replace("{{ headers }}", "".join(f"{x}" for x in HEADERS)) + .replace("{{ table_rows }}", "".join(table_rows)) + ) self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Connection', 'close') + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Connection", "close") self.end_headers() - self.wfile.write(text.encode('utf-8')) + self.wfile.write(text.encode("utf-8")) -def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080): - print(f'HTTP server running on http://127.0.0.1:{port}') +def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080) -> None: + print(f"HTTP server running on http://127.0.0.1:{port}") - server_address = ('', port) + server_address = ("", port) httpd = server_class(server_address, handler_class) httpd.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": run(port=10012) diff --git a/psutil_example/print_process_memory/print_process_by_memory.py b/psutil_example/print_process_memory/print_process_by_memory.py index 8e26b8577..c859bf762 100644 --- a/psutil_example/print_process_memory/print_process_by_memory.py +++ b/psutil_example/print_process_memory/print_process_by_memory.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from collections import defaultdict @@ -9,16 +9,8 @@ # pip install psutil import psutil - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/9007628099adb5964fdbf827f14cc872ba35f8ad/human_byte_size.py -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') +# pip install humanize +from humanize import naturalsize as sizeof_fmt column_width = defaultdict(int) @@ -26,15 +18,15 @@ def sizeof_fmt(num): for p in psutil.process_iter(): memory = p.memory_info().rss - cols = p.name(), str(memory) + ' bytes', sizeof_fmt(memory) + cols = p.name(), str(memory) + " bytes", sizeof_fmt(memory) process_list.append(cols) for i, x in enumerate(cols): column_width[i] = max(column_width[i], len(x)) # Sort by memory size -process_list.sort(key=lambda x: int(x[1].split(' ')[0]), reverse=True) +process_list.sort(key=lambda x: int(x[1].split(" ")[0]), reverse=True) for p in process_list: row = [x.rjust(column_width[i]) for i, x in enumerate(p)] - print(' | '.join(row)) + print(" | ".join(row)) diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple.py b/psutil_example/print_process_memory/print_process_by_memory__simple.py index 188aface3..17c2ae6b8 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py index 06d8b8444..b4cefbbf1 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -10,7 +10,7 @@ for p in psutil.process_iter(): name = p.name() - if name.lower() != 'firefox.exe': + if name.lower() != "firefox.exe": continue print(name, p.memory_info().rss) diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py index 2503e9d16..cf47d0986 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -12,7 +12,7 @@ for p in psutil.process_iter(): name = p.name() - if name.lower() != 'firefox.exe': + if name.lower() != "firefox.exe": continue memory = p.memory_info().rss @@ -20,9 +20,11 @@ print(name, memory) -print(f''' +print( + f""" Total: {total} bytes {total // 1024} KB {total // 1024 // 1024} MB {total // 1024 // 1024 // 1024} GB -''') +""" +) diff --git a/psutil_example/process_management/analog_subprocess_Popen.py b/psutil_example/process_management/analog_subprocess_Popen.py index ffaa915de..8f8f6f77f 100644 --- a/psutil_example/process_management/analog_subprocess_Popen.py +++ b/psutil_example/process_management/analog_subprocess_Popen.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil from subprocess import PIPE -p = psutil.Popen(['python', "-c", "print('hello')\nprint('world!')"], stdout=PIPE) + +p = psutil.Popen(["python", "-c", "print('hello')\nprint('world!')"], stdout=PIPE) print(p.name()) print(p.username()) print(p.communicate()) diff --git a/psutil_example/process_management/current_process_detail_info.py b/psutil_example/process_management/current_process_detail_info.py index 55ff0101e..a7ac6bbb8 100644 --- a/psutil_example/process_management/current_process_detail_info.py +++ b/psutil_example/process_management/current_process_detail_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import current_process diff --git a/psutil_example/process_management/on_terminate__callback.py b/psutil_example/process_management/on_terminate__callback.py index 712109595..36cffe170 100644 --- a/psutil_example/process_management/on_terminate__callback.py +++ b/psutil_example/process_management/on_terminate__callback.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Run 2 process and wait terminate +import threading +import time -def func(time_life=4): - import time - i = 1 - import threading +def func(time_life=4) -> None: + i = 1 while i <= time_life: print(threading.current_thread(), i) @@ -20,8 +20,9 @@ def func(time_life=4): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process + p1 = Process(target=func, args=(5,)) p1.start() @@ -30,18 +31,21 @@ def func(time_life=4): # pip install psutil import psutil + process1 = psutil.Process(p1.pid) - print('process1 is_running:', process1.is_running()) + print("process1 is_running:", process1.is_running()) process2 = psutil.Process(p2.pid) - print('process2 is_running:', process2.is_running()) + print("process2 is_running:", process2.is_running()) print() - def on_terminate(proc): - print('Process "{}" terminated'.format(proc)) + def on_terminate(proc) -> None: + print(f'Process "{proc}" terminated') # waits for multiple processes to terminate - gone, alive = psutil.wait_procs([process1, process2], timeout=30, callback=on_terminate) + gone, alive = psutil.wait_procs( + [process1, process2], timeout=30, callback=on_terminate + ) print() - print('gone:', gone) - print('alive:', alive) + print("gone:", gone) + print("alive:", alive) diff --git a/psutil_example/process_management/process_control.py b/psutil_example/process_management/process_control.py index eea6d2052..66961ef30 100644 --- a/psutil_example/process_management/process_control.py +++ b/psutil_example/process_management/process_control.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def func(): - import time - import threading +import time +import threading + +def func() -> None: i = 1 while True: @@ -17,37 +18,39 @@ def func(): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process + p = Process(target=func) p.start() print(p.pid) # pip install psutil import psutil + process = psutil.Process(p.pid) - print('is_running:', process.is_running()) + print("is_running:", process.is_running()) - print('Info:') + print("Info:") from process_detail_info import print_info + print_info(p.pid) - print('\n') + print("\n") - import time time.sleep(4) - print('suspend on 3 seconds') + print("suspend on 3 seconds") process.suspend() time.sleep(3) - print('resume') + print("resume") process.resume() - print('terminate after 5 seconds') + print("terminate after 5 seconds") time.sleep(5) process.terminate() - print('terminate') + print("terminate") result_code = process.wait(timeout=3) - print('result_code:', result_code) + print("result_code:", result_code) diff --git a/psutil_example/process_management/process_detail_info.py b/psutil_example/process_management/process_detail_info.py index aeaf2f7c2..b2ff6649b 100644 --- a/psutil_example/process_management/process_detail_info.py +++ b/psutil_example/process_management/process_detail_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Need run as admin @@ -10,52 +10,52 @@ import psutil -def print_info(pid): +def print_info(pid) -> None: process = psutil.Process(pid) - print('Process:', process) - - print('name:', process.name()) - print('as_dict:', process.as_dict()) - print('is_running:', process.is_running()) - - print('exe:', process.exe()) - print('cwd:', process.cwd()) - print('cmdline:', process.cmdline()) - print('pid:', process.pid) - print('ppid:', process.ppid()) - print('parent:', process.parent()) - print('children:', process.children()) - print('status:', process.status()) - print('username:', process.username()) - print('create_time:', process.create_time()) + print("Process:", process) + + print("name:", process.name()) + print("as_dict:", process.as_dict()) + print("is_running:", process.is_running()) + + print("exe:", process.exe()) + print("cwd:", process.cwd()) + print("cmdline:", process.cmdline()) + print("pid:", process.pid) + print("ppid:", process.ppid()) + print("parent:", process.parent()) + print("children:", process.children()) + print("status:", process.status()) + print("username:", process.username()) + print("create_time:", process.create_time()) # If POSIX: # print('uids:', process.uids()) # print('gids:', process.gids()) - print('cpu_times:', process.cpu_times()) - print('cpu_percent:', process.cpu_percent(interval=1.0)) - print('cpu_affinity:', process.cpu_affinity()) - print('cpu_affinity:', process.cpu_affinity([0, 1])) + print("cpu_times:", process.cpu_times()) + print("cpu_percent:", process.cpu_percent(interval=1.0)) + print("cpu_affinity:", process.cpu_affinity()) + print("cpu_affinity:", process.cpu_affinity([0, 1])) # If Linux, FreeBSD, SunOS # print('cpu_num:', process.cpu_num()) - print('memory_info:', process.memory_info()) - print('memory_full_info:', process.memory_full_info()) - print('memory_percent:', process.memory_percent()) - print('memory_maps:', process.memory_maps()) - print('io_counters:', process.io_counters()) - print('open_files:', process.open_files()) - print('connections:', process.connections()) - print('num_threads:', process.num_threads()) + print("memory_info:", process.memory_info()) + print("memory_full_info:", process.memory_full_info()) + print("memory_percent:", process.memory_percent()) + print("memory_maps:", process.memory_maps()) + print("io_counters:", process.io_counters()) + print("open_files:", process.open_files()) + print("connections:", process.connections()) + print("num_threads:", process.num_threads()) # If POSIX # print('num_fds:', process.num_fds()) - print('threads:', process.threads()) - print('num_ctx_switches:', process.num_ctx_switches()) - print('nice/priority get:', process.nice()) + print("threads:", process.threads()) + print("num_ctx_switches:", process.num_ctx_switches()) + print("nice/priority get:", process.nice()) # Error: OSError: [WinError 87] The parameter is incorrect # print('nice/priority set:', process.nice(10)) @@ -64,23 +64,24 @@ def print_info(pid): # Error: AttributeError: module 'psutil' has no attribute 'IOPRIO_CLASS_IDLE' # print('ionice set:', process.ionice(psutil.IOPRIO_CLASS_IDLE)) - print('ionice get:', process.ionice()) + print("ionice get:", process.ionice()) # If Linux # print('rlimit set:', process.rlimit(psutil.RLIMIT_NOFILE, (5, 5))) # print('rlimit get:', process.rlimit(psutil.RLIMIT_NOFILE)) # If Linux, OSX and Windows - print('environ:', process.environ()) + print("environ:", process.environ()) -if __name__ == '__main__': - print('Random process info') +if __name__ == "__main__": + print("Random process info") process_pid_list = psutil.pids() import random + pid = random.choice(process_pid_list) - print('Pid:', pid) + print("Pid:", pid) print_info(pid) diff --git a/psutil_example/process_management/process_list_info.py b/psutil_example/process_management/process_list_info.py index 1b8d0a78e..22b8e3162 100644 --- a/psutil_example/process_management/process_list_info.py +++ b/psutil_example/process_management/process_list_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Получение списка запущенных процессов.""" @@ -13,7 +13,7 @@ for proc in psutil.process_iter(): try: - pinfo = proc.as_dict(attrs=['pid', 'name']) + pinfo = proc.as_dict(attrs=["pid", "name"]) except psutil.NoSuchProcess: continue diff --git a/psutil_example/process_management/process_pid_list.py b/psutil_example/process_management/process_pid_list.py index 13d59670e..fe5b7caf1 100644 --- a/psutil_example/process_management/process_pid_list.py +++ b/psutil_example/process_management/process_pid_list.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil process_pid_list = psutil.pids() -print('Process pid list ({}): {}'.format(len(process_pid_list), process_pid_list)) +print(f"Process pid list ({len(process_pid_list)}): {process_pid_list}") diff --git a/psutil_example/process_management/random_process_info.py b/psutil_example/process_management/random_process_info.py index c5224eddc..1c4a997dc 100644 --- a/psutil_example/process_management/random_process_info.py +++ b/psutil_example/process_management/random_process_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -9,11 +9,11 @@ # pip install psutil import psutil -print('Random process info') +print("Random process info") process_pid_list = psutil.pids() pid = random.choice(process_pid_list) -print('Pid:', pid) +print("Pid:", pid) process = psutil.Process(pid) -print('Process:', process) +print("Process:", process) diff --git a/psutil_example/python_processes_started_more_than_an_hour_ago.py b/psutil_example/python_processes_started_more_than_an_hour_ago.py index ef38f2262..e8be19fe4 100644 --- a/psutil_example/python_processes_started_more_than_an_hour_ago.py +++ b/psutil_example/python_processes_started_more_than_an_hour_ago.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install psutil import psutil for proc in psutil.process_iter(): - if not proc.name().startswith('python'): + if not proc.name().startswith("python"): continue secs = proc.create_time() - started = DT.datetime.fromtimestamp(secs) - if (DT.datetime.now() - started) >= DT.timedelta(hours=1): + started = dt.datetime.fromtimestamp(secs) + if (dt.datetime.now() - started) >= dt.timedelta(hours=1): print(proc) diff --git a/psutil_example/sensors_info.py b/psutil_example/sensors_info.py index d5cce15e7..91fd5e487 100644 --- a/psutil_example/sensors_info.py +++ b/psutil_example/sensors_info.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Only Linux # pip install psutil import psutil + + print(psutil.sensors_temperatures()) print(psutil.sensors_fans()) print(psutil.sensors_battery()) diff --git a/psutil_example/windows_services.py b/psutil_example/windows_services.py index b824bf800..2d45453a1 100644 --- a/psutil_example/windows_services.py +++ b/psutil_example/windows_services.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +# pip install tabulate +from tabulate import tabulate + # pip install psutil import psutil + win_service_list = list(psutil.win_service_iter()) -print('Win service list ({}):'.format(len(win_service_list))) +print(f"Win service list ({len(win_service_list)}):") win_service_list.sort(key=lambda x: x.status()) @@ -18,7 +22,16 @@ # # OR: # # if win_service_list: - headers = ['name', 'display_name', 'status', 'start_type', 'username', 'pid', 'binpath', 'description'] + headers = [ + "name", + "display_name", + "status", + "start_type", + "username", + "pid", + "binpath", + "description", + ] rows = [] for win_service in win_service_list: @@ -27,15 +40,14 @@ rows.append(row) headers = [header.upper() for header in headers] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid", showindex=True)) print() print() + name = win_service_list[0].name() -print('Windows service info: {}'.format(name)) +print(f"Windows service info: {name}") + win_service = psutil.win_service_get(name) -print(' win_service:', win_service) -print(' win_service info:', win_service.as_dict()) +print(" win_service:", win_service) +print(" win_service info:", win_service.as_dict()) diff --git a/public_key_cryptography.py b/public_key_cryptography.py index f39a28fe0..07aa24b82 100644 --- a/public_key_cryptography.py +++ b/public_key_cryptography.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://ru.wikipedia.org/wiki/Криптосистема_с_открытым_ключом @@ -9,19 +9,19 @@ # Разбор примера шифрования с помощью справочника: https://ru.wikipedia.org/wiki/Криптосистема_с_открытым_ключом REFERENCE_GUIDE_NAME_NUM = { - 'Королёв': '5643452', - 'Орехов': '3572651', - 'Рузаева': '4673956', - 'Осипов': '3517289', - 'Батурин': '7755628', - 'Кирсанова': '1235267', - 'Арсеньева': '8492746', + "Королёв": "5643452", + "Орехов": "3572651", + "Рузаева": "4673956", + "Осипов": "3517289", + "Батурин": "7755628", + "Кирсанова": "1235267", + "Арсеньева": "8492746", } # Обратный словарь -- ключом будет число, а значением имя REFERENCE_GUIDE_NUM_NAME = {v: k for k, v in REFERENCE_GUIDE_NAME_NUM.items()} -MESS = 'коробка' +MESS = "коробка" def encrypt(mess): @@ -32,19 +32,20 @@ def encrypt(mess): encrypt_key = sorted(filter(lambda x: x[0].lower() == c, keys))[0] crypto_text_list.append(REFERENCE_GUIDE_NAME_NUM[encrypt_key]) - return '@'.join(crypto_text_list) + return "@".join(crypto_text_list) def decrypt(encrypt_mess): - crypto_num_list = encrypt_mess.split('@') - mess = '' + crypto_num_list = encrypt_mess.split("@") + mess = "" for num in crypto_num_list: mess += REFERENCE_GUIDE_NUM_NAME[num][0].lower() return mess + encrypt_mess = encrypt(MESS) -print('Encrypt: {} -> {}.'.format(MESS, encrypt_mess)) -print('Decrypt: {} -> {}'.format(encrypt_mess, decrypt(encrypt_mess))) +print(f"Encrypt: {MESS} -> {encrypt_mess}.") +print(f"Decrypt: {encrypt_mess} -> {decrypt(encrypt_mess)}") diff --git a/pyDes__examples/hello_world.py b/pyDes__examples/hello_world.py index f5c675a27..6d7f42db7 100644 --- a/pyDes__examples/hello_world.py +++ b/pyDes__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/toddw-as/pyDes @@ -10,11 +10,13 @@ # pip install pyDes import pyDes -data = b"Hello World!" +data = b"Hello World!" # def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): -k = pyDes.des(b"DESCRYPT", pyDes.CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=pyDes.PAD_PKCS5) +k = pyDes.des( + b"DESCRYPT", pyDes.CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=pyDes.PAD_PKCS5 +) d = k.encrypt(data) print("Encrypted: %r" % d) diff --git a/pyautogui__keyboard__examples/10_second_macro.py b/pyautogui__keyboard__examples/10_second_macro.py index c8b0e1278..031ab1fcd 100644 --- a/pyautogui__keyboard__examples/10_second_macro.py +++ b/pyautogui__keyboard__examples/10_second_macro.py @@ -5,9 +5,11 @@ # SOURCE: https://github.com/boppreh/keyboard/blob/master/examples/10_second_macro.py +import time + # pip install keyboard import keyboard -import time + keyboard.start_recording() time.sleep(10) diff --git a/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py b/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py index abc1b74ce..f595ff49f 100644 --- a/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py +++ b/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py @@ -1,34 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Frozen Throne, 2560x1440 import time + # import winsound # pip install pyautogui import pyautogui -LOCAL_NETWORK_BUTTON = 'images/local_network_button.png' -NEW_GAME_BUTTON = 'images/new_game_button.png' -NEW_GAME_BUTTON_SELECT_MAP = 'images/new_game_button_select_map.png' +LOCAL_NETWORK_BUTTON = "images/local_network_button.png" +NEW_GAME_BUTTON = "images/new_game_button.png" +NEW_GAME_BUTTON_SELECT_MAP = "images/new_game_button_select_map.png" -OPEN_COMBO_BOX_SENTINEL = 'images/open_combo_box_sentinel.png' -OPEN_COMBO_BOX_SCOURGE = 'images/open_combo_box_scourge.png' +OPEN_COMBO_BOX_SENTINEL = "images/open_combo_box_sentinel.png" +OPEN_COMBO_BOX_SCOURGE = "images/open_combo_box_scourge.png" SELECT_AI_MENU_SENTINEL = "images/select_ai_menu_sentinel.png" SELECT_AI_MENU_SCOURGE = "images/select_ai_menu_scourge.png" AI_EASY = "images/ai_easy.png" -def go_local_network(): +def go_local_network() -> bool: pos = pyautogui.locateCenterOnScreen(LOCAL_NETWORK_BUTTON) - print('LOCAL_NETWORK_BUTTON:', pos) + print("LOCAL_NETWORK_BUTTON:", pos) if pos: pyautogui.click(pos) @@ -40,9 +41,9 @@ def go_local_network(): return False -def go_new_game(): +def go_new_game() -> bool: pos = pyautogui.locateCenterOnScreen(NEW_GAME_BUTTON) - print('NEW_GAME_BUTTON:', pos) + print("NEW_GAME_BUTTON:", pos) if pos: pyautogui.click(pos) @@ -54,9 +55,9 @@ def go_new_game(): return False -def go_select_map(): +def go_select_map() -> bool: pos = pyautogui.locateCenterOnScreen(NEW_GAME_BUTTON_SELECT_MAP) - print('NEW_GAME_BUTTON_SELECT_MAP:', pos) + print("NEW_GAME_BUTTON_SELECT_MAP:", pos) if pos: pyautogui.click(pos) @@ -68,7 +69,7 @@ def go_select_map(): return False -def bot_says(): +def bot_says() -> None: text = """\ Bot say: -aremnpakulsc @@ -77,10 +78,10 @@ def bot_says(): for line in text.splitlines(): pyautogui.typewrite(line) - pyautogui.typewrite(['enter']) + pyautogui.typewrite(["enter"]) -def select_sentinel_and_scourge(coords_sentinel, coords_scourge): +def select_sentinel_and_scourge(coords_sentinel, coords_scourge) -> None: for rect in coords_sentinel + coords_scourge: pos = pyautogui.center(rect) @@ -93,14 +94,16 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): # Ждем появления меню pos_menu_sentinel = pyautogui.locateCenterOnScreen(SELECT_AI_MENU_SENTINEL) pos_menu_scourge = pyautogui.locateCenterOnScreen(SELECT_AI_MENU_SCOURGE) - print(f'SELECT_AI_MENU_SENTINEL: {pos_menu_sentinel}, SELECT_AI_MENU_SCOURGE: {pos_menu_scourge}') + print( + f"SELECT_AI_MENU_SENTINEL: {pos_menu_sentinel}, SELECT_AI_MENU_SCOURGE: {pos_menu_scourge}" + ) if pos_menu_sentinel or pos_menu_scourge: break # Выбираем игрока pos = pyautogui.locateCenterOnScreen(AI_EASY) - print('AI_EASY:', pos) + print("AI_EASY:", pos) if pos: pyautogui.moveTo(pos) or time.sleep(0.3) pyautogui.click(pos) @@ -108,7 +111,7 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): time.sleep(0.3) -if __name__ == '__main__': +if __name__ == "__main__": while True: # Кликаем на Локальную игру while not go_local_network(): @@ -133,7 +136,9 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): while True: coords_sentinel = list(pyautogui.locateAllOnScreen(OPEN_COMBO_BOX_SENTINEL)) coords_scourge = list(pyautogui.locateAllOnScreen(OPEN_COMBO_BOX_SCOURGE)) - print(f'coords_sentinel: {coords_sentinel}, coords_scourge: {coords_scourge}') + print( + f"coords_sentinel: {coords_sentinel}, coords_scourge: {coords_scourge}" + ) if coords_sentinel and coords_scourge: # winsound.Beep(1000, duration=10) diff --git a/pyautogui__keyboard__examples/auto_input_text__repo_example.py b/pyautogui__keyboard__examples/auto_input_text__repo_example.py index 97263406c..5beef36cf 100644 --- a/pyautogui__keyboard__examples/auto_input_text__repo_example.py +++ b/pyautogui__keyboard__examples/auto_input_text__repo_example.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.press_and_release('tab, tab, tab') -keyboard.write('The quick brown fox jumps over the lazy dog.') + +keyboard.press_and_release("tab, tab, tab") + +keyboard.write("The quick brown fox jumps over the lazy dog.") # Press PAGE UP then PAGE DOWN to type "foobar". -keyboard.add_hotkey('page up, page down', lambda: keyboard.write('foobar')) +keyboard.add_hotkey("page up, page down", lambda: keyboard.write("foobar")) # Blocks until you press esc. -keyboard.wait('esc') +keyboard.wait("esc") # Record events until 'esc' is pressed. -recorded = keyboard.record(until='esc') +recorded = keyboard.record(until="esc") # Then replay back at three times the speed. keyboard.play(recorded, speed_factor=3) # Type @@ then press space to replace with abbreviation. -keyboard.add_abbreviation('@@', 'my.long.email@example.com') +keyboard.add_abbreviation("@@", "my.long.email@example.com") # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/auto_replace_input_text.py b/pyautogui__keyboard__examples/auto_replace_input_text.py index 516b0f4ff..f2aeeaa91 100644 --- a/pyautogui__keyboard__examples/auto_replace_input_text.py +++ b/pyautogui__keyboard__examples/auto_replace_input_text.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard + # Type fuck then press space to replace with abbreviation. -keyboard.add_abbreviation('fuck', 'Furnification under consult of King') +keyboard.add_abbreviation("fuck", "Furnification under consult of King") # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py b/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py new file mode 100644 index 000000000..b618b0b83 --- /dev/null +++ b/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import os +import winsound + +from threading import Thread + +# pip install PyAutoIt==0.6.5 +import autoit + +# pip install keyboard==0.13.5 +import keyboard + + +DATA = { + "START": False, +} + +HOTKEY: str = os.environ.get("HOTKEY", "Ctrl+Alt+Space") + + +def change_start() -> None: + DATA["START"] = not DATA["START"] + print("START:", DATA["START"]) + + winsound.Beep(1000, duration=50) + + +def process_auto_click() -> None: + while True: + if not DATA["START"]: + time.sleep(0.100) + continue + + # Симуляция атаки + if DATA["START"]: + autoit.mouse_click() + + time.sleep(0.070) + + +keyboard.add_hotkey(HOTKEY, change_start) + +# Запуск потока для автоатаки +thread_auto_click = Thread(target=process_auto_click) +thread_auto_click.start() + + +print(f"Press {HOTKEY} for starting/stopping") + +while True: + if not DATA["START"]: + time.sleep(0.100) + continue diff --git a/pyautogui__keyboard__examples/customizable_hotkey.py b/pyautogui__keyboard__examples/customizable_hotkey.py index be71e8953..28da7ef28 100644 --- a/pyautogui__keyboard__examples/customizable_hotkey.py +++ b/pyautogui__keyboard__examples/customizable_hotkey.py @@ -8,16 +8,18 @@ # pip install keyboard import keyboard -print('Press and release your desired shortcut: ') + +print("Press and release your desired shortcut: ") shortcut = keyboard.read_shortcut() -print('Shortcut selected:', shortcut) +print("Shortcut selected:", shortcut) -def on_triggered(): +def on_triggered() -> None: print("Triggered!") + keyboard.add_hotkey(shortcut, on_triggered) print("Press ESC to stop.") -keyboard.wait('esc') +keyboard.wait("esc") diff --git a/pyautogui__keyboard__examples/hotkey.py b/pyautogui__keyboard__examples/hotkey.py index d1cdbc4a1..71d22ad30 100644 --- a/pyautogui__keyboard__examples/hotkey.py +++ b/pyautogui__keyboard__examples/hotkey.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('shift', lambda: keyboard.write('on')) -keyboard.add_hotkey('shift', lambda: keyboard.write('off'), trigger_on_release=True) + + +keyboard.add_hotkey("shift", lambda: keyboard.write("on")) +keyboard.add_hotkey("shift", lambda: keyboard.write("off"), trigger_on_release=True) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/hotkey__hello_world.py b/pyautogui__keyboard__examples/hotkey__hello_world.py index 0f43941fe..dd6599df4 100644 --- a/pyautogui__keyboard__examples/hotkey__hello_world.py +++ b/pyautogui__keyboard__examples/hotkey__hello_world.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -def foo(): - print('World') +def foo() -> None: + print("World") -keyboard.add_hotkey('Ctrl + 1', lambda: print('Hello')) -keyboard.add_hotkey('Ctrl + 2', foo) +keyboard.add_hotkey("Ctrl + 1", lambda: print("Hello")) +keyboard.add_hotkey("Ctrl + 2", foo) -keyboard.wait('Ctrl + Q') +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py b/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py index fe6a508e4..210c99773 100644 --- a/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py +++ b/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Win + PageDown', lambda: keyboard.send('Alt + Space + N')) + + +keyboard.add_hotkey("Win + PageDown", lambda: keyboard.send("Alt + Space + N")) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/hotkey__mouse___down_up.py b/pyautogui__keyboard__examples/hotkey__mouse___down_up.py index 2e65922eb..8ca58e968 100644 --- a/pyautogui__keyboard__examples/hotkey__mouse___down_up.py +++ b/pyautogui__keyboard__examples/hotkey__mouse___down_up.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -import pyautogui +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Ctrl + 1', pyautogui.mouseDown) -keyboard.add_hotkey('Ctrl + 2', pyautogui.mouseUp) -keyboard.wait('Ctrl + Q') +import pyautogui + + +keyboard.add_hotkey("Ctrl + 1", pyautogui.mouseDown) +keyboard.add_hotkey("Ctrl + 2", pyautogui.mouseUp) +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py b/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py index 27e3741ef..90f540165 100644 --- a/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py +++ b/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py @@ -1,15 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import pyautogui import winsound -# pip install keyboard +# pip install keyboard==0.13.5 import keyboard -keyboard.add_hotkey('Ctrl + 1', lambda: pyautogui.mouseDown() or winsound.Beep(1000, duration=50)) -keyboard.add_hotkey('Ctrl + 2', lambda: pyautogui.mouseUp() or winsound.Beep(1000, duration=50)) -keyboard.wait('Ctrl + Q') +# pip install pyautogui==0.9.54 +import pyautogui + + +def beep() -> None: + winsound.Beep(1000, duration=50) + + +keyboard.add_hotkey( + "Ctrl + 1", lambda: pyautogui.mouseDown() or beep() +) +keyboard.add_hotkey( + "Ctrl + 2", lambda: pyautogui.mouseUp() or beep() +) + +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__start_pause_quit.py b/pyautogui__keyboard__examples/hotkey__start_pause_quit.py index e4ee1d20a..a326f9091 100644 --- a/pyautogui__keyboard__examples/hotkey__start_pause_quit.py +++ b/pyautogui__keyboard__examples/hotkey__start_pause_quit.py @@ -1,51 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+Q' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +import time +import os + +import keyboard + + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+Q" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'START': False, - 'AUTO_ATTACK': False, + "START": False, + "AUTO_ATTACK": False, } -def change_start(): - BOT_DATA['START'] = not BOT_DATA['START'] - print('START:', BOT_DATA['START']) +def change_start() -> None: + BOT_DATA["START"] = not BOT_DATA["START"] + print("START:", BOT_DATA["START"]) -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN / PAUSE'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN / PAUSE') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.add_hotkey(RUN_COMBINATION, change_start) -print('Start') +print("Start") i = 1 while True: - if not BOT_DATA['START']: + if not BOT_DATA["START"]: time.sleep(0.1) continue - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 diff --git a/pyautogui__keyboard__examples/hotkey_change_state.py b/pyautogui__keyboard__examples/hotkey_change_state.py index 519cb8414..de9ea7729 100644 --- a/pyautogui__keyboard__examples/hotkey_change_state.py +++ b/pyautogui__keyboard__examples/hotkey_change_state.py @@ -1,41 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+T' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +import time +import os + +import keyboard + + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+T" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'AUTO_ATTACK': False, + "AUTO_ATTACK": False, } -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.wait(RUN_COMBINATION) -print('Start') +print("Start") i = 1 while True: - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 diff --git a/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py b/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py index cfb2de25d..bf4264347 100644 --- a/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py +++ b/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py @@ -1,50 +1,52 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time import threading +import os + +import keyboard -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+T' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+T" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'AUTO_ATTACK': False, + "AUTO_ATTACK": False, } -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.wait(RUN_COMBINATION) -print('Start') +print("Start") -def process_auto_attack(): +def process_auto_attack() -> None: i = 1 while True: - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 + thread_auto_attack = threading.Thread(target=process_auto_attack) thread_auto_attack.start() diff --git a/pyautogui__keyboard__examples/input__hello_world.py b/pyautogui__keyboard__examples/input__hello_world.py index 1b9caa5fa..b6595328a 100644 --- a/pyautogui__keyboard__examples/input__hello_world.py +++ b/pyautogui__keyboard__examples/input__hello_world.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.write('Hello') -keyboard.write(' world!', delay=0.2) + + +keyboard.write("Hello") +keyboard.write(" world!", delay=0.2) diff --git a/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py b/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py index e0a29d584..8040eb6fa 100644 --- a/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py +++ b/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py @@ -1,12 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Shift + Home', lambda: keyboard.write('Hello') or keyboard.write(' world!', delay=0.3)) + + +keyboard.add_hotkey( + "Shift + Home", + lambda: keyboard.write("Hello") or keyboard.write(" world!", delay=0.3), +) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py b/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py index e7fec5fcc..640268450 100644 --- a/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py +++ b/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://pyautogui.readthedocs.io/en/latest/keyboard.html#the-press-keydown-and-keyup-functions @@ -11,18 +11,18 @@ import pyautogui -TEXT = 'Hello World!' +TEXT = "Hello World!" # Input text pyautogui.typewrite(TEXT) # Select text -pyautogui.keyDown('shift') # Hold down the shift key +pyautogui.keyDown("shift") # Hold down the shift key for _ in TEXT: - pyautogui.press('left') # Press the left arrow key + pyautogui.press("left") # Press the left arrow key -pyautogui.keyUp('shift') # Release the shift key +pyautogui.keyUp("shift") # Release the shift key -pyautogui.press('delete') +pyautogui.press("delete") diff --git a/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py b/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py index 2afb4fc23..04d7784a4 100644 --- a/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py +++ b/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/boppreh/keyboard#keyboard.send @@ -9,4 +9,5 @@ # pip install keyboard import keyboard + keyboard.send("Ctrl+V") diff --git a/pyautogui__keyboard__examples/keyboard_hook.py b/pyautogui__keyboard__examples/keyboard_hook.py index 9f91aefb4..2bc6174c0 100644 --- a/pyautogui__keyboard__examples/keyboard_hook.py +++ b/pyautogui__keyboard__examples/keyboard_hook.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -def print_pressed_keys(e): +def print_pressed_keys(e) -> None: print(e, e.event_type, e.name) diff --git a/pyautogui__keyboard__examples/locate_center_on_screen.py b/pyautogui__keyboard__examples/locate_center_on_screen.py index da80a4f9e..0719d62fe 100644 --- a/pyautogui__keyboard__examples/locate_center_on_screen.py +++ b/pyautogui__keyboard__examples/locate_center_on_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui @@ -16,8 +16,8 @@ def locate_center_on_screen(needle_image, screenshot_image=None): return pyautogui.locateCenterOnScreen(needle_image) -if __name__ == '__main__': - needle_image = '' +if __name__ == "__main__": + needle_image = "" import time diff --git a/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py b/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py index 0c4a3b5e9..f3bac70f6 100644 --- a/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py +++ b/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -QUIT_COMBINATION = 'Esc' -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - +import time import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -import time +import keyboard import pyautogui + +QUIT_COMBINATION = "Esc" +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) + time.sleep(5) i = 0 while True: - pyautogui.press('pgdn') + pyautogui.press("pgdn") i += 1 print(i) diff --git a/pyautogui__keyboard__examples/message_box/alert.py b/pyautogui__keyboard__examples/message_box/alert.py index d9ace4589..6e9f1cc64 100644 --- a/pyautogui__keyboard__examples/message_box/alert.py +++ b/pyautogui__keyboard__examples/message_box/alert.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import alert -button = alert(text='My Text', title='My Title', button='OK') + + +button = alert(text="My Text", title="My Title", button="OK") print(button) diff --git a/pyautogui__keyboard__examples/message_box/confirm.py b/pyautogui__keyboard__examples/message_box/confirm.py index 8f3f88442..1c2c8c9f8 100644 --- a/pyautogui__keyboard__examples/message_box/confirm.py +++ b/pyautogui__keyboard__examples/message_box/confirm.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import confirm -button = confirm(text='My Text', title='My Title', buttons=['OK', 'Cancel']) + + +button = confirm(text="My Text", title="My Title", buttons=["OK", "Cancel"]) print(button) diff --git a/pyautogui__keyboard__examples/message_box/password.py b/pyautogui__keyboard__examples/message_box/password.py index 52cfdcaa8..e8794e0d0 100644 --- a/pyautogui__keyboard__examples/message_box/password.py +++ b/pyautogui__keyboard__examples/message_box/password.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import password -result = password(text='My Text', title='My Title', default='Default Text', mask='*') + + +result = password(text="My Text", title="My Title", default="Default Text", mask="*") print(result) -result = password(text='My Text', title='My Title', default='Default Text', mask='?') +result = password(text="My Text", title="My Title", default="Default Text", mask="?") print(result) diff --git a/pyautogui__keyboard__examples/message_box/prompt.py b/pyautogui__keyboard__examples/message_box/prompt.py index cfa67c035..026545a88 100644 --- a/pyautogui__keyboard__examples/message_box/prompt.py +++ b/pyautogui__keyboard__examples/message_box/prompt.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import prompt -result = prompt(text='My Text', title='My Title', default='Default Text') + + +result = prompt(text="My Text", title="My Title", default="Default Text") print(result) diff --git a/pyautogui__keyboard__examples/move_mouse_on_center.py b/pyautogui__keyboard__examples/move_mouse_on_center.py index 4e17f3bf7..fa0f598e1 100644 --- a/pyautogui__keyboard__examples/move_mouse_on_center.py +++ b/pyautogui__keyboard__examples/move_mouse_on_center.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui + + screenWidth, screenHeight = pyautogui.size() pyautogui.moveTo(screenWidth / 2, screenHeight / 2) diff --git a/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py b/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py index 88d30c557..913f9b723 100644 --- a/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py +++ b/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import random import pyautogui -# left up down right -directions = ['left', 'up', 'down', 'right'] -import random +# left up down right +directions = ["left", "up", "down", "right"] while True: direction = random.choice(directions) diff --git a/pyautogui__keyboard__examples/pressed_keys.py b/pyautogui__keyboard__examples/pressed_keys.py index b2488c6a2..520de3dcb 100644 --- a/pyautogui__keyboard__examples/pressed_keys.py +++ b/pyautogui__keyboard__examples/pressed_keys.py @@ -14,11 +14,11 @@ import keyboard -def print_pressed_keys(e): - line = ', '.join(str(code) for code in keyboard._pressed_events) +def print_pressed_keys(e) -> None: + line = ", ".join(str(code) for code in keyboard._pressed_events) # '\r' and end='' overwrites the previous line. # ' ' * 40 prints 40 spaces at the end to ensure the previous line is cleared. - print('\r' + line + ' ' * 40, end='') + print("\r" + line + " " * 40, end="") keyboard.hook(print_pressed_keys) diff --git a/pyautogui__keyboard__examples/pyautogui__ctrl+v.py b/pyautogui__keyboard__examples/pyautogui__ctrl+v.py index ef1ce5b92..42aae0e97 100644 --- a/pyautogui__keyboard__examples/pyautogui__ctrl+v.py +++ b/pyautogui__keyboard__examples/pyautogui__ctrl+v.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://pyautogui.readthedocs.io/en/latest/cheatsheet.html#keyboard-functions @@ -10,10 +10,11 @@ # pip install pyautogui import pyautogui + # Variant 1 pyautogui.hotkey("Ctrl", "V") # Variant 2 -pyautogui.keyDown('Ctrl') -pyautogui.press('V') -pyautogui.keyUp('Ctrl') +pyautogui.keyDown("Ctrl") +pyautogui.press("V") +pyautogui.keyUp("Ctrl") diff --git a/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py b/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py index 6e43a4f64..aa4710550 100644 --- a/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py +++ b/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pyautogui import pyautogui + # Prints out "Hello world!" instantly -pyautogui.typewrite('Hello world!') +pyautogui.typewrite("Hello world!") -pyautogui.press('enter') +pyautogui.press("enter") # Prints out "Hello world!" with a quarter second delay after each character -pyautogui.typewrite('Hello world!', interval=0.25) +pyautogui.typewrite("Hello world!", interval=0.25) diff --git a/pyautogui__keyboard__examples/quit_by_global_press_Escape.py b/pyautogui__keyboard__examples/quit_by_global_press_Escape.py index 88f79e9b7..ecdad9882 100644 --- a/pyautogui__keyboard__examples/quit_by_global_press_Escape.py +++ b/pyautogui__keyboard__examples/quit_by_global_press_Escape.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -QUIT_COMBINATION = 'Esc' -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - import os +import time + import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -import time + +QUIT_COMBINATION = "Esc" +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) i = 0 while True: diff --git a/pyautogui__keyboard__examples/screenshot.py b/pyautogui__keyboard__examples/screenshot.py index 509ca7860..42af1e3dc 100644 --- a/pyautogui__keyboard__examples/screenshot.py +++ b/pyautogui__keyboard__examples/screenshot.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui -im1 = pyautogui.screenshot() -print(im1) -im2 = pyautogui.screenshot('my_screenshot.png') # save file -print(im2) + +img1 = pyautogui.screenshot() +print(img1) + +img2 = pyautogui.screenshot("my_screenshot.png") # save file +print(img2) diff --git a/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py b/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py index 64d91d121..adacc32c8 100644 --- a/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py +++ b/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# from PIL import Image - -import pyautogui import cv2 import numpy +import pyautogui + pil_image = pyautogui.screenshot(region=(200, 200, 200, 200)) print(pil_image.size) -pil_image.show('pil_image') +pil_image.show("pil_image") opencv_image = cv2.cvtColor(numpy.array(pil_image), cv2.COLOR_RGB2BGR) print(opencv_image.shape[:2]) -cv2.imshow('opencv_image', opencv_image) +cv2.imshow("opencv_image", opencv_image) cv2.waitKey() diff --git a/pyautogui__keyboard__examples/screenshots_by_cycle.py b/pyautogui__keyboard__examples/screenshots_by_cycle.py index 72fb19191..81d6a4ce0 100644 --- a/pyautogui__keyboard__examples/screenshots_by_cycle.py +++ b/pyautogui__keyboard__examples/screenshots_by_cycle.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import datetime as dt import time -from datetime import datetime + +from pathlib import Path + import pyautogui -DIR = 'screenshots' -import os -if not os.path.exists(DIR): - os.mkdir(DIR) +DIR = Path(__file__).resolve().parent + +DIR_SCREENSHOTS = DIR / "screenshots" +DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) + while True: - file_name = DIR + '/screenshot_{}.png'.format(datetime.now().strftime('%d%m%y %H%M%S.%f')) + file_name = str(DIR_SCREENSHOTS / f"{dt.datetime.now():%Y-%m-%d_%H%M%S.%f}.png") print(file_name) - im = pyautogui.screenshot(file_name) # save file - # print(im) + pyautogui.screenshot(file_name) # Save file time.sleep(0.5) diff --git a/pyautogui__keyboard__examples/telegram_smile_clicker/main.py b/pyautogui__keyboard__examples/telegram_smile_clicker/main.py index 04b131836..7a221651e 100644 --- a/pyautogui__keyboard__examples/telegram_smile_clicker/main.py +++ b/pyautogui__keyboard__examples/telegram_smile_clicker/main.py @@ -1,35 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(): - OPEN_SMILE_MENU = 'elements/open_smile_menu.png' - GROUP_SMILE = 'elements/group_smile.png' - CLICK_SMILE = 'elements/click_smile.png' - SEND = 'elements/send.png' +import logging +import sys +import time + +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui + + +def go() -> None: + OPEN_SMILE_MENU = "elements/open_smile_menu.png" + GROUP_SMILE = "elements/group_smile.png" + CLICK_SMILE = "elements/click_smile.png" + SEND = "elements/send.png" def get_logger(): - import logging - log = logging.getLogger('telegram_smile_clicker') + log = logging.getLogger("telegram_smile_clicker") log.setLevel(logging.DEBUG) - import sys sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(logging.Formatter('[%(asctime)s] %(message)s')) + sh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s")) log.addHandler(sh) return log log = get_logger() - log_it = lambda pos, filename: log.debug('{} [{}]'.format(pos, filename)) - - # OpenCv -- for performance - # pip install opencv-python - - # pip install pyautogui - import pyautogui + log_it = lambda pos, filename: log.debug(f"{pos} [{filename}]") # Ищем меню с смайлами pos = pyautogui.locateCenterOnScreen(OPEN_SMILE_MENU) @@ -41,7 +44,6 @@ def get_logger(): pyautogui.moveTo(pos) # Ожидаем - import time time.sleep(1) # Ищем наш смайл в меню @@ -80,7 +82,7 @@ def get_logger(): pyautogui.click(pos) -if __name__ == '__main__': +if __name__ == "__main__": # import profile # profile.run('go()') diff --git a/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py b/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py index db1e27933..daaa48ec3 100644 --- a/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py +++ b/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py @@ -1,30 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Esc' - -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -keyboard.wait(RUN_COMBINATION) +import keyboard import pyautogui + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Esc" + +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) +keyboard.wait(RUN_COMBINATION) + indent = 2 duration = 0.1 distance = 300 while distance > 0: - pyautogui.dragRel(distance, 0, duration) # move right + pyautogui.dragRel(distance, 0, duration) # move right distance -= indent - pyautogui.dragRel(0, distance, duration) # move down + pyautogui.dragRel(0, distance, duration) # move down pyautogui.dragRel(-distance, 0, duration) # move left distance -= indent pyautogui.dragRel(0, -distance, duration) # move up diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main.py b/pyautogui__keyboard__examples/win_calc_clicker/main.py index 80bd5ac7a..812b850f0 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main.py @@ -1,32 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import time + +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui BUTTONS = { - '+': 'buttons/add.png', - '-': 'buttons/sub.png', - '/': 'buttons/div.png', - '*': 'buttons/mul.png', - '=': 'buttons/equal.png', + "+": "buttons/add.png", + "-": "buttons/sub.png", + "/": "buttons/div.png", + "*": "buttons/mul.png", + "=": "buttons/equal.png", } for i in range(10): - BUTTONS[str(i)] = 'buttons/{}.png'.format(i) + BUTTONS[str(i)] = f"buttons/{i}.png" CACHE_POS_BUTTON = dict() -def go(expression): - # OpenCv -- for performance - # pip install opencv-python - - # pip install pyautogui - import pyautogui - +def go(expression) -> None: for x in expression: if x not in BUTTONS: - print('Not found: "{}"'.format(x)) + print(f'Not found: "{x}"') continue if x in CACHE_POS_BUTTON: @@ -43,16 +47,13 @@ def go(expression): pyautogui.click(pos) -def show_test_calc(): - import os - os.startfile('calc.exe') - - import time +def show_test_calc() -> None: + os.startfile("calc.exe") time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": show_test_calc() - expression = '1234 * 222 + 3214 = ' + expression = "1234 * 222 + 3214 = " go(expression) diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py b/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py index ccc4ef5a7..407699b56 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from main import go, show_test_calc + + show_test_calc() -expression = '3,14159265358979323846264338327950288419716939937510582097494459230781640628620' \ - '8998628034825342117067982148086513282306647093844609550582231725359408128481117' +expression = ( + "3,14159265358979323846264338327950288419716939937510582097494459230781640628620" + "8998628034825342117067982148086513282306647093844609550582231725359408128481117" +) go(expression) diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py b/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py index 3bd1b841e..9694f9048 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py @@ -1,43 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui + from main import show_test_calc + + show_test_calc() BUTTONS = { - '+': 'buttons/add.png', - '-': 'buttons/sub.png', - '/': 'buttons/div.png', - '*': 'buttons/mul.png', - '=': 'buttons/equal.png', + "+": "buttons/add.png", + "-": "buttons/sub.png", + "/": "buttons/div.png", + "*": "buttons/mul.png", + "=": "buttons/equal.png", } for i in range(10): - BUTTONS[str(i)] = 'buttons/{}.png'.format(i) + BUTTONS[str(i)] = f"buttons/{i}.png" cache_pos_button = dict() -# OpenCv -- for performance -# pip install opencv-python - -# pip install pyautogui -import pyautogui - -expression = '1234 * 222 + 3214 = ' +expression = "1234 * 222 + 3214 = " for x in expression: if x not in BUTTONS: - print('Not found: "{}"'.format(x)) + print(f'Not found: "{x}"') continue if x in cache_pos_button: pos_list = cache_pos_button[x] else: file_name = BUTTONS[x] - pos_list = [pyautogui.center(pos) for pos in pyautogui.locateAllOnScreen(BUTTONS[x])] + pos_list = [ + pyautogui.center(pos) for pos in pyautogui.locateAllOnScreen(BUTTONS[x]) + ] if not pos_list: continue diff --git a/pycountry__examples/get_ru_country_by_alpha2.py b/pycountry__examples/get_ru_country_by_alpha2.py index 92055786b..2c5b83f5f 100644 --- a/pycountry__examples/get_ru_country_by_alpha2.py +++ b/pycountry__examples/get_ru_country_by_alpha2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import gettext @@ -18,7 +18,7 @@ def get_country(code): return _(ru.name) -if __name__ == '__main__': +if __name__ == "__main__": print(get_country("RU")) # Российская Федерация diff --git a/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py b/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py index d7151cb31..1c3a930a1 100644 --- a/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py +++ b/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -18,22 +18,26 @@ class AuthenticationError(Exception): class CryptoAES: - def __init__(self, key: str): - self.key = hashlib.sha256(key.encode('utf-8')).digest() + def __init__(self, key: str) -> None: + self.key = hashlib.sha256(key.encode("utf-8")).digest() def encrypt(self, plain_text: str) -> str: - data = plain_text.encode('utf-8') + data = plain_text.encode("utf-8") cipher = AES.new(self.key, AES.MODE_EAX) cipher_text, tag = cipher.encrypt_and_digest(data) encrypted_data = cipher.nonce + tag + cipher_text - return base64.b64encode(encrypted_data).decode('utf-8') + return base64.b64encode(encrypted_data).decode("utf-8") def decrypt(self, encrypted_text: str) -> str: encrypted_data = base64.b64decode(encrypted_text) - nonce, tag, cipher_text = encrypted_data[:16], encrypted_data[16:32], encrypted_data[32:] + nonce, tag, cipher_text = ( + encrypted_data[:16], + encrypted_data[16:32], + encrypted_data[32:], + ) cipher = AES.new(self.key, AES.MODE_EAX, nonce) try: @@ -41,12 +45,12 @@ def decrypt(self, encrypted_text: str) -> str: except ValueError as e: raise AuthenticationError(e) - return data.decode('utf-8') + return data.decode("utf-8") -if __name__ == '__main__': - text = 'Hello World!' - password = '123' +if __name__ == "__main__": + text = "Hello World!" + password = "123" crypto = CryptoAES(password) encrypted_text = crypto.encrypt(text) @@ -57,6 +61,6 @@ def decrypt(self, encrypted_text: str) -> str: # Decrypt with invalid password try: - CryptoAES('abc').decrypt(encrypted_text) + CryptoAES("abc").decrypt(encrypted_text) except AuthenticationError as e: - assert str(e) == 'MAC check failed' + assert str(e) == "MAC check failed" diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py index 64b0613b4..eddc432a6 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для импорта rsa import sys -sys.path.append('..') + +sys.path.append("..") import rsa -FILE_NAME_PUBLIC_KEY = 'public.pem' -FILE_NAME_PRIVATE_KEY = 'private.pem' +FILE_NAME_PUBLIC_KEY = "public.pem" +FILE_NAME_PRIVATE_KEY = "private.pem" -with open(FILE_NAME_PUBLIC_KEY, 'rb') as f: +with open(FILE_NAME_PUBLIC_KEY, "rb") as f: public_pem = f.read() -with open(FILE_NAME_PRIVATE_KEY, 'rb') as f: +with open(FILE_NAME_PRIVATE_KEY, "rb") as f: private_pem = f.read() if public_pem and private_pem: @@ -25,27 +26,29 @@ private_key = rsa.import_key(private_pem) else: - print(f'The {repr(FILE_NAME_PUBLIC_KEY)} or {repr(FILE_NAME_PRIVATE_KEY)} files are empty! ' - f'Performing key generation!') + print( + f"The {repr(FILE_NAME_PUBLIC_KEY)} or {repr(FILE_NAME_PRIVATE_KEY)} files are empty! " + f"Performing key generation!" + ) public_key, private_key = rsa.new_keys(key_size=2048) - print('Key generation completed successfully!') + print("Key generation completed successfully!") - with open(FILE_NAME_PUBLIC_KEY, 'wb') as f: - f.write(public_key.exportKey('PEM')) + with open(FILE_NAME_PUBLIC_KEY, "wb") as f: + f.write(public_key.exportKey("PEM")) - with open(FILE_NAME_PRIVATE_KEY, 'wb') as f: - f.write(private_key.exportKey('PEM')) + with open(FILE_NAME_PRIVATE_KEY, "wb") as f: + f.write(private_key.exportKey("PEM")) - print('Successfully save generated keys to files!') + print("Successfully save generated keys to files!") print() -print('private:', private_key.exportKey('PEM')) -print('public:', public_key.exportKey('PEM')) +print("private:", private_key.exportKey("PEM")) +print("public:", public_key.exportKey("PEM")) print() -text = 'Hello World!'.encode('utf-8') +text = "Hello World!".encode("utf-8") encrypted = rsa.encrypt(text, public_key) print(encrypted) diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" index 0e364e905..0512d7a76 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Legrandin/pycryptodome/blob/29810b96cefc900138523203f027a179a15ceffe/Doc/src/examples.rst#generate-public-key-and-private-key @@ -10,8 +10,6 @@ # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" index cc84ee883..b7050dda3 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA SECRET_CODE = "Unguessable" key = RSA.generate(2048) -encrypted_key = key.export_key(passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC") +encrypted_key = key.export_key( + passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC" +) with open("rsa_key.bin", "wb") as f: f.write(encrypted_key) diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" index fe016325b..20b31880d 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" @@ -1,14 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py index 72fb9faf6..197e4dde8 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/42856051/5909792 @@ -12,12 +12,12 @@ import rsa -msg1 = "Hello Tony, I am Jarvis!".encode('utf-8') -msg2 = "Hello Toni, I am Jarvis!".encode('utf-8') +msg1 = "Hello Tony, I am Jarvis!".encode("utf-8") +msg2 = "Hello Toni, I am Jarvis!".encode("utf-8") public, private = rsa.new_keys(key_size=2048) -print('private:', private.exportKey('PEM')) -print('public:', public.exportKey('PEM')) +print("private:", private.exportKey("PEM")) +print("public:", public.exportKey("PEM")) print() # Encrypt-Decrypt: private -> private @@ -39,7 +39,7 @@ print("Verify:", verify) # False # Encrypt-Decrypt: private -> private -print('\n') +print("\n") # Encrypt-Decrypt: public -> private encrypted = b64encode(rsa.encrypt(msg1, public)) diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py index 78a60d081..88984e6fd 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py index a75143b8e..d1ba5eeec 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA SECRET_CODE = "Unguessable" key = RSA.generate(2048) -encrypted_key = key.export_key(passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC") +encrypted_key = key.export_key( + passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC" +) with open("rsa_key.bin", "wb") as f: f.write(encrypted_key) diff --git a/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py b/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py index 8e7d1c046..dc6768e73 100644 --- a/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py +++ b/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome @@ -12,15 +12,16 @@ # pip install pkcs7 from pkcs7 import PKCS7Encoder -key = '12647863128053333215894456187814' + +key = "12647863128053333215894456187814" mode = AES.MODE_ECB -text = 'Hello World!' +text = "Hello World!" encoder = PKCS7Encoder() pad_text = encoder.encode(text) -print(pad_text) # "Hello World!" -print(bytes(pad_text, 'utf-8')) # b'Hello World!\x04\x04\x04\x04' +print(pad_text) # "Hello World!" +print(bytes(pad_text, "utf-8")) # b'Hello World!\x04\x04\x04\x04' encryptor = AES.new(key, mode) @@ -32,7 +33,7 @@ decrypt_text = encryptor.decrypt(cipher) print(decrypt_text) # b'Hello World!\x04\x04\x04\x04' -decrypt_text = str(decrypt_text, 'utf-8') +decrypt_text = str(decrypt_text, "utf-8") unpad_text = encoder.decode(decrypt_text) print(unpad_text) # Hello World! diff --git a/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py b/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py index 910f3db02..e57c1f305 100644 --- a/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py +++ b/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Legrandin/pycryptodome @@ -10,8 +10,6 @@ # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.Cipher import DES @@ -26,11 +24,11 @@ def unpad(s: bytes) -> bytes: return s[:-pad_size] -key = b'abcdefgh' +key = b"abcdefgh" des = DES.new(key, DES.MODE_ECB) -text = b'Hello World!' +text = b"Hello World!" padded_text = pad(text) encrypted_text = des.encrypt(padded_text) diff --git a/pycryptodome__examples__AES_DES_RSA/info_security.py b/pycryptodome__examples__AES_DES_RSA/info_security.py index c6477c143..4af65909b 100644 --- a/pycryptodome__examples__AES_DES_RSA/info_security.py +++ b/pycryptodome__examples__AES_DES_RSA/info_security.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -14,26 +14,26 @@ from Crypto import Random -AES_key = '' +AES_key = "" # SOURCE: https://github.com/maldevel/gdog/blob/a3a47b17231d0ee3a2d0fa8ac986f7485f3f6b82/gdog.py#L68 class InfoSecurity: - def __init__(self, key: str): + def __init__(self, key: str) -> None: self.bs = 32 self.key = hashlib.sha256(key.encode()).digest() def encrypt(self, plain_text: str) -> str: - raw = self._pad(plain_text.encode('utf-8')) + raw = self._pad(plain_text.encode("utf-8")) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) - return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8') + return base64.b64encode(iv + cipher.encrypt(raw)).decode("utf-8") def decrypt(self, cipher_text: str) -> str: enc = base64.b64decode(cipher_text) - iv = enc[:AES.block_size] + iv = enc[: AES.block_size] cipher = AES.new(self.key, AES.MODE_CBC, iv) - return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') + return self._unpad(cipher.decrypt(enc[AES.block_size :])).decode("utf-8") # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/82810f8397907679316107f135a5b1dd1fcca516/pad__unpad__example.py#L7 def _pad(self, s: bytes) -> bytes: @@ -46,15 +46,15 @@ def _unpad(s: bytes) -> bytes: return s[:-pad_size] -if __name__ == '__main__': +if __name__ == "__main__": info_sec = InfoSecurity(AES_key) - text = 'Hello World!' + text = "Hello World!" cipher_text = info_sec.encrypt(text) print(cipher_text) assert info_sec.decrypt(cipher_text) == text - text = 'Привет Мир!' + text = "Привет Мир!" cipher_text = info_sec.encrypt(text) print(cipher_text) assert info_sec.decrypt(cipher_text) == text diff --git a/pygame__examples/about_joystick.py b/pygame__examples/about_joystick.py index a7f2fe86f..5e9864c45 100644 --- a/pygame__examples/about_joystick.py +++ b/pygame__examples/about_joystick.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pygame.org/docs/ref/joystick.html @@ -9,6 +9,7 @@ import pygame + # Define some colors BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -18,24 +19,24 @@ # It has nothing to do with the joysticks, just outputting the # information. class TextPrint: - def __init__(self): + def __init__(self) -> None: self.reset() self.font = pygame.font.Font(None, 20) - def print(self, screen, textString): + def print(self, screen, textString) -> None: textBitmap = self.font.render(textString, True, BLACK) screen.blit(textBitmap, [self.x, self.y]) self.y += self.line_height - def reset(self): + def reset(self) -> None: self.x = 10 self.y = 10 self.line_height = 15 - def indent(self): + def indent(self) -> None: self.x += 10 - def unindent(self): + def unindent(self) -> None: self.x -= 10 @@ -81,7 +82,7 @@ def unindent(self): # Get count of joysticks joystick_count = pygame.joystick.get_count() - textPrint.print(screen, "Number of joysticks: {}".format(joystick_count)) + textPrint.print(screen, f"Number of joysticks: {joystick_count}") textPrint.indent() # For each joystick: @@ -89,42 +90,42 @@ def unindent(self): joystick = pygame.joystick.Joystick(i) joystick.init() - textPrint.print(screen, "Joystick {}".format(i)) + textPrint.print(screen, f"Joystick {i}") textPrint.indent() # Get the name from the OS for the controller/joystick name = joystick.get_name() - textPrint.print(screen, "Joystick name: {}".format(name)) + textPrint.print(screen, f"Joystick name: {name}") # Usually axis run in pairs, up/down for one, and left/right for # the other. axes = joystick.get_numaxes() - textPrint.print(screen, "Number of axes: {}".format(axes)) + textPrint.print(screen, f"Number of axes: {axes}") textPrint.indent() for i in range(axes): axis = joystick.get_axis(i) - textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis)) + textPrint.print(screen, f"Axis {i} value: {axis:>6.3f}") textPrint.unindent() buttons = joystick.get_numbuttons() - textPrint.print(screen, "Number of buttons: {}".format(buttons)) + textPrint.print(screen, f"Number of buttons: {buttons}") textPrint.indent() for i in range(buttons): button = joystick.get_button(i) - textPrint.print(screen, "Button {:>2} value: {}".format(i, button)) + textPrint.print(screen, f"Button {i:>2} value: {button}") textPrint.unindent() # Hat switch. All or nothing for direction, not like joysticks. # Value comes back in an array. hats = joystick.get_numhats() - textPrint.print(screen, "Number of hats: {}".format(hats)) + textPrint.print(screen, f"Number of hats: {hats}") textPrint.indent() for i in range(hats): hat = joystick.get_hat(i) - textPrint.print(screen, "Hat {} value: {}".format(i, str(hat))) + textPrint.print(screen, f"Hat {i} value: {str(hat)}") textPrint.unindent() textPrint.unindent() diff --git a/pygame__examples/buttons__using_pygametext.py b/pygame__examples/buttons__using_pygametext.py index efcd800d0..abe72750f 100644 --- a/pygame__examples/buttons__using_pygametext.py +++ b/pygame__examples/buttons__using_pygametext.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys + # pip install pygame # pip install pygametext import pygame import pygametext + running = True pygame.init() @@ -18,26 +21,32 @@ pgt = pygametext.PGT(screen) # Define pygametext object. -pgt.button(10, 10, 100, 50, (255, 0, 0), "Hello!", (0, 0, 0), print, "Hello World!", 0) # Add pgt Button -pgt.button(120, 10, 100, 50, (255, 255, 0), "Bye bye", (0, 0, 0), print, "Goodbye World!", 0) # Add pgt Button +pgt.button( + 10, 10, 100, 50, (255, 0, 0), "Hello!", (0, 0, 0), print, "Hello World!", 0 +) # Add pgt Button +pgt.button( + 120, 10, 100, 50, (255, 255, 0), "Bye bye", (0, 0, 0), print, "Goodbye World!", 0 +) # Add pgt Button pgt.text(10, 70, "Simple pygametext example.", (0, 120, 0), 20, 0) # Add pgt Text -def update(): # Update & Eventd +def update() -> None: # Update & Eventd global running events = pygame.event.get() - pgt.update(events, 0) # Update all pgt elements from layer 0. Takes events arg to process some elements. + pgt.update( + events, 0 + ) # Update all pgt elements from layer 0. Takes events arg to process some elements. for event in events: if event.type == pygame.QUIT: running = False pygame.quit() - quit() + sys.exit() -def draw(): +def draw() -> None: screen.fill((255, 255, 255)) # Clear screen pgt.draw() # Draw all pgt elements from layer 0 diff --git a/pygame__examples/circle_collision.py b/pygame__examples/circle_collision.py index 104107453..83290096a 100644 --- a/pygame__examples/circle_collision.py +++ b/pygame__examples/circle_collision.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -10,12 +10,12 @@ import pygame -BLACK = ( 0, 0, 0) +BLACK = (0, 0, 0) WHITE = (255, 255, 255) class Ball(pygame.sprite.Sprite): - def __init__(self, size, pos=(0, 0), color=WHITE): + def __init__(self, size, pos=(0, 0), color=WHITE) -> None: super().__init__() self.image = pygame.Surface([size, size], pygame.SRCALPHA) @@ -58,13 +58,13 @@ def __init__(self, size, pos=(0, 0), color=WHITE): screen.fill(WHITE) balls.draw(screen) - text = fnt.render("Collision!", True, color) - screen.blit(text, (260, 20)) - if pygame.sprite.collide_circle(ball_1, ball_2): color = BLACK else: color = WHITE + text = fnt.render("Collision!", True, color) + screen.blit(text, (260, 20)) + pygame.display.update() clock.tick(60) diff --git a/pygame__examples/complex_balls_with_physics.py b/pygame__examples/complex_balls_with_physics.py index 45ae742f9..f6fc2c93a 100644 --- a/pygame__examples/complex_balls_with_physics.py +++ b/pygame__examples/complex_balls_with_physics.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math import random -from typing import Tuple, Optional # SOURCE: http://archive.petercollingridge.co.uk/book/export/html/6 import pygame @@ -19,7 +18,7 @@ gravity = (math.pi, 0.002) -def add_vectors(angle1, length1, angle2, length2) -> Tuple[float, float]: +def add_vectors(angle1, length1, angle2, length2) -> tuple[float, float]: x = math.sin(angle1) * length1 + math.sin(angle2) * length2 y = math.cos(angle1) * length1 + math.cos(angle2) * length2 @@ -29,7 +28,7 @@ def add_vectors(angle1, length1, angle2, length2) -> Tuple[float, float]: return angle, length -def collide(p1, p2): +def collide(p1, p2) -> None: dx = p1.x - p2.x dy = p1.y - p2.y @@ -39,12 +38,16 @@ def collide(p1, p2): total_mass = p1.mass + p2.mass (p1.angle, p1.speed) = add_vectors( - p1.angle, p1.speed * (p1.mass - p2.mass) / total_mass, - angle, 2 * p2.speed * p2.mass / total_mass + p1.angle, + p1.speed * (p1.mass - p2.mass) / total_mass, + angle, + 2 * p2.speed * p2.mass / total_mass, ) (p2.angle, p2.speed) = add_vectors( - p2.angle, p2.speed * (p2.mass - p1.mass) / total_mass, - angle + math.pi, 2 * p1.speed * p1.mass / total_mass + p2.angle, + p2.speed * (p2.mass - p1.mass) / total_mass, + angle + math.pi, + 2 * p1.speed * p1.mass / total_mass, ) p1.speed *= elasticity p2.speed *= elasticity @@ -57,7 +60,7 @@ def collide(p1, p2): class Particle: - def __init__(self, x, y, size, mass=1): + def __init__(self, x, y, size, mass=1) -> None: self.x = x self.y = y self.size = size @@ -68,27 +71,25 @@ def __init__(self, x, y, size, mass=1): self.mass = mass self.drag = (self.mass / (self.mass + mass_of_air)) ** self.size - def display(self): + def display(self) -> None: pygame.draw.circle( - screen, self.colour, - (int(self.x), int(self.y)), - self.size, self.thickness + screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness ) - def move(self): + def move(self) -> None: self.x += math.sin(self.angle) * self.speed self.y -= math.cos(self.angle) * self.speed self.speed *= self.drag - def bounce(self): + def bounce(self) -> None: if self.x > width - self.size: self.x = 2 * (width - self.size) - self.x - self.angle = - self.angle + self.angle = -self.angle self.speed *= elasticity elif self.x < self.size: self.x = 2 * self.size - self.x - self.angle = - self.angle + self.angle = -self.angle self.speed *= elasticity if self.y > height - self.size: @@ -102,20 +103,20 @@ def bounce(self): self.speed *= elasticity -def find_particle(particles, x, y) -> Optional[Particle]: +def find_particle(particles, x, y) -> Particle | None: for p in particles: if math.hypot(p.x - x, p.y - y) <= p.size: return p return None -def get_rand_color() -> Tuple[int, int, int]: +def get_rand_color() -> tuple[int, int, int]: return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) clock = pygame.time.Clock() screen = pygame.display.set_mode((width, height)) -pygame.display.set_caption('Tutorial 9') +pygame.display.set_caption("Tutorial 9") number_of_particles = 5 my_particles = [] @@ -126,7 +127,7 @@ def get_rand_color() -> Tuple[int, int, int]: x = random.randint(size, width - size) y = random.randint(size, height - size) - particle = Particle(x, y, size, density * size ** 2) + particle = Particle(x, y, size, density * size**2) particle.colour = get_rand_color() particle.speed = random.random() particle.angle = random.uniform(0, math.pi * 2) @@ -157,7 +158,7 @@ def get_rand_color() -> Tuple[int, int, int]: for i, particle in enumerate(my_particles): particle.move() particle.bounce() - for particle2 in my_particles[i + 1:]: + for particle2 in my_particles[i + 1 :]: collide(particle, particle2) particle.display() diff --git a/pygame__examples/is_point_in_triangle.py b/pygame__examples/is_point_in_triangle.py new file mode 100644 index 000000000..911a0aeae --- /dev/null +++ b/pygame__examples/is_point_in_triangle.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +import sys + +import pygame + + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) + + +@dataclass +class Point: + x: float + y: float + + +def get_triangle_area(a: Point, b: Point, c: Point) -> float: + return abs((a.x - c.x) * (b.y - c.y) + (b.x - c.x) * (c.y - a.y)) + + +def is_point_in_triangle(a: Point, b: Point, c: Point, p: Point) -> bool: + tr_area = get_triangle_area(a, b, c) # Площадь основного треугольника + + tr_area2 = get_triangle_area( + a, b, p + ) # Площади треугольника, образованного из 2 точек основного + tr_area3 = get_triangle_area( + a, p, c + ) # и точки, которая проверяется на принадлежность + tr_area4 = get_triangle_area(b, p, c) # к треугольнику + + # Если площади образованных треугольников равны, то точка в треугольнике + return tr_area == tr_area2 + tr_area3 + tr_area4 + + +pygame.init() + +color = BLACK +screen = pygame.display.set_mode((600, 600)) +clock = pygame.time.Clock() +mouse_pos = Point(0, 0) + +# Triangle +a, b, c = Point(100, 100), Point(100, 400), Point(500, 500) + +fnt = pygame.font.Font(None, 40) + + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + + elif event.type == pygame.MOUSEMOTION: + mouse_pos.x, mouse_pos.y = event.pos + break + + screen.fill(WHITE) + + # This draws a triangle using the polygon command + pygame.draw.polygon(screen, BLACK, [[a.x, a.y], [b.x, b.y], [c.x, c.y]], 5) + + if is_point_in_triangle(a, b, c, mouse_pos): + color = BLACK + else: + color = WHITE + + text = fnt.render("Collision!", True, color) + screen.blit(text, (260, 20)) + + pygame.display.update() + clock.tick(60) diff --git a/pygame__examples/move_circle.py b/pygame__examples/move_circle.py index e9ffd57fa..e79999859 100644 --- a/pygame__examples/move_circle.py +++ b/pygame__examples/move_circle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygame @@ -58,6 +58,6 @@ if is_pressed[pygame.K_DOWN]: y += step - pygame.display.set_caption("move_circle [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_circle [{int(clock.get_fps())} fps]") clock.tick(FPS) diff --git a/pygame__examples/move_rect.py b/pygame__examples/move_rect.py index 48da911de..040bffe9f 100644 --- a/pygame__examples/move_rect.py +++ b/pygame__examples/move_rect.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.nerdparadise.com/programming/pygame/part1 # pip install pygame import pygame + + pygame.init() screen = pygame.display.set_mode((400, 300)) @@ -53,7 +55,7 @@ pygame.display.flip() - pygame.display.set_caption("move_rect [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_rect [{int(clock.get_fps())} fps]") # will block execution until 1/60 seconds have passed # since the previous time clock.tick was called. diff --git a/pygame__examples/move_rect__with_joystick__hat_axis_button.py b/pygame__examples/move_rect__with_joystick__hat_axis_button.py index cfc7b7a27..afbe15f66 100644 --- a/pygame__examples/move_rect__with_joystick__hat_axis_button.py +++ b/pygame__examples/move_rect__with_joystick__hat_axis_button.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.nerdparadise.com/programming/pygame/part1 -# pip install pygame -import pygame import math +import sys + +# pip install pygame +import pygame pygame.init() @@ -22,7 +24,7 @@ else: # No joysticks! print("Error, I didn't find any joysticks.") - quit() + sys.exit() screen = pygame.display.set_mode((400, 300)) @@ -83,7 +85,7 @@ pygame.display.flip() - pygame.display.set_caption("move_rect [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_rect [{int(clock.get_fps())} fps]") # will block execution until 1/60 seconds have passed # since the previous time clock.tick was called. diff --git a/pygame__examples/multiline_text__using_ptext/invisible_text.py b/pygame__examples/multiline_text__using_ptext/invisible_text.py index c0e6c394b..6dab8fb77 100644 --- a/pygame__examples/multiline_text__using_ptext/invisible_text.py +++ b/pygame__examples/multiline_text__using_ptext/invisible_text.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -12,7 +12,7 @@ import ptext -BLACK = ( 0, 0, 0) +BLACK = (0, 0, 0) WHITE = (255, 255, 255) diff --git a/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py b/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py index 02bfaf0dc..790bb8938 100644 --- a/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py +++ b/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pygame -import pygame.locals + pygame.init() -screen = pygame.display.set_mode((300,200)) +screen = pygame.display.set_mode((300, 200)) running = True @@ -20,7 +20,10 @@ elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False - elif event.key == pygame.K_BACKSPACE and pygame.key.get_mods() & pygame.KMOD_LCTRL: + elif ( + event.key == pygame.K_BACKSPACE + and pygame.key.get_mods() & pygame.KMOD_LCTRL + ): print("pressed: LCTRL + BACKSPACE") pygame.quit() diff --git a/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py b/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py index 2cb988349..bc09d0c56 100644 --- a/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py +++ b/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pygame -import pygame.locals + pygame.init() diff --git a/pygame__examples/rect_collision.py b/pygame__examples/rect_collision.py index 67a91125c..3d3270ff4 100644 --- a/pygame__examples/rect_collision.py +++ b/pygame__examples/rect_collision.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -36,6 +36,11 @@ rect2 = surf2.get_rect(center=event.pos) break + if rect2.colliderect(rect1): + color = (200, 200, 200) + else: + color = (0, 0, 0) + text = fnt.render("Collision!", True, color) screen.fill((0, 0, 0)) @@ -43,10 +48,5 @@ screen.blit(surf2, rect2) screen.blit(text, (260, 20)) - if rect2.colliderect(rect1): - color = (200, 200, 200) - else: - color = (0, 0, 0) - pygame.display.update() clock.tick(60) diff --git a/pygame__examples/simple_balls.py b/pygame__examples/simple_balls.py index e66f48e91..2295a3d57 100644 --- a/pygame__examples/simple_balls.py +++ b/pygame__examples/simple_balls.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from random import randint @@ -11,7 +11,7 @@ class Ball: - def __init__(self, x, y, r, v_x, v_y, color): + def __init__(self, x, y, r, v_x, v_y, color) -> None: self.x = x self.y = y self.r = r @@ -19,11 +19,11 @@ def __init__(self, x, y, r, v_x, v_y, color): self.v_y = v_y self.color = color - def update(self): + def update(self) -> None: self.x += self.v_x self.y += self.v_y - def draw(self, screen): + def draw(self, screen) -> None: pygame.draw.circle(screen, self.color, self.center, self.r) # Нарисуем поверх первого, прозрачный второй с границей (параметр width) @@ -51,7 +51,14 @@ def right(self): class Game: - def __init__(self, width: int, height: int, caption='Balls!', background_color=(255, 255, 255), frame_rate=60): + def __init__( + self, + width: int, + height: int, + caption="Balls!", + background_color=(255, 255, 255), + frame_rate=60, + ) -> None: self.width = width self.height = height self.frame_rate = frame_rate @@ -70,10 +77,12 @@ def __init__(self, width: int, height: int, caption='Balls!', background_color=( self.update_caption() - def update_caption(self): - pygame.display.set_caption("{} [{} fps]".format(self.caption, int(self.clock.get_fps()))) + def update_caption(self) -> None: + pygame.display.set_caption( + f"{self.caption} [{int(self.clock.get_fps())} fps]" + ) - def handle_events(self): + def handle_events(self) -> None: # Получение всех событий for event in pygame.event.get(): # Проверка события "Выход" @@ -85,7 +94,7 @@ def handle_events(self): if is_pressed[pygame.K_ESCAPE]: self.game_active = False - def run(self): + def run(self) -> None: self.game_active = True while self.game_active: @@ -112,7 +121,7 @@ def run(self): self.clock.tick(self.frame_rate) - def append_random_ball(self): + def append_random_ball(self) -> None: def get_random_vector(): pos = 0, 0 # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться @@ -134,8 +143,8 @@ def get_random_color(): self.balls.append(ball) -if __name__ == '__main__': - WIDTH = 600 # ширина экрана +if __name__ == "__main__": + WIDTH = 600 # ширина экрана HEIGHT = 600 # высота экрана BALL_NUMBER = 100 diff --git a/pyglet_example/main.py b/pyglet_example/main.py index b5436f639..648220488 100644 --- a/pyglet_example/main.py +++ b/pyglet_example/main.py @@ -1,34 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyglet from pyglet.gl import * + # Optional audio outputs (Linux examples): # pyglet.options['audio'] = ('alsa', 'openal', 'silent') key = pyglet.window.key class main(pyglet.window.Window): - def __init__ (self): - super(main, self).__init__(800, 800, fullscreen = False) + def __init__(self) -> None: + super(main, self).__init__(800, 800, fullscreen=False) self.x, self.y = 0, 0 - self.bg = pyglet.sprite.Sprite(pyglet.image.load('background.jpg')) + self.bg = pyglet.sprite.Sprite(pyglet.image.load("background.jpg")) self.sprites = {} self.player = pyglet.media.Player() self.alive = 1 - def on_draw(self): + def on_draw(self) -> None: self.render() - def on_close(self): + def on_close(self) -> None: self.alive = 0 - def on_key_press(self, symbol, modifiers): + def on_key_press(self, symbol, modifiers) -> None: # Do something when a key is pressed? # Pause the audio for instance? # use `if symbol == key.SPACE: ...` @@ -37,13 +38,13 @@ def on_key_press(self, symbol, modifiers): # You could also do a standard input() call and enter a string # on the command line. if symbol == key.ENTER: - self.player.queue(pyglet.media.load('speak.mp3', streaming = False)) + self.player.queue(pyglet.media.load("speak.mp3", streaming=False)) if not self.player.playing: self.player.play() - if symbol == key.ESCAPE: # [ESC] + if symbol == key.ESCAPE: # [ESC] self.alive = 0 - def render(self): + def render(self) -> None: self.clear() self.bg.draw() @@ -54,7 +55,7 @@ def render(self): self.flip() - def run(self): + def run(self) -> None: while self.alive == 1: self.render() @@ -63,7 +64,7 @@ def run(self): # but is required for the GUI to not freeze # event = self.dispatch_events() - self.player.delete() # Free resources. (Not really needed but as an example) + self.player.delete() # Free resources. (Not really needed but as an example) x = main() diff --git a/pyglet_example/using_thread.py b/pyglet_example/using_thread.py index 849763cf6..a5af84548 100644 --- a/pyglet_example/using_thread.py +++ b/pyglet_example/using_thread.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from threading import Thread @@ -10,8 +10,8 @@ import pyglet -def play_song(): - song = pyglet.media.load('speak.mp3') +def play_song() -> None: + song = pyglet.media.load("speak.mp3") song.play() pyglet.app.run() diff --git a/pymem__examples/hello_world.py b/pymem__examples/hello_world.py new file mode 100644 index 000000000..86455892d --- /dev/null +++ b/pymem__examples/hello_world.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pymem +from pymem import Pymem + + +pm = Pymem("notepad.exe") +print("Process id: %s" % pm.process_id) +address = pm.allocate(10) +print("Allocated address: %s" % address) +pm.write_int(address, 1337) +value = pm.read_int(address) +print("Allocated value: %s" % value) +pm.free(address) diff --git a/pymem__examples/inject_python_interpreter.py b/pymem__examples/inject_python_interpreter.py new file mode 100644 index 000000000..2d214f26a --- /dev/null +++ b/pymem__examples/inject_python_interpreter.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import subprocess + +from pymem import Pymem + + +# TODO: user +# 2021-02-05 13:17:35,762 - pymem - DEBUG - Process 48824 is being debugged +# 2021-02-05 13:17:35,787 - pymem - WARNING - Got an error in start thread, code: 87 +# 2021-02-05 13:17:35,799 - pymem - DEBUG - New thread_id: 0x000001d0 +# 2021-02-05 13:17:35,799 - pymem - DEBUG - Py_InitializeEx loc: 0x7ff853a31e08 +# 2021-02-05 13:17:35,799 - pymem - DEBUG - PyRun_SimpleString loc: 0x7ff853a45348 +# 2021-02-05 13:17:35,799 - pymem - DEBUG - shellcode_addr loc: 0x2358be30000 +# 2021-02-05 13:17:35,799 - pymem - WARNING - Got an error in start thread, code: 5 +# Traceback (most recent call last): +# File "C:/Users/ipetrash/Projects/SimplePyScripts/pymem__examples/inject_python_interpreter.py", line 22, in +# pm.inject_python_shellcode(shellcode) +# File "C:\Users\ipetrash\Anaconda3\lib\site-packages\pymem\__init__.py", line 147, in inject_python_shellcode +# self.start_thread(self.py_run_simple_string, shellcode_addr) +# File "C:\Users\ipetrash\Anaconda3\lib\site-packages\pymem\__init__.py", line 180, in start_thread +# pymem.logger.debug('New thread_id: 0x%08x' % thread_h) +# TypeError: %x format: an integer is required, not NoneType +# +# Process finished with exit code 1 + +# TODO: admin +# 2021-02-05 13:18:14,957 - pymem - DEBUG - Process 20412 is being debugged +# 2021-02-05 13:18:14,977 - pymem - WARNING - Got an error in start thread, code: 87 +# 2021-02-05 13:18:14,995 - pymem - DEBUG - New thread_id: 0x00000234 +# 2021-02-05 13:18:14,995 - pymem - DEBUG - Py_InitializeEx loc: 0x7ff853a31e08 +# 2021-02-05 13:18:14,995 - pymem - DEBUG - PyRun_SimpleString loc: 0x7ff853a45348 +# Traceback (most recent call last): +# File "C:\Users\ipetrash\Projects\SimplePyScripts\pymem__examples\inject_python_interpreter.py", line 22, in +# pm.inject_python_shellcode(shellcode) +# File "C:\Users\ipetrash\Anaconda3\lib\site-packages\pymem\__init__.py", line 142, in inject_python_shellcode +# raise RuntimeError('Could not allocate memory for shellcode') +# RuntimeError: Could not allocate memory for shellcode + + +notepad = subprocess.Popen(["notepad.exe"]) + +pm = Pymem("notepad.exe") +pm.inject_python_interpreter() +filepath = os.path.join(os.path.abspath("."), "pymem_injection.txt") +filepath = filepath.replace("\\", "\\\\") +shellcode = f""" +f = open("{filepath}", "w+") +f.write("pymem_injection") +f.close() +""" +pm.inject_python_shellcode(shellcode) +notepad.kill() diff --git a/pymem__examples/listing_process_modules.py b/pymem__examples/listing_process_modules.py new file mode 100644 index 000000000..23d089d7e --- /dev/null +++ b/pymem__examples/listing_process_modules.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import pymem + + +pm = pymem.Pymem("python.exe") + +for module in pm.list_modules(): + print(module.name) diff --git "a/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" "b/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" index 73981ee04..30693d05a 100644 --- "a/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" +++ "b/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" @@ -1,45 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# Для импортирования ascii_table__simple_pretty__format.py -import sys -sys.path.append('..') - -from ascii_table__simple_pretty__format import print_pretty_table - # pip install pymorphy2 import pymorphy2 +# pip install tabulate +from tabulate import tabulate + morph = pymorphy2.MorphAnalyzer() -words = [ - 'программист', - 'ёлка', - 'фрукт', - 'игр', - 'барсук' -] +words = ["программист", "ёлка", "фрукт", "игр", "барсук"] parsed_words = [morph.parse(word)[0] for word in words] -rows = [words] +rows = [] for i in range(5 + 1): row = [] for parsed_word in parsed_words: word = parsed_word.make_agree_with_number(i).word - row.append(f'{i} {word}') + row.append(f"{i} {word}") rows.append(row) -print_pretty_table(rows, align='<') -# программист | ёлка | фрукт | игр | барсук -# ----------------+--------+-----------+--------+----------- -# 0 программистов | 0 ёлок | 0 фруктов | 0 игр | 0 барсуков -# 1 программист | 1 ёлка | 1 фрукт | 1 игры | 1 барсук -# 2 программиста | 2 ёлки | 2 фрукта | 2 игр | 2 барсука -# 3 программиста | 3 ёлки | 3 фрукта | 3 игр | 3 барсука -# 4 программиста | 4 ёлки | 4 фрукта | 4 игр | 4 барсука -# 5 программистов | 5 ёлок | 5 фруктов | 5 игр | 5 барсуков +print(tabulate(rows, headers=words, tablefmt="orgtbl")) +""" +| программист | ёлка | фрукт | игр | барсук | +|-----------------+--------+-----------+--------+------------| +| 0 программистов | 0 ёлок | 0 фруктов | 0 игр | 0 барсуков | +| 1 программист | 1 ёлка | 1 фрукт | 1 игры | 1 барсук | +| 2 программиста | 2 ёлки | 2 фрукта | 2 игр | 2 барсука | +| 3 программиста | 3 ёлки | 3 фрукта | 3 игр | 3 барсука | +| 4 программиста | 4 ёлки | 4 фрукта | 4 игр | 4 барсука | +| 5 программистов | 5 ёлок | 5 фруктов | 5 игр | 5 барсуков | +""" diff --git a/pymorphy2__examples/find_gender_by_first_name.py b/pymorphy2__examples/find_gender_by_first_name.py index 3e88ba131..44d59d310 100644 --- a/pymorphy2__examples/find_gender_by_first_name.py +++ b/pymorphy2__examples/find_gender_by_first_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 @@ -9,15 +9,23 @@ name_list = [ - 'Константин', 'Виктор', 'Любовь', 'Дамир', 'Венера', - 'Таисия', 'Алёна', 'Евгений', 'Егор', 'Никита' + "Константин", + "Виктор", + "Любовь", + "Дамир", + "Венера", + "Таисия", + "Алёна", + "Евгений", + "Егор", + "Никита", ] morph = pymorphy2.MorphAnalyzer() for name in name_list: parsed_word = morph.parse(name)[0] - print('{:<15} {}'.format(name, parsed_word.tag.gender)) + print(f"{name:<15} {parsed_word.tag.gender}") # Константин masc # Виктор masc diff --git a/pymorphy2__examples/get_tokens.py b/pymorphy2__examples/get_tokens.py new file mode 100644 index 000000000..518e1ec40 --- /dev/null +++ b/pymorphy2__examples/get_tokens.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pymorphy2 +import pymorphy2 +from pymorphy2.tokenizers import simple_word_tokenize + + +morph = pymorphy2.MorphAnalyzer() + + +def get_tokens(text: str, ignore_punctuations=False) -> list[pymorphy2.analyzer.Parse]: + tokens = [morph.parse(word)[0] for word in simple_word_tokenize(text)] + if ignore_punctuations: + tokens = [token for token in tokens if "PNCT" not in token.tag] + return tokens + + +if __name__ == "__main__": + text = "чё за дом! ни конфет, ни печенек, пришлось жрать СОЛЁНЫЙ огурец ((" + + for token in get_tokens(text, ignore_punctuations=True): + print(token.word, token.normal_form) diff --git a/pymorphy2__examples/normal_form.py b/pymorphy2__examples/normal_form.py index cb3b3b69f..ac64bcbca 100644 --- a/pymorphy2__examples/normal_form.py +++ b/pymorphy2__examples/normal_form.py @@ -1,17 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 import pymorphy2 + + morph = pymorphy2.MorphAnalyzer() -def get_normal_form(word): +def get_normal_form(word: str) -> str: return morph.parse(word)[0].normal_form -print(get_normal_form('Иванова')) # иванов -print(get_normal_form('Иванов')) # иванов +if __name__ == "__main__": + print(get_normal_form("ИВАНОВА")) # иванов + print(get_normal_form("Иванова")) # иванов + print(get_normal_form("Иванов")) # иванов diff --git a/pymorphy2__examples/parse_word.py b/pymorphy2__examples/parse_word.py index d79e7f831..b28bea875 100644 --- a/pymorphy2__examples/parse_word.py +++ b/pymorphy2__examples/parse_word.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 @@ -10,9 +10,7 @@ morph = pymorphy2.MorphAnalyzer() -words = [ - "Думала", "Подумала", "Согласен" -] +words = ["Думала", "Подумала", "Согласен"] for word in words: print(repr(word)) diff --git "a/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" "b/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" index 6acb45ef5..682f49b82 100644 --- "a/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" +++ "b/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk @@ -19,35 +19,35 @@ def is_ADJS_sing_femn(parsed: pymorphy2.analyzer.Parse) -> bool: """ Имя прилагательное (краткое) + единственное число + женский род """ - return {'ADJS', 'sing', 'femn'} in parsed.tag + return {"ADJS", "sing", "femn"} in parsed.tag def is_ADJS_sing_masc(parsed: pymorphy2.analyzer.Parse) -> bool: """ Имя прилагательное (краткое) + единственное число + мужской род """ - return {'ADJS', 'sing', 'masc'} in parsed.tag + return {"ADJS", "sing", "masc"} in parsed.tag def is_VERB_sing_femn(parsed: pymorphy2.analyzer.Parse) -> bool: """ Глагол (личная форма) + единственное число + женский род """ - return {'VERB', 'sing', 'femn'} in parsed.tag + return {"VERB", "sing", "femn"} in parsed.tag def is_VERB_sing_masc(parsed: pymorphy2.analyzer.Parse) -> bool: """ Глагол (личная форма) + единственное число + мужской род """ - return {'VERB', 'sing', 'masc'} in parsed.tag + return {"VERB", "sing", "masc"} in parsed.tag def is_NPRO_1per_sing(parsed: pymorphy2.analyzer.Parse) -> bool: """ Местоимение-существительное + 1 лицо + единственное число """ - return {'NPRO', '1per', 'sing'} in parsed.tag + return {"NPRO", "1per", "sing"} in parsed.tag LOG_DEBUG = False @@ -58,11 +58,11 @@ def is_NPRO_1per_sing(parsed: pymorphy2.analyzer.Parse) -> bool: def is_femn(text: str) -> bool: for line in text.splitlines(): - LOG_DEBUG and print(f'[#] Comment: {line!r}') + LOG_DEBUG and print(f"[#] Comment: {line!r}") # Перебор предложений - for sent in nltk.sent_tokenize(line, language='russian'): - LOG_DEBUG and print(f'[#] Comment part: {sent!r}') + for sent in nltk.sent_tokenize(line, language="russian"): + LOG_DEBUG and print(f"[#] Comment part: {sent!r}") words = nltk.word_tokenize(sent) parsed_words = [morph.parse(word)[0] for word in words] @@ -76,11 +76,11 @@ def is_femn(text: str) -> bool: has_NPRO_1per_sing = False - LOG_DEBUG and print(f'[#] ({len(words)}): {words}') - LOG_DEBUG and print(f'[#] ({len(parsed_words)}):') + LOG_DEBUG and print(f"[#] ({len(words)}): {words}") + LOG_DEBUG and print(f"[#] ({len(parsed_words)}):") for parsed in parsed_words: - LOG_DEBUG and print(f'[#] {parsed.word} - {str(parsed.tag)!r}') + LOG_DEBUG and print(f"[#] {parsed.word} - {str(parsed.tag)!r}") if is_NPRO_1per_sing(parsed): has_NPRO_1per_sing = True @@ -99,26 +99,28 @@ def is_femn(text: str) -> bool: return False -if __name__ == '__main__': +if __name__ == "__main__": import json - with open('comments.json', encoding='utf-8') as f: + with open("comments.json", encoding="utf-8") as f: data = json.load(f) - comments = [(x['text'], x['expected']) for x in data] + comments = [(x["text"], x["expected"]) for x in data] matches = 0 total = len(comments) for i, (text, expected) in enumerate(comments, 1): has_female = is_femn(text) - match = has_female == (expected == 'female') + match = has_female == (expected == "female") matches += match - print(f"{i}. {text!r}" - f"\n [{'+' if match else '-'}] " - f"Expected={expected}, " - f"actual={'female' if has_female else 'male'}") - print('-' * 100) + print( + f"{i}. {text!r}" + f"\n [{'+' if match else '-'}] " + f"Expected={expected}, " + f"actual={'female' if has_female else 'male'}" + ) + print("-" * 100) - print(f'Total: {matches} / {total}') + print(f"Total: {matches} / {total}") diff --git a/pynput__examples/console_board_game.py b/pynput__examples/console_board_game.py new file mode 100644 index 000000000..0bc7ec743 --- /dev/null +++ b/pynput__examples/console_board_game.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from random import randint + +import time +import os + +# pip install pynput-1.8.1 +from pynput import keyboard + + +BOARD_TEMPLATE = """ +############################## +#G # +######## ####### ####### # +# GG # # +# GG # # G G G G G G # # +# GG # ################ # +# GG # # +# # ###### ### #### # +# #GG # # # #GG# # +# #GG # # @ # +############################## +""".strip() + + +def clear() -> None: + os.system("clear" if os.name == "posix" else "cls") + + +@dataclass +class GameObject: + pos_x: int = 0 + pos_y: int = 0 + + icon: str = " " + + +@dataclass +class Wall(GameObject): + icon: str = "#" + + +@dataclass +class Gold(GameObject): + value: int = 0 + icon: str = "G" + + +@dataclass +class Hero(GameObject): + gold: int = 0 + icon: str = "@" + + +class Game: + def __init__(self) -> None: + self.hero: Hero = Hero() + self.board: list[list[GameObject]] = [] + self.is_active: bool = False + + self.listener = keyboard.Listener(on_release=self._on_release) + + def _on_release(self, key) -> None: + self.logic(key) + + def do_step(self) -> bool: + self.draw() + + return self.is_active + + def logic(self, key) -> None: + pos_x, pos_y = self.hero.pos_x, self.hero.pos_y + + if key == keyboard.Key.esc: + self.finish() + return + + elif key == keyboard.KeyCode.from_char("w") or key == keyboard.Key.up: + self.hero.pos_y -= 1 + + elif key == keyboard.KeyCode.from_char("s") or key == keyboard.Key.down: + self.hero.pos_y += 1 + + elif key == keyboard.KeyCode.from_char("d") or key == keyboard.Key.right: + self.hero.pos_x += 1 + + elif key == keyboard.KeyCode.from_char("a") or key == keyboard.Key.left: + self.hero.pos_x -= 1 + + if (pos_x, pos_y) != (self.hero.pos_x, self.hero.pos_y): + game_object: GameObject = self.board[self.hero.pos_y][self.hero.pos_x] + + if isinstance(game_object, Wall): # Если уперлись в стену + self.hero.pos_x, self.hero.pos_y = pos_x, pos_y + return + + if isinstance(game_object, Gold): + self.hero.gold += game_object.value + + self.board[self.hero.pos_y][self.hero.pos_x] = self.hero # Рисуем героя + self.board[pos_y][pos_x] = GameObject( + pos_x, pos_y + ) # Старое место стало пустым + + def draw(self) -> None: + clear() + + self.draw_board() + self.draw_game_info() + + def draw_board(self) -> None: + for row in self.board: + print("".join(game_object.icon for game_object in row)) + + def draw_game_info(self) -> None: + print( + f"Hero: position: {self.hero.pos_x}x{self.hero.pos_y}, gold: {self.hero.gold}" + ) + + def start(self, board_template: str) -> None: + self.is_active = True + self.board.clear() + self.listener.start() + + for y, lines in enumerate(board_template.splitlines()): + self.board.append([]) + + for x, value in enumerate(lines): + match value: + case "@": + self.hero.pos_x = x + self.hero.pos_y = y + game_object = self.hero + + case "G": + game_object = Gold( + pos_x=x, + pos_y=y, + value=randint(1, 100), + ) + + case "#": + game_object = Wall(pos_x=x, pos_y=y) + + case _: + game_object = GameObject( + pos_x=x, + pos_y=y, + ) + + self.board[-1].append(game_object) + + def finish(self) -> None: + self.is_active = False + self.listener.stop() + + +if __name__ == "__main__": + game = Game() + game.start(board_template=BOARD_TEMPLATE) + + while game.do_step(): + time.sleep(0.100) diff --git a/pynput__examples/global_hot_key.py b/pynput__examples/global_hot_key.py new file mode 100644 index 000000000..fe4c26a16 --- /dev/null +++ b/pynput__examples/global_hot_key.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Listener + +# pip install pyscreenshot +import pyscreenshot as ImageGrab + + +def on_release(key) -> None: + if key == Key.f7: + print("screenshot") + + # Fullscreen + im = ImageGrab.grab() + im.save("screenshot.png") + im.show() + + +with Listener(on_release=on_release) as listener: + listener.join() diff --git a/pynput__examples/hello_world.py b/pynput__examples/hello_world.py deleted file mode 100644 index fe6cadc79..000000000 --- a/pynput__examples/hello_world.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install pynput -from pynput.keyboard import Key, Listener - - -def on_press(key): - print('{0} pressed'.format(key)) - - -def on_release(key): - print('{0} release'.format(key)) - - if key == Key.esc: - # Stop listener - return False - -# Collect events until released -with Listener(on_press=on_press, on_release=on_release) as listener: - listener.join() diff --git a/pynput__examples/key_listener.py b/pynput__examples/key_listener.py new file mode 100644 index 000000000..e408ff0fa --- /dev/null +++ b/pynput__examples/key_listener.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Listener + + +def on_press(key) -> None: + print(f"{key} pressed") + + +def on_release(key) -> bool | None: + print(f"{key} release") + + if key == Key.esc: + # Stop listener + return False + + +# Collect events until released +with Listener(on_press=on_press, on_release=on_release) as listener: + listener.join() diff --git a/pynput__examples/media_stop.py b/pynput__examples/media_stop.py new file mode 100644 index 000000000..404ae32fd --- /dev/null +++ b/pynput__examples/media_stop.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Controller + + +keyboard = Controller() + +print("Attempting to stop media playback using pynput...") + +keyboard.press(Key.media_stop) +keyboard.release(Key.media_stop) + +print("Media stop command sent via pynput.") diff --git a/pynput__examples/pynput__global_hot_key.py b/pynput__examples/pynput__global_hot_key.py deleted file mode 100644 index 75b02f25f..000000000 --- a/pynput__examples/pynput__global_hot_key.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install pynput -from pynput.keyboard import Key, Listener - - -def on_release(key): - if key == Key.f7: - print('screenshot') - - # Fullscreen - import pyscreenshot as ImageGrab - im = ImageGrab.grab() - im.save('screenshot.png') - im.show() - - -with Listener(on_release=on_release) as listener: - listener.join() diff --git a/pynput__examples/pynput__key_listener.py b/pynput__examples/pynput__key_listener.py deleted file mode 100644 index 5f9972ea2..000000000 --- a/pynput__examples/pynput__key_listener.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def on_press(key): - print('on_press', str(key), key, type(key)) - - -def on_release(key): - print('on_release', str(key), key, type(key)) - - -# pip install pynput -from pynput import keyboard -with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: - listener.join() diff --git a/pyqrcode__examples/hello_world.py b/pyqrcode__examples/hello_world.py deleted file mode 100644 index 317736d8e..000000000 --- a/pyqrcode__examples/hello_world.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://pythonhosted.org/PyQRCode/ - - -# pip install pyqrcode -# pip install pypng -import pyqrcode - - -qr_code = pyqrcode.create('http://uca.edu') -qr_code.png('img.png') - -qr_code = pyqrcode.create('Hello World!') -qr_code.png('img_hello.png') - -qr_code.png('img_hello__scale=6.png', scale=6) diff --git a/pyscreeze__examples/win_calc_clicker/README.md b/pyscreeze__examples/win_calc_clicker/README.md new file mode 100644 index 000000000..de669a9bf --- /dev/null +++ b/pyscreeze__examples/win_calc_clicker/README.md @@ -0,0 +1,2 @@ +## main.py +![](screenshots/screenshot.gif) diff --git a/pyscreeze__examples/win_calc_clicker/buttons/+.png b/pyscreeze__examples/win_calc_clicker/buttons/+.png new file mode 100644 index 000000000..084c9c285 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/+.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/0.png b/pyscreeze__examples/win_calc_clicker/buttons/0.png new file mode 100644 index 000000000..12e8b4f96 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/0.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/1.png b/pyscreeze__examples/win_calc_clicker/buttons/1.png new file mode 100644 index 000000000..63521ac03 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/1.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/2.png b/pyscreeze__examples/win_calc_clicker/buttons/2.png new file mode 100644 index 000000000..a4b2586f3 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/2.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/3.png b/pyscreeze__examples/win_calc_clicker/buttons/3.png new file mode 100644 index 000000000..ded0748f5 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/3.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/4.png b/pyscreeze__examples/win_calc_clicker/buttons/4.png new file mode 100644 index 000000000..87c7d85b2 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/4.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/5.png b/pyscreeze__examples/win_calc_clicker/buttons/5.png new file mode 100644 index 000000000..fc7991e11 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/5.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/6.png b/pyscreeze__examples/win_calc_clicker/buttons/6.png new file mode 100644 index 000000000..22936758e Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/6.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/7.png b/pyscreeze__examples/win_calc_clicker/buttons/7.png new file mode 100644 index 000000000..483623193 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/7.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/8.png b/pyscreeze__examples/win_calc_clicker/buttons/8.png new file mode 100644 index 000000000..188390ada Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/8.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/9.png b/pyscreeze__examples/win_calc_clicker/buttons/9.png new file mode 100644 index 000000000..ab5ba3774 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/9.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/=.png b/pyscreeze__examples/win_calc_clicker/buttons/=.png new file mode 100644 index 000000000..69088492a Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/=.png differ diff --git a/pyscreeze__examples/win_calc_clicker/main.py b/pyscreeze__examples/win_calc_clicker/main.py new file mode 100644 index 000000000..a522ead8a --- /dev/null +++ b/pyscreeze__examples/win_calc_clicker/main.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import time +import win32api +import win32con + +from pathlib import Path + +# pip install pyscreeze +import pyscreeze + + +DIR = Path(__file__).parent.resolve() +DIR_BUTTONS = DIR / "buttons" + + +def click(x, y, sleep_secs: float = 0.01) -> None: + win32api.SetCursorPos((x, y)) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) + + time.sleep(sleep_secs) + + +BUTTON_BY_POSITION = dict() + + +def init_button_positions(grayscale: bool = True) -> None: + BUTTON_BY_POSITION.clear() + + for path in DIR_BUTTONS.glob("*.png"): + button = path.stem + + position = pyscreeze.locateCenterOnScreen(str(path), grayscale=grayscale) + if not position: + print(f"Position of button {button!r} not found!") + continue + + BUTTON_BY_POSITION[button] = position + + +def go(expression: str) -> None: + init_button_positions() + + for button in expression: + if not button.strip(): + continue + + if button not in BUTTON_BY_POSITION: + print(f"Unknown button {button!r}!") + continue + + x, y = BUTTON_BY_POSITION[button] + print(f"Click on {button!r} at {x}x{y}") + + click(x, y) + + +def show_test_calc() -> None: + os.startfile("calc.exe") + time.sleep(1) + + +if __name__ == "__main__": + show_test_calc() + + expression = "1234 + 222 + 3214 + 1111 + 1234567890 + 222 = " + go(expression) diff --git a/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif b/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif new file mode 100644 index 000000000..a7c1b0f92 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif differ diff --git a/pysimplegui__examples/generation_random.py b/pysimplegui__examples/generation_random.py new file mode 100644 index 000000000..414e2973f --- /dev/null +++ b/pysimplegui__examples/generation_random.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from random import randint + +# pip install PySimpleGUI +import PySimpleGUI as sg + + +sg.theme("SystemDefault") +layout = [ + [sg.Text("Number of digits"), sg.Input(default_text="5", key="ND")], + [sg.Text("Minimum"), sg.Input(default_text="1", key="Min")], + [sg.Text("Maximum"), sg.Input(default_text="9", key="Max")], + [sg.Text(" ", key="OUT")], + [sg.Button("Generate", key="button_generate"), sg.Button("Clear")], +] + +window = sg.Window("Generator", layout) + + +def generate(number: int, min_number: int, max_number: int) -> str: + return "".join(str(randint(min_number, max_number)) for _ in range(number)) + + +while True: + event, values = window.read() + if event is None or event == sg.WIN_CLOSED: + break + + if event == "button_generate": + number, min_number, max_number = ( + int(values["ND"]), + int(values["Min"]), + int(values["Max"]), + ) + text = generate(number, min_number, max_number) + print(text) + window["OUT"].update(f"Result: {text}") + + if event == "Clear": + window.find_element("ND").update("") + window.find_element("Min").update("") + window.find_element("Max").update("") + window.find_element("OUT").update("") diff --git a/pytesseract__examples__image_to_text/GTA/main.py b/pytesseract__examples__image_to_text/GTA/main.py index 6868c0d18..54791cd2b 100644 --- a/pytesseract__examples__image_to_text/GTA/main.py +++ b/pytesseract__examples__image_to_text/GTA/main.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# pip install pillow -from PIL import Image - # pip install pytesseract # tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +# pip install pillow +from PIL import Image + + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" -file_name = 'RuFjPBg.png' +file_name = "RuFjPBg.png" img = Image.open(file_name) -text = pytesseract.image_to_string(img, lang='eng') +text = pytesseract.image_to_string(img, lang="eng") print(repr(text)) # '.\nCheat activated' diff --git a/pytesseract__examples__image_to_text/hello_world.py b/pytesseract__examples__image_to_text/hello_world.py index 4cfec19f7..722288cc7 100644 --- a/pytesseract__examples__image_to_text/hello_world.py +++ b/pytesseract__examples__image_to_text/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/madmaze/pytesseract @@ -9,20 +9,22 @@ import re -# pip install pillow -from PIL import Image - # pip install pytesseract # Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +# pip install pillow +from PIL import Image + + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # Simple image to string -img = Image.open('test.jpg') -text = pytesseract.image_to_string(img, lang='eng') +img = Image.open("test.jpg") +text = pytesseract.image_to_string(img, lang="eng") -text = re.sub(r'(\s){2,}', '\1', text) +text = re.sub(r"(\s){2,}", "\1", text) print(text) # At this Time. two Great Empires struggled # for Dominion over Ivalice:Archadia in the East. Rozarria. the West. diff --git a/pytesseract__examples__image_to_text/hello_world__with__googletrans.py b/pytesseract__examples__image_to_text/hello_world__with__googletrans.py index 2dbbb9d36..8e2085af1 100644 --- a/pytesseract__examples__image_to_text/hello_world__with__googletrans.py +++ b/pytesseract__examples__image_to_text/hello_world__with__googletrans.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/madmaze/pytesseract @@ -9,23 +9,25 @@ import re +# pip install pytesseract +# Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki +import pytesseract + # pip install googletrans from googletrans import Translator # pip install pillow from PIL import Image -# pip install pytesseract -# Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki -import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # Simple image to string -img = Image.open('test.jpg') -text = pytesseract.image_to_string(img, lang='eng') +img = Image.open("test.jpg") +text = pytesseract.image_to_string(img, lang="eng") -text = re.sub(r'(\s){2,}', '\1', text) +text = re.sub(r"(\s){2,}", "\1", text) print(text) # At this Time. two Great Empires struggled # for Dominion over Ivalice:Archadia in the East. Rozarria. the West. @@ -35,17 +37,13 @@ # tolled the Destruction of the greater # part of Dalmasca’'s f orces. -print('', '-' * 100, '', sep='\n') +print("", "-" * 100, "", sep="\n") -from_lang = 'en' -to_lang = 'ru' +from_lang = "en" +to_lang = "ru" translator = Translator() -translation = translator.translate( - text, - src=from_lang, - dest=to_lang -).text +translation = translator.translate(text, src=from_lang, dest=to_lang).text print(translation) # В это время. два Великие Империи боролись # для Dominion над Ивалисом: Archadia на Востоке. Rozarria. Запад. diff --git a/python_implementation.py b/python_implementation.py index dfa994cba..659662411 100644 --- a/python_implementation.py +++ b/python_implementation.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import platform + + print(platform.python_implementation()) +# CPython diff --git a/python_interpreter_info.py b/python_interpreter_info.py index 29363df55..8a2406dd9 100644 --- a/python_interpreter_info.py +++ b/python_interpreter_info.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import platform -print('Implementation:', platform.python_implementation()) -print('Version:', platform.python_version()) -print('Compiler:', platform.python_compiler()) - import sys -print('Executable:', sys.executable) + + +print("Implementation:", platform.python_implementation()) +print("Version:", platform.python_version()) +print("Compiler:", platform.python_compiler()) + +print("Executable:", sys.executable) diff --git a/python_object_to_json.py b/python_object_to_json.py index b0b7a9bfe..339d4b0dc 100644 --- a/python_object_to_json.py +++ b/python_object_to_json.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class SubField: - def __init__(self): + def __init__(self) -> None: self.flag = True class Field: - def __init__(self, tag1, tag2, sub_field_flag=True): + def __init__(self, tag1, tag2, sub_field_flag=True) -> None: self.tag1 = tag1 self.tag2 = tag2 @@ -20,17 +20,17 @@ def __init__(self, tag1, tag2, sub_field_flag=True): class Object: a = 0 - b = '123' + b = "123" - def __init__(self): + def __init__(self) -> None: self.c = 3 self.items = [1, 2, 3, 4] self.maps = { - 'is': True, - 'not': 0, + "is": True, + "not": 0, } - self.field = Field('abc', 'tag2') + self.field = Field("abc", "tag2") self.field_2 = Field(777, False, sub_field_flag=False) @@ -39,11 +39,11 @@ def object_to_dict(object): fields.update(object.__class__.__dict__) fields.update(object.__dict__) - fields = dict(filter(lambda x: not x[0].startswith('_'), fields.items())) + fields = dict(filter(lambda x: not x[0].startswith("_"), fields.items())) new_fields = dict() for k, v in fields.items(): - if hasattr(v, '__dict__'): + if hasattr(v, "__dict__"): v = object_to_dict(v) new_fields[k] = v @@ -51,11 +51,12 @@ def object_to_dict(object): return new_fields -if __name__ == '__main__': +if __name__ == "__main__": obj = Object() fields = object_to_dict(obj) print(fields) print() import json + print(json.dumps(fields, ensure_ascii=False, indent=4, sort_keys=True)) diff --git a/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py b/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py index e67b33ea0..c30092c25 100644 --- a/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py +++ b/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Run as administrator @@ -12,22 +12,41 @@ # pip install pythonnet import clr +from OpenHardwareMonitor import Hardware + openhardwaremonitor_hwtypes = [ - 'Mainboard', 'SuperIO', 'CPU', 'RAM', 'GpuNvidia', 'GpuAti', 'TBalancer', 'Heatmaster', 'HDD' + "Mainboard", + "SuperIO", + "CPU", + "RAM", + "GpuNvidia", + "GpuAti", + "TBalancer", + "Heatmaster", + "HDD", ] openhardwaremonitor_sensortypes = [ - 'Voltage', 'Clock', 'Temperature', 'Load', 'Fan', 'Flow', 'Control', 'Level', - 'Factor', 'Power', 'Data', 'SmallData' + "Voltage", + "Clock", + "Temperature", + "Load", + "Fan", + "Flow", + "Control", + "Level", + "Factor", + "Power", + "Data", + "SmallData", ] def initialize_openhardwaremonitor(): DIR = os.path.abspath(os.path.dirname(__file__)) - dll_file_name = DIR + R'\OpenHardwareMonitorLib.dll' + dll_file_name = DIR + R"\OpenHardwareMonitorLib.dll" clr.AddReference(dll_file_name) - from OpenHardwareMonitor import Hardware handle = Hardware.Computer() handle.MainboardEnabled = True handle.CPUEnabled = True @@ -38,7 +57,7 @@ def initialize_openhardwaremonitor(): return handle -def fetch_stats(handle): +def fetch_stats(handle) -> None: for i in handle.Hardware: i.Update() @@ -51,15 +70,17 @@ def fetch_stats(handle): parse_sensor(subsensor) -def parse_sensor(sensor): +def parse_sensor(sensor) -> None: if sensor.Value is None: return # If SensorType is Temperature - if sensor.SensorType == openhardwaremonitor_sensortypes.index('Temperature'): + if sensor.SensorType == openhardwaremonitor_sensortypes.index("Temperature"): type_name = openhardwaremonitor_hwtypes[sensor.Hardware.HardwareType] - print(f" {type_name}. {sensor.Hardware.Name!r} " - f"Temperature Sensor #{sensor.Index} {sensor.Name} - {sensor.Value}°C") + print( + f" {type_name}. {sensor.Hardware.Name!r} " + f"Temperature Sensor #{sensor.Index} {sensor.Name} - {sensor.Value}°C" + ) if __name__ == "__main__": diff --git a/pytube__examples/main.py b/pytube__examples/main.py index a71e2b421..5edc52193 100644 --- a/pytube__examples/main.py +++ b/pytube__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -10,20 +10,20 @@ from pytube import YouTube -url = 'https://www.youtube.com/watch?v=90KZnwrVMgY' +url = "https://www.youtube.com/watch?v=90KZnwrVMgY" yt = YouTube(url) -print(f'Download video {yt.title!r}: {url}') +print(f"Download video {yt.title!r}: {url}") # Атрибут progressive=True нужен, чтобы получить поток с видео и аудио в одном файле -streams = yt.streams\ - .filter(progressive=True, file_extension='mp4', resolution='720p')\ - .order_by('resolution') +streams = yt.streams.filter( + progressive=True, file_extension="mp4", resolution="720p" +).order_by("resolution") if not streams: - print('Not found mp4 with 720p!') + print("Not found mp4 with 720p!") sys.exit() video = streams[-1] -print('Stream url:', video.url) +print("Stream url:", video.url) video.download() diff --git a/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py new file mode 100644 index 000000000..c10204f9a --- /dev/null +++ b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +tz_offset_neg0300 = pytz.FixedOffset(offset=-3 * 60) +tz_utc = pytz.timezone("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +dt_moscow = dt.replace(tzinfo=tz_moscow) +print(dt_moscow) +print(dt_moscow.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+02:30 +# 2025-01-01 09:30:00 + +print() + +dt_etc_0300 = dt.replace(tzinfo=tz_etc_0300) +print(dt_etc_0300) +print(dt_etc_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_0300 = dt.replace(tzinfo=tz_offset_0300) +print(dt_offset_0300) +print(dt_offset_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_neg0300 = dt.replace(tzinfo=tz_offset_neg0300) +print(dt_offset_neg0300) +print(dt_offset_neg0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00-03:00 +# 2025-01-01 15:00:00 + +print() + +dt_utc = dt.replace(tzinfo=tz_utc) +print(dt_utc) +print(dt_utc.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+00:00 +# 2025-01-01 12:00:00 diff --git a/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py new file mode 100644 index 000000000..e5e761c72 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +tz_offset_neg0300 = timezone(offset=timedelta(minutes=-3 * 60)) +tz_utc = zoneinfo.ZoneInfo("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +dt_moscow = dt.replace(tzinfo=tz_moscow) +print(dt_moscow) +print(dt_moscow.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_etc_0300 = dt.replace(tzinfo=tz_etc_0300) +print(dt_etc_0300) +print(dt_etc_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_0300 = dt.replace(tzinfo=tz_offset_0300) +print(dt_offset_0300) +print(dt_offset_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_neg0300 = dt.replace(tzinfo=tz_offset_neg0300) +print(dt_offset_neg0300) +print(dt_offset_neg0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00-03:00 +# 2025-01-01 15:00:00 + +print() + +dt_utc = dt.replace(tzinfo=tz_utc) +print(dt_utc) +print(dt_utc.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+00:00 +# 2025-01-01 12:00:00 diff --git a/pytz_vs_zoneinfo__examples/get_offset__pytz.py b/pytz_vs_zoneinfo__examples/get_offset__pytz.py new file mode 100644 index 000000000..2bee60209 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_offset__pytz.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +print(tz_etc_0300) +# Etc/GMT-3 + +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +print(tz_offset_0300) +# pytz.FixedOffset(180) + +tz_utc = pytz.timezone("UTC") +print(tz_utc) +# UTC + +print() + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) + +print(dt.astimezone(tz_moscow).strftime("%z")) +print(dt.astimezone(tz_etc_0300).strftime("%z")) +print(dt.astimezone(tz_offset_0300).strftime("%z")) +print(dt.astimezone(tz_utc).strftime("%z")) +# +0300 +# +0300 +# +0300 +# +0000 diff --git a/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py new file mode 100644 index 000000000..7c4f3c6f3 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +print(tz_etc_0300) +# Etc/GMT-3 + +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +print(tz_offset_0300) +# pytz.FixedOffset(180) + +tz_utc = zoneinfo.ZoneInfo("UTC") +print(tz_utc) +# UTC + +print() + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) + +print(dt.astimezone(tz_moscow).strftime("%z")) +print(dt.astimezone(tz_etc_0300).strftime("%z")) +print(dt.astimezone(tz_offset_0300).strftime("%z")) +print(dt.astimezone(tz_utc).strftime("%z")) +# +0300 +# +0300 +# +0300 +# +0000 diff --git a/pytz_vs_zoneinfo__examples/get_timezones__pytz.py b/pytz_vs_zoneinfo__examples/get_timezones__pytz.py new file mode 100644 index 000000000..4cc985eac --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_timezones__pytz.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +timezones: list[str] = pytz.all_timezones +print(len(timezones), timezones) +# 594 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'W-SU', 'WET', 'Zulu'] + +timezones: list[str] = pytz.common_timezones +print(len(timezones), timezones) +# 439 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'US/Mountain', 'US/Pacific', 'UTC'] diff --git a/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py new file mode 100644 index 000000000..a7cc79dc6 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo + + +timezones: list[str] = sorted(zoneinfo.available_timezones()) +print(len(timezones), timezones) +# 597 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'W-SU', 'WET', 'Zulu'] diff --git a/pytz_vs_zoneinfo__examples/get_tz__pytz.py b/pytz_vs_zoneinfo__examples/get_tz__pytz.py new file mode 100644 index 000000000..18165a422 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz__pytz.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_utc = pytz.timezone("UTC") +print(tz_utc) +# UTC + +try: + tz = pytz.timezone("INVALID") + raise Exception() +except pytz.UnknownTimeZoneError as e: + print(e) +# 'INVALID' + +try: + tz = pytz.timezone(None) + raise Exception() +except pytz.UnknownTimeZoneError as e: + print(e) +# None diff --git a/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py new file mode 100644 index 000000000..c6fe4b826 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_utc = zoneinfo.ZoneInfo("UTC") +print(tz_utc) +# UTC + +try: + tz = zoneinfo.ZoneInfo("INVALID") + raise Exception() +except zoneinfo.ZoneInfoNotFoundError as e: + print(e) +# 'No time zone found with key INVALID' + +try: + tz = zoneinfo.ZoneInfo(None) + raise Exception() +except TypeError as e: + print(e) +# expected str, bytes or os.PathLike object, not NoneType diff --git a/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py b/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py new file mode 100644 index 000000000..6937b5366 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +ru_timezones: list[str] = pytz.country_timezones["ru"] +print(len(ru_timezones)) +# 26 + +for tz in ru_timezones: + print(tz) +""" +Europe/Kaliningrad +Europe/Moscow +Europe/Kirov +Europe/Volgograd +Europe/Astrakhan +Europe/Saratov +Europe/Ulyanovsk +Europe/Samara +Asia/Yekaterinburg +Asia/Omsk +Asia/Novosibirsk +Asia/Barnaul +Asia/Tomsk +Asia/Novokuznetsk +Asia/Krasnoyarsk +Asia/Irkutsk +Asia/Chita +Asia/Yakutsk +Asia/Khandyga +Asia/Vladivostok +Asia/Ust-Nera +Asia/Magadan +Asia/Sakhalin +Asia/Srednekolymsk +Asia/Kamchatka +Asia/Anadyr +""" diff --git a/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py b/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py new file mode 100644 index 000000000..f673cb92b --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from datetime import datetime, tzinfo, timezone + +# pip install pytz==2025.2 +import pytz + + +PATTERN_TZ_OFFSET = re.compile(r"(?P[+-])(?P\d{2}):?(?P\d{2})") + + +def get_tz(value: str) -> tzinfo: + m = PATTERN_TZ_OFFSET.search(value) + if not m: + raise Exception(f"Invalid time zone: {value!r}") + + sign: str = m.group("sign") + hour: int = int(m.group("hour")) + minute: int = int(m.group("minute")) + + total_minutes = hour * 60 + minute + if sign == "-": + total_minutes = -total_minutes + + return pytz.FixedOffset(offset=total_minutes) + + +if __name__ == "__main__": + now_utc = datetime.now(timezone.utc) + + test_cases = [ + (pytz.timezone("Etc/GMT-3"), "+0300"), + (pytz.FixedOffset(offset=3 * 60), "+0300"), + (get_tz("+0300"), "+0300"), + (get_tz("+03:00"), "+0300"), + (get_tz("-0300"), "-0300") + ] + + for tz, expected in test_cases: + result = now_utc.astimezone(tz).strftime("%z") + print(result) + assert result == expected diff --git a/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py new file mode 100644 index 000000000..d213e598f --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from datetime import datetime, timedelta, timezone, tzinfo + + +PATTERN_TZ_OFFSET = re.compile(r"(?P[+-])(?P\d{2}):?(?P\d{2})") + + +def get_tz(value: str) -> tzinfo: + m = PATTERN_TZ_OFFSET.search(value) + if not m: + raise Exception(f"Invalid time zone: {value!r}") + + sign: str = m.group("sign") + hour: int = int(m.group("hour")) + minute: int = int(m.group("minute")) + + total_minutes = hour * 60 + minute + if sign == "-": + total_minutes = -total_minutes + + return timezone(offset=timedelta(minutes=total_minutes)) + + +if __name__ == "__main__": + import zoneinfo + + now_utc = datetime.now(timezone.utc) + + test_cases = [ + (zoneinfo.ZoneInfo("Etc/GMT-3"), "+0300"), + (timezone(offset=timedelta(minutes=3 * 60)), "+0300"), + (get_tz("+0300"), "+0300"), + (get_tz("+03:00"), "+0300"), + (get_tz("-0300"), "-0300") + ] + + for tz, expected in test_cases: + result = now_utc.astimezone(tz).strftime("%z") + print(result) + assert result == expected diff --git a/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py b/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py new file mode 100644 index 000000000..b62df7409 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +tz_utc = pytz.timezone("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +print(tz_moscow.fromutc(dt)) +print(tz_etc_0300.fromutc(dt)) +print(tz_offset_0300.fromutc(tz_offset_0300.localize(dt))) +print(tz_utc.fromutc(tz_utc.localize(dt))) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 diff --git a/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py b/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py new file mode 100644 index 000000000..73ad0f6d1 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +tz_utc = zoneinfo.ZoneInfo("UTC") + +dt_utc = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0, tzinfo=tz_utc) +print(dt_utc) +# 2025-01-01 12:00:00+00:00 + +assert dt_utc == datetime.fromisoformat("2025-01-01 12:00:00").replace(tzinfo=dt_utc.tzinfo) +assert dt_utc == datetime.fromisoformat("2025-01-01 12:00:00").replace(tzinfo=timezone.utc) + +print() + +print(dt_utc.astimezone(tz_moscow)) +print(dt_utc.astimezone(tz_etc_0300)) +print(dt_utc.astimezone(tz_offset_0300)) +print(dt_utc.astimezone(tz_utc)) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 + +print("\n" + "-" * 10 + "\n") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +print(tz_moscow.fromutc(dt.replace(tzinfo=tz_moscow))) +print(tz_etc_0300.fromutc(dt.replace(tzinfo=tz_etc_0300))) +print(tz_offset_0300.fromutc(dt.replace(tzinfo=tz_offset_0300))) +print(tz_utc.fromutc(dt.replace(tzinfo=tz_utc))) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 diff --git a/qbittorrent_examples/about_version.py b/qbittorrent_examples/about_version.py index 2fe3c6757..466279d99 100644 --- a/qbittorrent_examples/about_version.py +++ b/qbittorrent_examples/about_version.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client + qb = get_client() -print('Get qBittorrent version:', qb.qbittorrent_version) -print('Get WEB API version:', qb.api_version) +print("Get qBittorrent version:", qb.qbittorrent_version) +print("Get WEB API version:", qb.api_version) diff --git a/qbittorrent_examples/check_changes_in_torrent.py b/qbittorrent_examples/check_changes_in_torrent.py index a62124426..d6b80645e 100644 --- a/qbittorrent_examples/check_changes_in_torrent.py +++ b/qbittorrent_examples/check_changes_in_torrent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,6 +10,21 @@ """ +import time +import traceback +import re + +from collections import defaultdict +from datetime import datetime + +import requests +import effbot_bencode + +from bs4 import BeautifulSoup + +# pip install simple-wait +from simple_wait import wait + def get_rutor_torrent_download_info(torrent_url): """ @@ -17,27 +32,23 @@ def get_rutor_torrent_download_info(torrent_url): """ - import requests rs = requests.get(torrent_url) + root = BeautifulSoup(rs.content, "lxml") - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - magnet_url = root.select_one('#download > a[href^="magnet"]')['href'] + magnet_url = root.select_one('#download > a[href^="magnet"]')["href"] # For get info hash from magnet url - import re - match = re.compile(r'btih:([abcdef\d]+?)&', flags=re.IGNORECASE).search(magnet_url) + match = re.compile(r"btih:([abcdef\d]+?)&", flags=re.IGNORECASE).search(magnet_url) if match: info_hash = match.group(1) - return torrent_url.replace('/torrent/', '/download/'), magnet_url, info_hash - + return torrent_url.replace("/torrent/", "/download/"), magnet_url, info_hash -def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): - info_hash_by_name_dict = {torrent['hash']: torrent['name'] for torrent in qb.torrents()} - from collections import defaultdict +def remove_previous_torrent_from_qbittorrent(qb, new_info_hash) -> None: + info_hash_by_name_dict = { + torrent["hash"]: torrent["name"] for torrent in qb.torrents() + } name_by_info_hash_list_dict = defaultdict(list) for info_hash, name in info_hash_by_name_dict.items(): @@ -56,60 +67,61 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): # Remove previous torrents if info_hash_list: - print('Удаление предыдущих раздач этого торрента: {}'.format(info_hash_list)) + print( + f"Удаление предыдущих раздач этого торрента: {info_hash_list}" + ) qb.delete(info_hash_list) else: print("Предыдущие закачки не найдены") - -# Import https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py -import sys -sys.path.append('../wait') - -from wait import wait - -if __name__ == '__main__': +if __name__ == "__main__": from config import API_ID, TO from common import get_client qb = get_client() - torrent_url = 'http://anti-tor.org/torrent/544942' + torrent_url = "http://anti-tor.org/torrent/544942" last_info_hash = None last_torrent_files = list() while True: try: - from datetime import datetime today = datetime.today() - torrent_file_url, _, info_hash = get_rutor_torrent_download_info(torrent_url) - print('{}: Проверка {}: {} / {}'.format(today, torrent_url, torrent_file_url, info_hash)) + torrent_file_url, _, info_hash = get_rutor_torrent_download_info( + torrent_url + ) + print( + f"{today}: Проверка {torrent_url}: {torrent_file_url} / {info_hash}" + ) if qb.get_torrent(info_hash): - print('Торрент {} уже есть в списке раздачи'.format(info_hash)) + print(f"Торрент {info_hash} уже есть в списке раздачи") else: if info_hash != last_info_hash: - import requests - data = requests.get(torrent_file_url).content.decode('latin1') - - import effbot_bencode + data = requests.get(torrent_file_url).content.decode("latin1") torrent = effbot_bencode.decode(data) - files = ["/".join(file["path"]) for file in torrent["info"]["files"]] + files = [ + "/".join(file["path"]) for file in torrent["info"]["files"] + ] # Использование множеств, чтобы узнать разницу списков, т.е. какие файлы были добавлены # А чтобы узнать какие были удалены: list(set(last_torrent_files) - set(files)) new_files = list(set(files) - set(last_torrent_files)) if last_info_hash is None: - print("Добавление торрента с {} файлами: {}".format(len(new_files), new_files)) + print( + f"Добавление торрента с {len(new_files)} файлами: {new_files}" + ) else: - print('Торрент изменился, пора его перекачивать') - print("Добавлено {} файлов: {}".format(len(new_files), new_files)) + print("Торрент изменился, пора его перекачивать") + print( + f"Добавлено {len(new_files)} файлов: {new_files}" + ) last_info_hash = info_hash last_torrent_files = files @@ -119,22 +131,21 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): qb.download_from_link(torrent_file_url) # Отправляю смс на номер - url = 'https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}'.format( + url = "https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}".format( api_id=API_ID, to=TO, - text="Вышла новая серия '{}'".format(torrent['info']['name']) + text=f"Вышла новая серия '{torrent['info']['name']}'", ) rs = requests.get(url) print(rs.text) # Даем 5 секунд на добавление торрента в клиент - import time time.sleep(5) remove_previous_torrent_from_qbittorrent(qb, info_hash) else: - print('Изменений нет') + print("Изменений нет") print() @@ -142,12 +153,10 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): wait(hours=3) except Exception: - import traceback - print('Ошибка:') + print("Ошибка:") print(traceback.format_exc()) - print('Через 1 минуту попробую снова...') + print("Через 1 минуту попробую снова...") - # Wait 1 minute before next attempt - import time - time.sleep(60) + # Wait before next attempt + wait(minutes=1) diff --git a/qbittorrent_examples/common.py b/qbittorrent_examples/common.py index c42d95336..6691e6592 100644 --- a/qbittorrent_examples/common.py +++ b/qbittorrent_examples/common.py @@ -1,31 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List, Dict, Union - +# pip install humanize +from humanize import naturalsize as sizeof_fmt # pip install tabulate from tabulate import tabulate # pip install python-qbittorrent from qbittorrent import Client -from config import IP_HOST, USER, PASSWORD - -def sizeof_fmt(num: Union[int, float]) -> str: - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - - num /= 1024.0 - - return "%3.1f %s" % (num, 'TB') +from config import IP_HOST, USER, PASSWORD -def print_table(rows: List[List[str]], headers: List[str], show_index=True): +def print_table(rows: list[list[str]], headers: list[str], show_index=True) -> None: if show_index: show_index = range(1, len(rows) + 1) @@ -33,23 +24,28 @@ def print_table(rows: List[List[str]], headers: List[str], show_index=True): print(text) -def print_files_table(files: List[Dict]): - rows = [(file['name'], sizeof_fmt(file['size'])) for file in sorted(files, key=lambda x: x['name'])] - headers = ['#', 'File Name', 'Size'] +def print_files_table(files: list[dict]) -> None: + rows = [ + (file["name"], sizeof_fmt(file["size"])) + for file in sorted(files, key=lambda x: x["name"]) + ] + headers = ["#", "File Name", "Size"] print_table(rows, headers) -def print_torrents(torrents: List[Dict]): +def print_torrents(torrents: list[dict]) -> None: total_size = 0 for i, torrent in enumerate(torrents, 1): - torrent_size = torrent['total_size'] + torrent_size = torrent["total_size"] total_size += torrent_size print(f"{i:3}. {torrent['name']} ({sizeof_fmt(torrent_size)})") print() - print(f'Total torrents: {len(torrents)}, total size: {sizeof_fmt(total_size)} ({total_size} bytes)') + print( + f"Total torrents: {len(torrents)}, total size: {sizeof_fmt(total_size)} ({total_size} bytes)" + ) def get_client() -> Client: diff --git a/qbittorrent_examples/config.py b/qbittorrent_examples/config.py index 9d2ee95d9..657fe9bde 100644 --- a/qbittorrent_examples/config.py +++ b/qbittorrent_examples/config.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -IP_HOST = 'http://127.0.0.1:8080/' -USER = 'admin' -PASSWORD = '' +IP_HOST = "http://127.0.0.1:8080/" +USER = "admin" +PASSWORD = "" # Для отправки смс -API_ID = '' -TO = '' +API_ID = "" +TO = "" diff --git a/qbittorrent_examples/effbot_bencode.py b/qbittorrent_examples/effbot_bencode.py index b6cea7858..3777e710b 100644 --- a/qbittorrent_examples/effbot_bencode.py +++ b/qbittorrent_examples/effbot_bencode.py @@ -17,7 +17,7 @@ def tokenize(text, match=re.compile("([idel])|(\d+):|(-?\d+)").match): i = m.end() if m.lastindex == 2: yield "s" - yield text[i:i+int(s)] + yield text[i : i + int(s)] i = i + int(s) else: yield s @@ -50,7 +50,7 @@ def decode(text): try: src = tokenize(text) data = decode_item(src.__next__, next(src)) - for token in src: # look for more tokens + for token in src: # look for more tokens raise SyntaxError("trailing junk") except (AttributeError, ValueError, StopIteration): raise SyntaxError("syntax error") diff --git a/qbittorrent_examples/get_info_random_torrent.py b/qbittorrent_examples/get_info_random_torrent.py index 5a43b603b..295f6b9f0 100644 --- a/qbittorrent_examples/get_info_random_torrent.py +++ b/qbittorrent_examples/get_info_random_torrent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -13,7 +13,7 @@ print(f"{torrent['name']} ({sizeof_fmt(torrent['total_size'])})") print() -files = qb.get_torrent_files(torrent['hash']) -print(f'Files ({len(files)}):') +files = qb.get_torrent_files(torrent["hash"]) +print(f"Files ({len(files)}):") print_files_table(files) diff --git a/qbittorrent_examples/get_top5_torrent_by_size.py b/qbittorrent_examples/get_top5_torrent_by_size.py index a4468e863..5b89fb8d7 100644 --- a/qbittorrent_examples/get_top5_torrent_by_size.py +++ b/qbittorrent_examples/get_top5_torrent_by_size.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client @@ -11,15 +11,19 @@ torrents = qb.torrents() -torrents_max_top5 = sorted(torrents, key=lambda x: x['total_size'], reverse=True)[:5] -torrents_min_top5 = sorted(torrents, key=lambda x: x['total_size'])[:5] +torrents_max_top5 = sorted(torrents, key=lambda x: x["total_size"], reverse=True)[:5] +torrents_min_top5 = sorted(torrents, key=lambda x: x["total_size"])[:5] -print('Max top5:') +print("Max top5:") for i, torrent in enumerate(torrents_max_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) print() -print('Min top5:') +print("Min top5:") for i, torrent in enumerate(torrents_min_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) diff --git a/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py b/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py index 44fef4851..c47a2f3aa 100644 --- a/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py +++ b/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client @@ -9,15 +9,19 @@ qb = get_client() -torrents_max_top5 = qb.torrents(sort='size', reverse='true', limit='5') -torrents_min_top5 = qb.torrents(sort='size', limit='5') +torrents_max_top5 = qb.torrents(sort="size", reverse="true", limit="5") +torrents_min_top5 = qb.torrents(sort="size", limit="5") -print('Max top5:') +print("Max top5:") for i, torrent in enumerate(torrents_max_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) print() -print('Min top5:') +print("Min top5:") for i, torrent in enumerate(torrents_min_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) diff --git a/qbittorrent_examples/get_top_torrent_by_files_number.py b/qbittorrent_examples/get_top_torrent_by_files_number.py index 93481442e..6b7963568 100644 --- a/qbittorrent_examples/get_top_torrent_by_files_number.py +++ b/qbittorrent_examples/get_top_torrent_by_files_number.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client, print_files_table @@ -10,12 +10,12 @@ qb = get_client() torrents = qb.torrents() -torrent = max(torrents, key=lambda x: len(qb.get_torrent_files(x['hash']))) +torrent = max(torrents, key=lambda x: len(qb.get_torrent_files(x["hash"]))) -print('{} ({})'.format(torrent['name'], sizeof_fmt(torrent['total_size']))) +print(f"{torrent['name']} ({sizeof_fmt(torrent['total_size'])})") print() -files = qb.get_torrent_files(torrent['hash']) -print('Files ({}):'.format(len(files))) +files = qb.get_torrent_files(torrent["hash"]) +print(f"Files ({len(files)}):") print_files_table(files) diff --git a/qbittorrent_examples/print_current__save_path__of_torrents.py b/qbittorrent_examples/print_current__save_path__of_torrents.py index 08103cda9..cb134c5f3 100644 --- a/qbittorrent_examples/print_current__save_path__of_torrents.py +++ b/qbittorrent_examples/print_current__save_path__of_torrents.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from collections import Counter @@ -10,10 +10,10 @@ qb = get_client() -items = [torrent['save_path'] for torrent in qb.torrents()] +items = [torrent["save_path"] for torrent in qb.torrents()] counter = Counter(items) -print('Save path:', sorted(counter.keys())) +print("Save path:", sorted(counter.keys())) for name, number in counter.items(): - print(f'{name!r}: {number}') + print(f"{name!r}: {number}") diff --git a/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py b/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py index e12257a8e..79b7fc0ca 100644 --- a/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py +++ b/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qBittorrent/blob/ccd8f3e0f1447c0f88fedb975b52e2691722d6e9/src/base/bittorrent/torrenthandle.cpp#L302 @@ -10,6 +10,7 @@ from glob import glob, escape as glob_escape from os.path import join, normpath, getsize, isfile from os import listdir + from common import get_client, sizeof_fmt @@ -17,9 +18,9 @@ save_path_current_paths = [] -for save_path in set(torrent['save_path'] for torrent in qb.torrents()): +for save_path in set(torrent["save_path"] for torrent in qb.torrents()): for file_name in listdir(save_path): - if file_name == '.unwanted': + if file_name == ".unwanted": continue path = normpath(join(save_path, file_name)) @@ -29,20 +30,20 @@ torrent_content_path_list = [] -for torrent in qb.torrents(sort='name'): +for torrent in qb.torrents(sort="name"): # Нужны только те торренты, что скачаны (хоть частично) - if torrent['downloaded'] == 0: + if torrent["downloaded"] == 0: continue - files = qb.get_torrent_files(torrent['hash']) + files = qb.get_torrent_files(torrent["hash"]) - save_path = torrent['save_path'] - first_file_path = files[0]['name'] + save_path = torrent["save_path"] + first_file_path = files[0]["name"] # Найдем первый разделите папки - index = first_file_path.find('/') + index = first_file_path.find("/") if index == -1: - index = first_file_path.find('\\') + index = first_file_path.find("\\") if index != -1: # Путь до папки торрента @@ -67,12 +68,12 @@ size = getsize(path) else: # Найдем все файлы в папке и подпапках и подсчитаем их суммарный размер - sub_files = filter(isfile, glob(glob_escape(path) + '/**', recursive=True)) + sub_files = filter(isfile, glob(glob_escape(path) + "/**", recursive=True)) size = sum(getsize(file_name) for file_name in sub_files) total_size += size - print(f'{path} ({size} / {sizeof_fmt(size)})') + print(f"{path} ({size} / {sizeof_fmt(size)})") print() -print(f'Total size: {total_size} / {sizeof_fmt(total_size)}') +print(f"Total size: {total_size} / {sizeof_fmt(total_size)}") diff --git a/qbittorrent_examples/print_torrent_comment.py b/qbittorrent_examples/print_torrent_comment.py index ffa968c26..2bae74324 100644 --- a/qbittorrent_examples/print_torrent_comment.py +++ b/qbittorrent_examples/print_torrent_comment.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client def get_comment(torrent): - return qb.get_torrent(torrent['hash'])['comment'] + return qb.get_torrent(torrent["hash"])["comment"] qb = get_client() @@ -21,4 +21,4 @@ def get_comment(torrent): comment_list.append(comment) comment_list.sort() -print('\n'.join(comment_list)) +print("\n".join(comment_list)) diff --git a/qbittorrent_examples/print_torrent_created_by.py b/qbittorrent_examples/print_torrent_created_by.py index db6b0ab77..8a724f973 100644 --- a/qbittorrent_examples/print_torrent_created_by.py +++ b/qbittorrent_examples/print_torrent_created_by.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client @@ -12,8 +12,8 @@ created_by_list = set() for torrent in qb.torrents(): - created_by = qb.get_torrent(torrent['hash'])['created_by'] + created_by = qb.get_torrent(torrent["hash"])["created_by"] if created_by: created_by_list.add(created_by) -print('\n'.join(created_by_list)) +print("\n".join(created_by_list)) diff --git a/qbittorrent_examples/print_torrents_by_specific_Disc.py b/qbittorrent_examples/print_torrents_by_specific_Disc.py index 7a9a75d46..3f687bb13 100644 --- a/qbittorrent_examples/print_torrents_by_specific_Disc.py +++ b/qbittorrent_examples/print_torrents_by_specific_Disc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, print_torrents @@ -9,5 +9,5 @@ qb = get_client() need_disc = "E:" -torrents = [x for x in qb.torrents() if x['save_path'].startswith(need_disc)] +torrents = [x for x in qb.torrents() if x["save_path"].startswith(need_disc)] print_torrents(torrents) diff --git a/qbittorrent_examples/print_total_size.py b/qbittorrent_examples/print_total_size.py index db0d9e616..e92d04f41 100644 --- a/qbittorrent_examples/print_total_size.py +++ b/qbittorrent_examples/print_total_size.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, sizeof_fmt @@ -10,7 +10,7 @@ qb = get_client() torrents = qb.torrents() -total_size = sum(torrent['total_size'] for torrent in torrents) +total_size = sum(torrent["total_size"] for torrent in torrents) -print(f'Total torrents: {len(torrents)}') -print(f'Total size: {sizeof_fmt(total_size)} ({total_size} bytes)') +print(f"Total torrents: {len(torrents)}") +print(f"Total size: {sizeof_fmt(total_size)} ({total_size} bytes)") diff --git a/qbittorrent_examples/send_api.py b/qbittorrent_examples/send_api.py index e41ab8017..43f354e4d 100644 --- a/qbittorrent_examples/send_api.py +++ b/qbittorrent_examples/send_api.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('http://127.0.0.1:8080/api/v2/torrents/info') + + +rs = requests.get("http://127.0.0.1:8080/api/v2/torrents/info") print(rs) print(rs.text) diff --git a/qbittorrent_examples/shutdown.py b/qbittorrent_examples/shutdown.py index b7019406b..ff659ad6d 100644 --- a/qbittorrent_examples/shutdown.py +++ b/qbittorrent_examples/shutdown.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client + qb = get_client() qb.shutdown() diff --git a/qbittorrent_examples/torrent_list.py b/qbittorrent_examples/torrent_list.py index f2eb9f9a5..4f9b1b8a9 100644 --- a/qbittorrent_examples/torrent_list.py +++ b/qbittorrent_examples/torrent_list.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list diff --git a/qbittorrent_examples/torrent_list__sort_by_added_on.py b/qbittorrent_examples/torrent_list__sort_by_added_on.py index 842c6e810..d6ffe6d7b 100644 --- a/qbittorrent_examples/torrent_list__sort_by_added_on.py +++ b/qbittorrent_examples/torrent_list__sort_by_added_on.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(sort='added_on', reverse='true') +torrents = qb.torrents(sort="added_on", reverse="true") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__sort_by_name.py b/qbittorrent_examples/torrent_list__sort_by_name.py index f3cb95e0a..34aeeabaa 100644 --- a/qbittorrent_examples/torrent_list__sort_by_name.py +++ b/qbittorrent_examples/torrent_list__sort_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -12,7 +12,7 @@ qb = get_client() -torrents = qb.torrents(sort='name') +torrents = qb.torrents(sort="name") # Desc / Reverse: # torrents = qb.torrents(sort='name', reverse='true') diff --git a/qbittorrent_examples/torrent_list__table_gui_pyqt.py b/qbittorrent_examples/torrent_list__table_gui_pyqt.py index 0e6c3b35a..9b0c01f7b 100644 --- a/qbittorrent_examples/torrent_list__table_gui_pyqt.py +++ b/qbittorrent_examples/torrent_list__table_gui_pyqt.py @@ -1,33 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list -from common import get_client - from PyQt5.Qt import * +from common import get_client + class TorrentInfoWidget(QTableWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionBehavior(QTableWidget.SelectRows) self.setSelectionMode(QTableWidget.SingleSelection) - headers = ['KEY', 'VALUE'] + headers = ["KEY", "VALUE"] self.setColumnCount(len(headers)) self.setHorizontalHeaderLabels(headers) self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) - def fill(self, torrent_details: dict): + def fill(self, torrent_details: dict) -> None: while self.rowCount(): self.removeRow(0) @@ -40,7 +40,7 @@ def fill(self, torrent_details: dict): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.table_torrent = QTableWidget() @@ -57,7 +57,7 @@ def __init__(self): self.setCentralWidget(splitter) - def fill_table(self): + def fill_table(self) -> None: qb = get_client() torrents = qb.torrents() if not torrents: @@ -80,7 +80,7 @@ def fill_table(self): self.table_torrent.setItem(row, column, item) - def fill_torrent_info(self): + def fill_torrent_info(self) -> None: items = self.table_torrent.selectedItems() if not items: return @@ -88,12 +88,12 @@ def fill_torrent_info(self): torrent = items[0].data(Qt.UserRole) qb = get_client() - torrent_details = qb.get_torrent(torrent['hash']) + torrent_details = qb.get_torrent(torrent["hash"]) self.torrent_info_widget.fill(torrent_details) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qbittorrent_examples/torrent_list__with_filter.py b/qbittorrent_examples/torrent_list__with_filter.py index b11e888ea..09bc98b9d 100644 --- a/qbittorrent_examples/torrent_list__with_filter.py +++ b/qbittorrent_examples/torrent_list__with_filter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(filter='downloading') +torrents = qb.torrents(filter="downloading") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__with_filter_by_name.py b/qbittorrent_examples/torrent_list__with_filter_by_name.py index 6d527f511..2ac0e4b4c 100644 --- a/qbittorrent_examples/torrent_list__with_filter_by_name.py +++ b/qbittorrent_examples/torrent_list__with_filter_by_name.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, print_torrents -def get_torrents(qb, search_name='', **filters) -> list: +def get_torrents(qb, search_name="", **filters) -> list: def match(name: str) -> bool: return search_name.lower() in name.lower() - return [torrent for torrent in qb.torrents(**filters) if match(torrent['name'])] + return [torrent for torrent in qb.torrents(**filters) if match(torrent["name"])] qb = get_client() -torrents = get_torrents(qb, search_name='.mkv') +torrents = get_torrents(qb, search_name=".mkv") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py b/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py index ef7e2b087..f8c036311 100644 --- a/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py +++ b/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(filter='downloading', sort='added_on', reverse='true') +torrents = qb.torrents(filter="downloading", sort="added_on", reverse="true") print_torrents(torrents) diff --git a/pyqrcode__examples/db_sqlite3.py b/qrcode/pyqrcode__examples/db_sqlite3.py similarity index 80% rename from pyqrcode__examples/db_sqlite3.py rename to qrcode/pyqrcode__examples/db_sqlite3.py index 3a714ca4c..806d1bf7e 100644 --- a/pyqrcode__examples/db_sqlite3.py +++ b/qrcode/pyqrcode__examples/db_sqlite3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sqlite3 @@ -16,7 +16,7 @@ print(items) # ('Version: 3.29.0', '2020-07-20') -text = '\n'.join(items) +text = "\n".join(items) qr_code = pyqrcode.create(text) -qr_code.png('sqlite3.png', scale=6) +qr_code.png("sqlite3.png", scale=6) diff --git a/qrcode/pyqrcode__examples/hello_world.py b/qrcode/pyqrcode__examples/hello_world.py new file mode 100644 index 000000000..d2e5aee67 --- /dev/null +++ b/qrcode/pyqrcode__examples/hello_world.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://pythonhosted.org/PyQRCode/ + + +# pip install pyqrcode +# pip install pypng +import pyqrcode + + +qr_code = pyqrcode.create("http://uca.edu") +qr_code.png("img.png") + +qr_code = pyqrcode.create("Hello World!") +qr_code.png("img_hello.png") + +qr_code.png("img_hello__scale=6.png", scale=6) diff --git a/qrcode__generate/README.md b/qrcode/qrcode__generate/README.md similarity index 100% rename from qrcode__generate/README.md rename to qrcode/qrcode__generate/README.md diff --git a/qrcode/qrcode__generate/main.py b/qrcode/qrcode__generate/main.py new file mode 100644 index 000000000..1d98b762e --- /dev/null +++ b/qrcode/qrcode__generate/main.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/lincolnloop/python-qrcode + + +# In memory +import io + +# pip install qrcode +import qrcode + + +text = "Hello World!" + +img = qrcode.make(text) +io_data = io.BytesIO() +img.save(io_data, "png") +data = io_data.getvalue() +print(data) + +# In file +img.save("qr_code_1.png") +# +# In file, version 2: +with open("qr_code_2.png", "wb") as f: + f.write(data) diff --git a/qrcode__generate/qr_code_1.png b/qrcode/qrcode__generate/qr_code_1.png similarity index 100% rename from qrcode__generate/qr_code_1.png rename to qrcode/qrcode__generate/qr_code_1.png diff --git a/qrcode__generate/qr_code_2.png b/qrcode/qrcode__generate/qr_code_2.png similarity index 100% rename from qrcode__generate/qr_code_2.png rename to qrcode/qrcode__generate/qr_code_2.png diff --git a/qrcode/qrcode__reader/main.py b/qrcode/qrcode__reader/main.py new file mode 100644 index 000000000..dc67f7e7c --- /dev/null +++ b/qrcode/qrcode__reader/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import io +from urllib.request import urlopen + +import requests + +# SOURCE: https://github.com/tomerfiliba/reedsolomon/blob/master/reedsolo.py +# pip install reedsolo +# +# pip install git+https://github.com/ewino/qreader.git +import qreader + + +# FROM FILE +data = qreader.read("../qrcode__generate/qr_code_1.png") +print(data) # Hello World! + + +# FROM URL +url = "https://upload.wikimedia.org/wikipedia/commons/8/8f/Qr-2.png" +data = qreader.read(urlopen(url)) +print(data) # "Version 2" + +url = "https://upload.wikimedia.org/wikipedia/commons/e/eb/QR-%D0%BA%D0%BE%D0%B4.png" +data = qreader.read(urlopen(url)) +print(data) # http://ru.wikipedia.org/wiki/QR_Code + + +# FROM URL as bytes in file-like object +rs = requests.get(url) +image_data = rs.content + +bytes_io = io.BytesIO(image_data) + +data = qreader.read(bytes_io) +print(data) # http://ru.wikipedia.org/wiki/QR_Code diff --git a/qrcode__reader/reedsolo.py b/qrcode/qrcode__reader/reedsolo.py similarity index 100% rename from qrcode__reader/reedsolo.py rename to qrcode/qrcode__reader/reedsolo.py diff --git a/qrcode__generate/main.py b/qrcode__generate/main.py deleted file mode 100644 index bb224e762..000000000 --- a/qrcode__generate/main.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://github.com/lincolnloop/python-qrcode - - -text = 'Hello World!' - -# pip install qrcode -import qrcode -img = qrcode.make(text) - -# In memory -import io -io_data = io.BytesIO() -img.save(io_data, 'png') -data = io_data.getvalue() -print(data) - -# In file -img.save('qr_code_1.png') -# -# In file, version 2: -with open('qr_code_2.png', 'wb') as f: - f.write(data) diff --git a/qrcode__reader/main.py b/qrcode__reader/main.py deleted file mode 100644 index f02951d38..000000000 --- a/qrcode__reader/main.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install git+https://github.com/ewino/qreader.git - -# SOURCE: https://github.com/tomerfiliba/reedsolomon/blob/master/reedsolo.py -# pip install reedsolo -import qreader - - -# FROM FILE -data = qreader.read('../qrcode__generate/qr_code_1.png') -print(data) # Hello World! - - -# FROM URL -url = 'https://upload.wikimedia.org/wikipedia/commons/8/8f/Qr-2.png' -from urllib.request import urlopen -data = qreader.read(urlopen(url)) -print(data) # "Version 2" - -url = 'https://upload.wikimedia.org/wikipedia/commons/e/eb/QR-%D0%BA%D0%BE%D0%B4.png' -data = qreader.read(urlopen(url)) -print(data) # http://ru.wikipedia.org/wiki/QR_Code - - -# FROM URL as bytes in file-like object -import requests -rs = requests.get(url) -image_data = rs.content - -import io -bytes_io = io.BytesIO(image_data) - -data = qreader.read(bytes_io) -print(data) # http://ru.wikipedia.org/wiki/QR_Code diff --git a/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py b/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py index cd54ca99c..ccb4e9403 100644 --- a/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py +++ b/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py @@ -2,14 +2,23 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import time import random +import sys +import time +import traceback from PyQt5.QtWidgets import ( - QWidget, QMessageBox, QVBoxLayout, QApplication, QLabel, QPushButton, QScrollArea, QSizePolicy + QWidget, + QMessageBox, + QVBoxLayout, + QApplication, + QLabel, + QPushButton, + QScrollArea, + QSizePolicy, ) from PyQt5.QtGui import QPainter, QImage, QPixmap from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -18,18 +27,15 @@ # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions @@ -42,12 +48,12 @@ def seconds_to_str(seconds: int) -> str: class ImgThread(QThread): new_img = pyqtSignal(QImage) - def __init__(self, life_time_seconds=60): + def __init__(self, life_time_seconds=60) -> None: super().__init__() self.life_time_seconds = life_time_seconds - def run(self): + def run(self) -> None: painter = QPainter() painter.setRenderHint(QPainter.Antialiasing) @@ -71,7 +77,9 @@ def run(self): try: # Алгоритм изменения размера текста взят из http://stackoverflow.com/a/2204501 # Для текущего пришлось немного адаптировать - factor = img.width() / painter.fontMetrics().width(text) / 1.5 # Ширина текста 2/3 от ширины + factor = ( + img.width() / painter.fontMetrics().width(text) / 1.5 + ) # Ширина текста 2/3 от ширины if factor < 1 or factor > 1.25: f = painter.font() point_size = f.pointSizeF() * factor @@ -92,7 +100,7 @@ def run(self): class ImgWidget(QWidget): closed = pyqtSignal() - def __init__(self, life_time_seconds=60): + def __init__(self, life_time_seconds=60) -> None: super().__init__() self._thread = ImgThread(life_time_seconds) @@ -108,26 +116,26 @@ def __init__(self, life_time_seconds=60): self.setLayout(layout) - def _on_new_img(self, img: QImage): + def _on_new_img(self, img: QImage) -> None: if not img: return self.setFixedSize(img.size()) self._label.setPixmap(QPixmap.fromImage(img)) - def closeEvent(self, event): + def closeEvent(self, event) -> None: self.closed.emit() super().closeEvent(event) class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle(__file__.split('/')[-1]) + self.setWindowTitle(__file__.split("/")[-1]) - button = QPushButton('Add') + button = QPushButton("Add") button.clicked.connect(self._on_push_add) self.items_layout = FlowLayout() @@ -144,14 +152,14 @@ def __init__(self): self.setLayout(main_layout) - def _on_push_add(self): + def _on_push_add(self) -> None: w = ImgWidget(life_time_seconds=random.randint(10, 100)) w.closed.connect(lambda w=w: self.items_layout.removeWidget(w)) self.items_layout.addWidget(w) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py b/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py index e53204811..17fd1fad0 100644 --- a/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py +++ b/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py @@ -1,30 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + from PyQt5.QtCore import QFile, QStringListModel, Qt from PyQt5.QtGui import QCursor, QKeySequence, QTextCursor -from PyQt5.QtWidgets import QApplication, QCompleter, QMainWindow, QTextEdit, QMessageBox +from PyQt5.QtWidgets import ( + QApplication, + QCompleter, + QMainWindow, + QTextEdit, + QMessageBox, +) -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class CompletingTextEdit(QTextEdit): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.completerSequence = QKeySequence("Ctrl+Space") @@ -33,7 +40,7 @@ def __init__(self, parent=None): self._completer.setWidget(self) self._completer.activated.connect(self.insertCompletion) - def insertCompletion(self, completion): + def insertCompletion(self, completion) -> None: if self._completer.widget() is not self: return @@ -50,7 +57,7 @@ def textUnderCursor(self): return tc.selectedText() - def loadFromFile(self, fileName): + def loadFromFile(self, fileName) -> None: f = QFile(fileName) if not f.open(QFile.ReadOnly): model = QStringListModel() @@ -63,7 +70,7 @@ def loadFromFile(self, fileName): line = f.readLine().trimmed() if line.length() != 0: try: - line = str(line, encoding='utf-8') + line = str(line, encoding="utf-8") except TypeError: line = str(line) @@ -74,14 +81,20 @@ def loadFromFile(self, fileName): model = QStringListModel(words) self._completer.setModel(model) - def loadFromList(self, words): + def loadFromList(self, words) -> None: model = QStringListModel(words) self._completer.setModel(model) - def keyPressEvent(self, e): + def keyPressEvent(self, e) -> None: if self._completer.popup().isVisible(): # The following keys are forwarded by the completer to the widget. - if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): + if e.key() in ( + Qt.Key_Enter, + Qt.Key_Return, + Qt.Key_Escape, + Qt.Key_Tab, + Qt.Key_Backtab, + ): e.ignore() # Let the completer do default behavior. return @@ -102,7 +115,12 @@ def keyPressEvent(self, e): hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift completionPrefix = self.textUnderCursor() - if not isShortcut and (hasModifier or not e.text() or len(completionPrefix) < 3 or e.text()[-1] in eow): + if not isShortcut and ( + hasModifier + or not e.text() + or len(completionPrefix) < 3 + or e.text()[-1] in eow + ): self._completer.popup().hide() return @@ -113,33 +131,38 @@ def keyPressEvent(self, e): ) cr = self.cursorRect() - cr.setWidth(self._completer.popup().sizeHintForColumn(0) + - self._completer.popup().verticalScrollBar().sizeHint().width()) + cr.setWidth( + self._completer.popup().sizeHintForColumn(0) + + self._completer.popup().verticalScrollBar().sizeHint().width() + ) self._completer.complete(cr) class MainWindow(QMainWindow): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle("Completer") self.textEdit = CompletingTextEdit() - self.textEdit.loadFromList(['dog', 'cat', 'carry', 'python', 'собака', 'foobar']) + self.textEdit.loadFromList( + ["dog", "cat", "carry", "python", "собака", "foobar", "editor", "egg", "foo", "bar", "tester"] + ) # OR: # self.textEdit.loadFromFile('wordlist.txt') self.textEdit.setPlainText( "This TextEdit provides autocompletions for words that have " "more than 3 characters. You can trigger autocompletion " - "using %s\n\n" % self.textEdit.completerSequence.toString(QKeySequence.NativeText)) + "using %s\n\n" + % self.textEdit.completerSequence.toString(QKeySequence.NativeText) + ) self.setCentralWidget(self.textEdit) -if __name__ == '__main__': - import sys +if __name__ == "__main__": app = QApplication(sys.argv) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/EgoDialog.py b/qt__pyqt__pyside__pyqode/EgoDialog.py index 41aedb943..d4bbf7b9a 100644 --- a/qt__pyqt__pyside__pyqode/EgoDialog.py +++ b/qt__pyqt__pyside__pyqode/EgoDialog.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget @@ -10,7 +10,7 @@ class EgoDialog(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlag(Qt.FramelessWindowHint) @@ -29,19 +29,19 @@ def exec(title: str, text: str) -> bool: return ok == QMessageBox.Yes - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(QColor(0, 0, 0, 127)) painter.drawRect(self.rect()) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - ok = EgoDialog.exec('Question', 'Question?') + ok = EgoDialog.exec("Question", "Question?") if ok: - print('User select Ok') + print("User select Ok") else: - print('User select No') + print("User select No") # app.exec() diff --git a/qt__pyqt__pyside__pyqode/ElidedLabel.py b/qt__pyqt__pyside__pyqode/ElidedLabel.py index 277df1ceb..5b1bdf5ae 100644 --- a/qt__pyqt__pyside__pyqode/ElidedLabel.py +++ b/qt__pyqt__pyside__pyqode/ElidedLabel.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QLabel @@ -10,7 +10,12 @@ class ElidedLabel(QLabel): - def paintEvent(self, event): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.setMinimumWidth(50) + + def paintEvent(self, event) -> None: painter = QPainter(self) metrics = QFontMetrics(self.font()) @@ -19,14 +24,14 @@ def paintEvent(self, event): painter.drawText(self.rect(), self.alignment(), elided_text) -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QFont app = QApplication([]) - mw = ElidedLabel('abc123' * 100) - mw.setFont(QFont('Arial', 20)) + mw = ElidedLabel("abc123" * 100) + mw.setFont(QFont("Arial", 20)) mw.resize(100, 100) mw.show() diff --git a/qt__pyqt__pyside__pyqode/FlowLayout.py b/qt__pyqt__pyside__pyqode/FlowLayout.py index 990c2bae3..c7cf4eb52 100644 --- a/qt__pyqt__pyside__pyqode/FlowLayout.py +++ b/qt__pyqt__pyside__pyqode/FlowLayout.py @@ -76,8 +76,12 @@ def doLayout(self, rect, testOnly): for item in self.itemList: wid = item.widget() - spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal) - spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical) + spaceX = self.spacing() + wid.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal + ) + spaceY = self.spacing() + wid.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical + ) nextX = x + item.sizeHint().width() + spaceX if nextX - spaceX > rect.right() and lineHeight > 0: x = rect.x() @@ -94,13 +98,12 @@ def doLayout(self, rect, testOnly): return y + lineHeight - rect.y() -if __name__ == '__main__': +if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QWidget, QPushButton, QApplication class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() flowLayout = FlowLayout() diff --git a/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py b/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py index 160fa2955..ff9c1f653 100644 --- a/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py +++ b/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py @@ -1,8 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os +from pathlib import Path + + +DIR = Path(__file__).resolve().parent +TOKEN_FILE_NAME = DIR / "TOKEN.txt" + # SOURCE: https://developers.giphy.com/dashboard/ -GIPHY_API_KEY = '' +GIPHY_API_KEY = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() + +TEMP_DIR = DIR / "temp" +TEMP_DIR.mkdir(exist_ok=True) diff --git a/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py b/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py index 39241f60a..114f44686 100644 --- a/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py +++ b/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py @@ -1,40 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/1134473/201445 -import json import time import sys -from pathlib import Path +import traceback import requests from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, - QLineEdit, QPushButton, QLabel, QScrollArea, QWidget, QGridLayout + QApplication, + QMainWindow, + QVBoxLayout, + QHBoxLayout, + QFrame, + QMessageBox, + QLineEdit, + QPushButton, + QLabel, + QScrollArea, + QWidget, + QGridLayout, ) -from PyQt5.QtCore import QThread, pyqtSignal, Qt, QRegExp -from PyQt5.QtGui import QRegExpValidator, QMovie +from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtGui import QMovie -from config import GIPHY_API_KEY +from config import GIPHY_API_KEY, TEMP_DIR -# Absolute file name -TEMP_DIR = Path(__file__).resolve().parent / 'temp' -TEMP_DIR.mkdir(exist_ok=True) - -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -42,129 +46,124 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class SearchGifThread(QThread): - SITE_URL = 'https://api.giphy.com/v1' + SITE_URL = "https://api.giphy.com/v1" - about_add_gif = pyqtSignal(dict) + about_add_gif = pyqtSignal(dict, int, int) - def __init__(self, name_gif=None): + def __init__(self, name_gif=None) -> None: super().__init__() self.name_gif = name_gif def get_gif(self) -> dict: - url = f'{SearchGifThread.SITE_URL}/gifs/search?api_key={GIPHY_API_KEY}&q={self.name_gif}' + url = f"{self.SITE_URL}/gifs/search?api_key={GIPHY_API_KEY}&q={self.name_gif}" try: rs = requests.get(url) rs.raise_for_status() - data = json.loads(rs.content.decode('utf-8'))['data'] - if not data: - # TODO: emit 'not found' to MainWindow - data = {'error': 1} + data = rs.json()["data"] + if data: return data - return data - - except Exception as err: + except Exception as e: # TODO: emit error to MainWindow - print(err) + print(e) - return {'error': 1} + # TODO: emit 'not found' to MainWindow + return {"error": True} - def _process_gif(self, data: dict, index: int) -> dict: - url_gif = data[index]['images']['fixed_width']['url'] + def _process_gif(self, img: dict, index: int) -> dict: + url_gif = img["images"]["fixed_width"]["url"] image_rs = requests.get(url_gif) image_rs.raise_for_status() file_name = TEMP_DIR / f"img{index}.gif" - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: f.write(image_rs.content) - data = {'row': index // 3, 'col': index % 3, 'error': None, 'file_name': str(file_name)} + data = { + "row": index // 3, + "col": index % 3, + "error": None, + "file_name": str(file_name), + } return data - def run(self): + def run(self) -> None: data = self.get_gif() - if 'error' in data: + if "error" in data: # TODO: emit error to MainWindow return - for i in range(20): - data_gif = self._process_gif(data, i) - self.about_add_gif.emit(data_gif) + for i, img in enumerate(data): + data_gif = self._process_gif(img, i) + self.about_add_gif.emit(data_gif, i + 1, len(data)) time.sleep(1) class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.width = self.size().width() - self.height = self.size().height() - self.left = 200 - self.top = 300 - self.title = 'Gif Manager' + self.title = "Gif Manager" + self.setWindowTitle(self.title) self.search_gif_thread = SearchGifThread() self.search_gif_thread.started.connect(self.on_start) self.search_gif_thread.finished.connect(self.on_finish) self.search_gif_thread.about_add_gif.connect(self.add_gif) - self.init_window() self.init_ui() - self.gif_edit.setText('cat') - - def init_window(self): - self.setGeometry(self.left, self.top, self.width, self.height) - self.setWindowTitle(self.title) - - def init_ui(self): - root_layout = QVBoxLayout() - gif_data_layout = QHBoxLayout() - gif_data_layout.setAlignment(Qt.AlignTop) - self.gif_data_frame = QFrame() + def init_ui(self) -> None: self.gif_edit = QLineEdit() self.gif_edit.returnPressed.connect(self.search_gif) - regex = QRegExp('[a-z-A-Z]+') - validator = QRegExpValidator(regex) - self.gif_edit.setValidator(validator) - self.gif_edit.setPlaceholderText('Enter name gif') - gif_search_button = QPushButton('Search gif') + self.gif_edit.setPlaceholderText("Enter name gif") + + gif_search_button = QPushButton("Search gif") gif_search_button.clicked.connect(self.search_gif) + + gif_data_layout = QHBoxLayout() + gif_data_layout.setContentsMargins(0, 0, 0, 0) gif_data_layout.addWidget(self.gif_edit) gif_data_layout.addWidget(gif_search_button) + + self.gif_data_frame = QFrame() self.gif_data_frame.setLayout(gif_data_layout) - root_layout.addWidget(self.gif_data_frame) - self.info_label = QLabel('Something went wrong. Check that the request was made correctly.') + + self.info_label = QLabel( + 'Something went wrong. Check that the request was made correctly.' + ) self.info_label.setAlignment(Qt.AlignHCenter) self.info_label.hide() - root_layout.addWidget(self.info_label) + + scrollWidget = QWidget() + self.gifs_layout = QGridLayout(scrollWidget) + self.scroll = QScrollArea() - self.scroll.setStyleSheet('background: rgba(255, 255, 255, 30%);') - self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setWidgetResizable(True) - scrollWidget = QWidget() - self.gifs_layout = QGridLayout() - scrollWidget.setLayout(self.gifs_layout) self.scroll.setWidget(scrollWidget) - root_layout.addWidget(self.scroll) + root_widget = QWidget() - root_widget.setLayout(root_layout) self.setCentralWidget(root_widget) - def on_finish(self): - self.gif_data_frame.show() + root_layout = QVBoxLayout(root_widget) + root_layout.addWidget(self.gif_data_frame) + root_layout.addWidget(self.info_label) + root_layout.addWidget(self.scroll) - def on_start(self): + def on_finish(self) -> None: + self.gif_data_frame.setEnabled(True) + + def on_start(self) -> None: self.info_label.hide() self.scroll.show() - self.gif_data_frame.hide() + self.gif_data_frame.setEnabled(False) - def search_gif(self): + def search_gif(self) -> None: # TODO: replace on QListWidget for i in range(self.gifs_layout.count()): self.gifs_layout.itemAt(i).widget().deleteLater() @@ -172,9 +171,11 @@ def search_gif(self): self.search_gif_thread.name_gif = self.gif_edit.text() self.search_gif_thread.start() - def add_gif(self, data): - if data['error']: - self.gif_data_frame.show() + def add_gif(self, data: dict, num: int, total: int) -> None: + self.setWindowTitle(f"{self.title}. {num} / {total}") + + if data["error"]: + self.gif_data_frame.setEnabled(True) self.scroll.hide() self.info_label.show() return @@ -194,6 +195,9 @@ def add_gif(self, data): app = QApplication(sys.argv) mw = MainWindow() + mw.resize(800, 600) mw.show() + mw.gif_edit.setText("cat") + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py b/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py index 2af77002d..56467789b 100644 --- a/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py +++ b/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QColor diff --git a/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py b/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py new file mode 100644 index 000000000..db0860632 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import ( + QApplication, + QGraphicsView, + QGraphicsScene, + QGraphicsPixmapItem, +) +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt + + +class ImageLabel(QGraphicsView): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.setScene(QGraphicsScene()) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def setImage(self, filename: str) -> None: + self.setPixmap(QPixmap(filename)) + + def setPixmap(self, pixmap: QPixmap) -> None: + item = QGraphicsPixmapItem(pixmap) + item.setTransformationMode(Qt.SmoothTransformation) + self.scene().addItem(item) + + def resizeEvent(self, event) -> None: + super().resizeEvent(event) + + rect = self.scene().itemsBoundingRect() + self.fitInView(rect, Qt.KeepAspectRatio) + + +if __name__ == "__main__": + app = QApplication([]) + + screenshot = app.primaryScreen().grabWindow(QApplication.desktop().winId()) + + w = ImageLabel() + w.setPixmap(screenshot) + w.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py index f999c1b98..702592e7d 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QRegularExpressionValidator diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py index 21b9ebbda..a2f49e2fc 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,17 +9,21 @@ # SOURCE: https://stackoverflow.com/a/23166561/5909792 IP_RANGE = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" -IP_REGEXP = f'^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$' +IP_REGEXP = rf"^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$" def is_ip(ip: str) -> bool: return bool(re.match(IP_REGEXP, ip)) -if __name__ == '__main__': +if __name__ == "__main__": tests = [ - ('127.0.0.1', True), ('100.100.100.100', True), ('0.0.0.0', True), ('300.300.300.300', False), - ('300.0.0.1', False), ('255.255.255.255', True) + ("127.0.0.1", True), + ("100.100.100.100", True), + ("0.0.0.0", True), + ("300.300.300.300", False), + ("300.0.0.1", False), + ("255.255.255.255", True), ] for ip, expected in tests: diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py index b7c01b9a0..e8c03e338 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QLineEdit, QApplication @@ -9,7 +9,7 @@ from IP_Validator import get_ip_validator -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QLineEdit() diff --git a/qt__pyqt__pyside__pyqode/OID__QValidator.py b/qt__pyqt__pyside__pyqode/OID__QValidator.py new file mode 100644 index 000000000..cfdd10e50 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/OID__QValidator.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QLineEdit +from PyQt5.QtGui import QRegExpValidator +from PyQt5.QtCore import QRegExp + + +app = QApplication([]) + +mw = QLineEdit() +mw.setText("1.3.6.1.2.1.25.1.1.0") +mw.setValidator(QRegExpValidator(QRegExp(r"\d+(\.\d+)+"))) +mw.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py index b2c0036fe..1b5cf743b 100644 --- a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py +++ b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py @@ -1,32 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/970882/201445 # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/d50de0f237d778f7d69ee75e328503f8c8344743/qt__pyqt__pyside__pyqode/QWebEngineView__one_ball__in_outer_circle_area__collision_with_mouse/index.html +import traceback +import sys + +from dataclasses import dataclass from pathlib import Path from timeit import default_timer -from PyQt5.Qt import QApplication, QWidget, QPainter, QTimer, Qt, QPoint, QPen +from PyQt5.QtCore import QTimer, Qt, QPointF +from PyQt5.QtGui import QPainter, QPen +from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}\n" + text += "".join(traceback.format_tb(tb)) + print(text) + + if QApplication.instance(): + msg_box = QMessageBox( + QMessageBox.Critical, + "Error", + f"Error: {ex}", + parent=None, + ) + msg_box.setDetailedText(text) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + + +sys.excepthook = log_uncaught_exceptions +@dataclass class Ball: - r = 50 # Радиус шарика - x = 0 # Координата по х центра шарика - y = 0 # Координата по y центра шарика - speed = 0 # Скорость движения - dir_x = 0 # Компонент x вектора движения шарика - dir_y = 0 # Компонент y вектора движения шарика - damp = 10 # Скорость уменьшения скорости движения (сопротивление) - collision = False # Признак коллизии с внешним кругом - speed_after_collision = 300 # Скорость движения шарика после столкновения + r: float = 50 # Радиус шарика + x: float = 0 # Координата по х центра шарика + y: float = 0 # Координата по y центра шарика + speed: float = 0 # Скорость движения + dir_x: float = 0 # Компонент x вектора движения шарика + dir_y: float = 0 # Компонент y вектора движения шарика + damp: float = 10 # Скорость уменьшения скорости движения (сопротивление) + collision: bool = False # Признак коллизии с внешним кругом + speed_after_collision: float = 300 # Скорость движения шарика после столкновения # Функция, которая проверяет наличие коллизии шарика с внешним кругом - def hit_outer_circle_check(self, outer_circle: int): + def hit_outer_circle_check(self, outer_circle: int) -> None: dr = outer_circle - self.r # Разница радиусов # По теореме пифагора проверяем выход за пределы круга (коллизию) @@ -69,7 +96,7 @@ def hit_outer_circle_check(self, outer_circle: int): self.collision = False # Функция проверки коллизии шарика и мышки - def hit_mouse_check(self, x, y): + def hit_mouse_check(self, x, y) -> None: # Если есть коллизия с внешним кругом игнорируем мышку if self.collision: return @@ -93,7 +120,7 @@ def hit_mouse_check(self, x, y): # Тут осуществляется передвижение # dt - кол-во секунд с прошлого обсчета - def do_move(self, dt): + def do_move(self, dt: float) -> None: # К текущей координате прибавляем вектор скорости помноженный # на значение скорости помноженные на прошедшее время self.x += self.dir_x * self.speed * dt @@ -104,17 +131,15 @@ def do_move(self, dt): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - title = Path(__file__).parent.name - self.setWindowTitle(title) - - timeout = 1000 // 60 + timeout: int = 1000 // 60 # Таймер обновления движения и обработки столкновения шариков self.timer = QTimer() self.timer.timeout.connect(self.tick) + self.timer.timeout.connect(self.update_window_title) self.timer.start(timeout) # NOTE: Перерисовку помести в tick, но этот вариант с отдельным таймером тоже @@ -124,19 +149,26 @@ def __init__(self): # self.timer_render.timeout.connect(self.update) # self.timer_render.start(timeout) - self.outer_circle = 195 + self.outer_circle: int = 195 self.ball = Ball() - self.mouse_center_x = 0 - self.mouse_center_y = 0 + self.mouse_center_x: int = 0 + self.mouse_center_y: int = 0 # Используется, чтобы в независимости от количества вызовов # tick скорость шарика была одинаковая - self.t = 0 + self.t: float = 0 self.setMouseTracking(True) - def tick(self): + self.update_window_title() + + def update_window_title(self) -> None: + name = Path(__file__).parent.name + title = f"{name}. Speed: {self.ball.speed}" + self.setWindowTitle(title) + + def tick(self) -> None: # Считаем сколько времени прошло с прошлого обсчета dt = default_timer() - self.t @@ -148,11 +180,11 @@ def tick(self): self.update() - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: self.mouse_center_x = event.pos().x() - (self.width() / 2) self.mouse_center_y = event.pos().y() - (self.height() / 2) - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.HighQualityAntialiasing) @@ -161,11 +193,11 @@ def paintEvent(self, event): painter.translate(self.width() / 2, self.height() / 2) - painter.drawEllipse(QPoint(0, 0), self.outer_circle, self.outer_circle) - painter.drawEllipse(QPoint(self.ball.x, self.ball.y), self.ball.r, self.ball.r) + painter.drawEllipse(QPointF(0, 0), self.outer_circle, self.outer_circle) + painter.drawEllipse(QPointF(self.ball.x, self.ball.y), self.ball.r, self.ball.r) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py index 8abe15c2c..e3ef79230 100644 --- a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py +++ b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/970882/201445 @@ -13,8 +13,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView -if __name__ == '__main__': - with open('index.html', encoding='utf-8') as f: +if __name__ == "__main__": + with open("index.html", encoding="utf-8") as f: html = f.read() title = Path(__file__).parent.name @@ -27,4 +27,3 @@ view.show() app.exec() - diff --git a/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py b/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py new file mode 100644 index 000000000..ec7232233 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QGuiApplication + + +app = QApplication([]) +for screen in QApplication.screens(): + print(screen.geometry(), screen.size()) +""" +PyQt6.QtCore.QRect(0, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +PyQt6.QtCore.QRect(-3840, 0, 1536, 864) PyQt6.QtCore.QSize(1536, 864) +PyQt6.QtCore.QRect(-1920, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +""" + +app = None +print() + +app = QGuiApplication([]) +for screen in QGuiApplication.screens(): + print(screen.geometry(), screen.size()) +""" +PyQt6.QtCore.QRect(0, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +PyQt6.QtCore.QRect(-3840, 0, 1536, 864) PyQt6.QtCore.QSize(1536, 864) +PyQt6.QtCore.QRect(-1920, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +""" diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py index a8621542b..03c97bf5c 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from random import randint import sys - +from random import randint from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -14,9 +13,9 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Widget') + self.setWindowTitle("Widget") self.cell_size = 20 self.matrix_size = 9 @@ -35,7 +34,7 @@ def __init__(self): self.setMouseTracking(True) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) w, h = event.size().width(), event.size().height() @@ -43,7 +42,7 @@ def resizeEvent(self, event): self.cell_size = min_size // self.matrix_size - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: super().mouseMoveEvent(event) pos = event.pos() @@ -53,13 +52,16 @@ def mouseMoveEvent(self, event): self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: super().paintEvent(event) painter = QPainter(self) # Если индекс ячейки под курсором валидный - if 0 <= self.x_highlight_cell < self.matrix_size and 0 <= self.y_highlight_cell < self.matrix_size: + if ( + 0 <= self.x_highlight_cell < self.matrix_size + and 0 <= self.y_highlight_cell < self.matrix_size + ): # Выделение всего столбца и строки пересекающих ячейку под курсором painter.save() painter.setBrush(Qt.lightGray) @@ -70,7 +72,7 @@ def paintEvent(self, event): i * self.cell_size, self.y_highlight_cell * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) # Выделение столбца @@ -79,7 +81,7 @@ def paintEvent(self, event): self.x_highlight_cell * self.cell_size, j * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) painter.restore() @@ -91,7 +93,7 @@ def paintEvent(self, event): self.x_highlight_cell * self.cell_size, self.y_highlight_cell * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) painter.restore() @@ -131,7 +133,7 @@ def paintEvent(self, event): x2 += self.cell_size -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py index 882963664..7f6db329a 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py @@ -9,12 +9,8 @@ from cx_Freeze import setup, Executable -executables = [ - Executable('draw_table.py') -] +executables = [Executable("draw_table.py")] -setup(name='draw_table', - version='0.1', - description='draw_table', - executables=executables - ) +setup( + name="draw_table", version="0.1", description="draw_table", executables=executables +) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e1.py b/qt__pyqt__pyside__pyqode/PySide_examples/e1.py index 600ecf9c9..6d43f7ab5 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e1.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e1.py @@ -1,17 +1,18 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Простой пример использования модуля PySide from PySide.QtGui import * -# from PySide.QtCore import * -import sys -if __name__ == '__main__': + +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) - def says(): + def says() -> None: print("Hello, Python!") label = QLabel("Hello, PySize!") @@ -33,4 +34,4 @@ def says(): window.setLayout(layout) window.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e2.py b/qt__pyqt__pyside__pyqode/PySide_examples/e2.py index bdf84542b..5d60a5f05 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e2.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Пример показывает как можно создать простое окно с небольшим функционалом, @@ -11,7 +11,7 @@ class SimpleWindow(QtGui.QWidget): """Просто окно""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.setWindowTitle("Простое окно") @@ -30,18 +30,18 @@ def __init__(self, *args, **kwargs): self.setLayout(layout_main) - def slot_add_log(self): + def slot_add_log(self) -> None: text = self.le_mess.text() if text: self.lw_log.addItem(text) -import sys +if __name__ == "__main__": + import sys -if __name__ == '__main__': app = QtGui.QApplication(sys.argv) w = SimpleWindow() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e3.py b/qt__pyqt__pyside__pyqode/PySide_examples/e3.py index d756ac811..5bdfbe286 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e3.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e3.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" from PySide.QtGui import * class MainWindow(QMainWindow): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.editor = QTextEdit() self.setCentralWidget(self.editor) @@ -19,25 +19,25 @@ def __init__(self, *args, **kwargs): self.edit_menu = self.menuBar().addMenu("Правка") self.edit_menu.aboutToShow.connect(self.update_edit_menu) - def update_edit_menu(self): + def update_edit_menu(self) -> None: self.edit_menu.clear() actions = self.editor.createStandardContextMenu().actions() self.edit_menu.addActions(actions) - def slot_save_as(self): + def slot_save_as(self) -> None: file_name = QFileDialog.getSaveFileName()[0] if file_name: - with open(file_name, 'w') as f: - f.write(self.editor.toPlainText()) + with open(file_name, "w") as f: + f.write(self.editor.toPlainText()) -if __name__ == '__main__': +if __name__ == "__main__": import sys app = QApplication(sys.argv) - app.setApplicationName('myWindow') + app.setApplicationName("myWindow") w = MainWindow() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e4.py b/qt__pyqt__pyside__pyqode/PySide_examples/e4.py index 17b03bb5c..736d1b344 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e4.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e4.py @@ -1,51 +1,55 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time +from threading import Thread + from grab import Grab -import sys from PySide.QtGui import * -from threading import Thread -import time def get_confucius_quotes(): g = Grab() # http://ru.wikiquote.org/wiki/Конфуций - url = "http://ru.wikiquote.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9" + url = ( + "http://ru.wikiquote.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9" + ) g.go(url) quotes = list() - for el in g.doc.select('//h2/following-sibling::ul/li'): + for el in g.doc.select("//h2/following-sibling::ul/li"): quotes.append(el.text()) return quotes class MyThread(Thread): - def __init__(self, log): + def __init__(self, log) -> None: super().__init__() self.log = log - def run(self): + def run(self) -> None: for i, quote in enumerate(get_confucius_quotes(), 1): - self.log.append('{}. "{}"\n'.format(i, quote)) + self.log.append(f'{i}. "{quote}"\n') time.sleep(0.1) # задержка каждые 100 миллисекунд class Window(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.te_quotes = QTextEdit() self.te_quotes.setReadOnly(True) self.setCentralWidget(self.te_quotes) - def slot_refresh(self): + def slot_refresh(self) -> None: self.te_quotes.clear() thread = MyThread(self.te_quotes) thread.start() -if __name__ == '__main__': +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) w = Window() diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py b/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py index ecb58f731..8390f5d6b 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys - from PySide.QtGui import * from PySide.QtCore import * class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.layout = QVBoxLayout() @@ -21,14 +19,14 @@ def __init__(self): self._add_line_edit() - def _add_line_edit(self): + def _add_line_edit(self) -> None: line_edit = QLineEdit() line_edit.textEdited.connect(self._text_edited) self.layout.addWidget(line_edit) self.line_edit_list.append(line_edit) - def _text_edited(self, text): + def _text_edited(self, text) -> None: if not self.line_edit_list: return @@ -36,7 +34,9 @@ def _text_edited(self, text): self._add_line_edit() -if __name__ == '__main__': +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py b/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py index ef79d74f1..c10d47b94 100644 --- a/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py +++ b/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton @@ -9,10 +9,10 @@ class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = QPushButton('Test') + self.button = QPushButton("Test") self.button.clicked.connect(self.on_clicked) main_layout = QVBoxLayout() @@ -20,13 +20,13 @@ def __init__(self): self.setLayout(main_layout) - def foo(self): - print('foo') + def foo(self) -> None: + print("foo") - def bar(self): - print('bar') + def bar(self) -> None: + print("bar") - def on_clicked(self): + def on_clicked(self) -> None: modifiers = QApplication.keyboardModifiers() if modifiers == Qt.AltModifier: @@ -35,7 +35,7 @@ def on_clicked(self): self.foo() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/QApplication_show_screens.py b/qt__pyqt__pyside__pyqode/QApplication_show_screens.py new file mode 100644 index 000000000..0c21d0bf2 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QApplication_show_screens.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication + + +app = QApplication([]) + +screens = app.screens() +print(f"Screens ({len(screens)}):") + +for screen in screens: + print(f" {screen.availableGeometry()} {screen.availableSize()}") diff --git a/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py b/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py new file mode 100644 index 000000000..69357fd5a --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from PyQt5.QAxContainer import QAxWidget +from PyQt5.QtWidgets import ( + QWidget, + QGridLayout, + QPushButton, + QFileDialog, + QMessageBox, + QApplication, +) + + +class Window(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Document Word.Application") + self.axWidget = QAxWidget(self) + + self.buttonOpen = QPushButton("Open") + self.buttonOpen.clicked.connect(self.handleOpen) + + layout = QGridLayout(self) + layout.addWidget(self.axWidget) + layout.addWidget(self.buttonOpen) + + def handleOpen(self): + path, _ = QFileDialog.getOpenFileName( + self, "Выберите файл word", "", "word(*.docx *.doc)" + ) + if not path: + return + + return self.openOffice(path, "Word.Application") + + def openOffice(self, path, app): + self.axWidget.clear() + if not self.axWidget.setControl(app): + return QMessageBox.critical(self, "Ошибка", f"Нет установки {app!r}") + + self.axWidget.dynamicCall("SetVisible (bool Visible)", "false") + self.axWidget.setProperty("DisplayAlerts", False) + self.axWidget.setControl(path) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + mw = Window() + mw.resize(840, 480) + mw.show() + + sys.exit(app.exec()) diff --git a/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py b/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py index b7cd3aebd..2c0192947 100644 --- a/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py +++ b/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import datetime as dt from pathlib import Path from PyQt5.QtCore import QCoreApplication, QFileSystemWatcher @@ -11,11 +12,16 @@ current_dir = str(Path(__file__).resolve().parent) - app = QCoreApplication([]) watcher = QFileSystemWatcher([current_dir]) -watcher.directoryChanged.connect(lambda directory: print('Directory:', directory)) -watcher.fileChanged.connect(lambda file: print('File:', file)) +watcher.directoryChanged.connect( + lambda directory: print( + f"[{dt.datetime.now():%d/%m/%Y %H:%M:%S}] Directory: {directory!r}" + ) +) +watcher.fileChanged.connect( + lambda file: print(f"[{dt.datetime.now():%d/%m/%Y %H:%M:%S}] File: {file!r}") +) app.exec_() diff --git a/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py b/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py new file mode 100644 index 000000000..fa1511990 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtGui import QImageReader + + +print(list(map(lambda x: x.data().decode(), QImageReader.supportedImageFormats()))) +# ['bmp', 'cur', 'gif', 'icns', 'ico', 'jpeg', 'jpg', 'pbm', 'pgm', 'png', 'ppm', 'svg', 'svgz', 'tga', 'tif', 'tiff', 'wbmp', 'webp', 'xbm', 'xpm'] diff --git a/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/52rQ3.png b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/52rQ3.png new file mode 100644 index 000000000..89740d6ad Binary files /dev/null and b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/52rQ3.png differ diff --git a/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py new file mode 100644 index 000000000..24b062c28 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QLabel +from PyQt5.QtGui import QPixmap + + +class Label(QLabel): + def __init__(self) -> None: + super().__init__() + + self.pixmap = QPixmap("52rQ3.png") + self.pixmap_leave = self.pixmap.copy(0, 0, 26, 26) + self.pixmap_enter = self.pixmap.copy(26, 0, 52, 26) + + self.setPixmap(self.pixmap_leave) + + def enterEvent(self, event) -> None: + self.setPixmap(self.pixmap_enter) + + def leaveEvent(self, event) -> None: + self.setPixmap(self.pixmap_leave) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = Label() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py index 3ec556b50..d2b09cff9 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") font = label_1.font() font.setItalic(True) @@ -17,7 +18,7 @@ label_1.setFont(font) label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py index 2f214642b..897b864f6 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py index 2856d3e7e..749dbe34a 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py @@ -1,30 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) -app.setStyleSheet(''' +app.setStyleSheet( + """ #welcome { font: bold italic } #label_foo_bar { font: italic; color: green; } -''') +""" +) label_1 = QLabel() -label_1.setObjectName('welcome') -label_1.setText('Hello World!!!') +label_1.setObjectName("welcome") +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") label_3 = QLabel() -label_3.setObjectName('label_foo_bar') -label_3.setText('FooBar') +label_3.setObjectName("label_foo_bar") +label_3.setText("FooBar") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py index ddbf73edb..096fd41ca 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) -app.setStyleSheet('QLabel { font: bold italic }') +app.setStyleSheet("QLabel { font: bold italic }") label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py index c8beffea6..4ff050f43 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py @@ -1,23 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') -label_1.setStyleSheet(""" +label_1.setText("Hello World!!!") +label_1.setStyleSheet( + """ font: bold italic; color: green; background-color: black; -""") +""" +) label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py index 45766a589..8fbbf55c1 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py +++ b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time - from PyQt5.Qt import QMainWindow, QLabel, QFont, QApplication, Qt, QThread class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.label = QLabel() self.label.setAlignment(Qt.AlignCenter) - self.label.setFont(QFont('Courier', 25)) + self.label.setFont(QFont("Courier", 25)) - def loop(): + def loop() -> None: i = 0 while True: @@ -27,7 +26,7 @@ def loop(): time.sleep(1) class Thread(QThread): - def run(self): + def run(self) -> None: loop() self.thread = Thread() @@ -36,7 +35,7 @@ def run(self): self.setCentralWidget(self.label) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py index 75e644223..80d27525b 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py +++ b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading @@ -11,14 +11,14 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.label = QLabel() self.label.setAlignment(Qt.AlignCenter) - self.label.setFont(QFont('Courier', 25)) + self.label.setFont(QFont("Courier", 25)) - def loop(): + def loop() -> None: i = 0 while True: @@ -33,7 +33,7 @@ def loop(): self.setCentralWidget(self.label) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py b/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py new file mode 100644 index 000000000..9d6ebb392 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtCore import QLibraryInfo + + +print(QLibraryInfo.location(QLibraryInfo.TranslationsPath)) +print(QLibraryInfo.location(QLibraryInfo.PluginsPath)) +print(QLibraryInfo.location(QLibraryInfo.ExamplesPath)) +""" +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/translations +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/plugins +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/examples +""" diff --git a/qt__pyqt__pyside__pyqode/QLineEdit__mask.py b/qt__pyqt__pyside__pyqode/QLineEdit__mask.py index 934aad3f4..934c4f95c 100644 --- a/qt__pyqt__pyside__pyqode/QLineEdit__mask.py +++ b/qt__pyqt__pyside__pyqode/QLineEdit__mask.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qlineedit.html#inputMask-prop @@ -11,7 +11,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.mask_qt = QLineEdit("+7([000])[000]-[0000]") @@ -25,14 +25,14 @@ def __init__(self): self.setLayout(layout) - def _on_changed_mask(self, mask): + def _on_changed_mask(self, mask) -> None: text = self.test_input.text() self.test_input.setInputMask(mask) self.test_input.setText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py b/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py new file mode 100644 index 000000000..9bf014214 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt + +from PyQt5.QtWidgets import ( + QApplication, + QListWidget, + QListWidgetItem, + QFrame, + QFormLayout, + QLabel, +) +from PyQt5.QtCore import Qt + + +class ListItem(QFrame): + def __init__( + self, + host: str, + port: int, + status: str = "Not checked!", + last_check_time: dt.datetime = None, + ) -> None: + super().__init__() + + if not last_check_time: + last_check_time = dt.datetime.now() + + self.setFrameShape(QFrame.Box) + self.setFrameShadow(QFrame.Plain) + + self.label_address = QLabel(f"{host}:{port}") + self.label_status = QLabel(status) + self.label_last_check_time = QLabel( + last_check_time.strftime("%d/%m/%Y %H:%M:%S") + ) + + main_layout = QFormLayout(self) + main_layout.addRow("Address:", self.label_address) + main_layout.addRow("Status:", self.label_status) + main_layout.addRow("Last check time:", self.label_last_check_time) + + self.setFixedHeight(self.sizeHint().height()) + + +def add_item(list_widget: QListWidget, host: str, port: int) -> None: + widget = ListItem(host, port) + + item = QListWidgetItem() + item.setSizeHint(widget.sizeHint()) + item.setData(Qt.UserRole, widget) + + list_widget.addItem(item) + list_widget.setItemWidget(item, widget) + + +if __name__ == "__main__": + app = QApplication([]) + + list_widgets = QListWidget() + list_widgets.resize(350, 240) + list_widgets.setSpacing(2) + + list_widgets.itemClicked.connect(lambda item: print(item.data(Qt.UserRole))) + + add_item(list_widgets, "127.0.0.1", 161) + add_item(list_widgets, "192.168.0.0", 161) + for i in range(1, 10 + 1): + add_item(list_widgets, "127.0.0.1", 161 + i) + + list_widgets.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml b/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml new file mode 100644 index 000000000..001a12cf1 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml @@ -0,0 +1,23 @@ +import QtQuick 2.3 + +Rectangle { + width: 360 + height: 360 + + Text { + id: helloText + objectName: "helloText" + + text: qsTr("Click me!") + anchors.centerIn: parent + font.pointSize: 24 + font.bold: true + + MouseArea { + anchors.fill: parent + onClicked: { + helloText.text = qsTr("Hello World!"); + } + } + } +} \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml b/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml new file mode 100644 index 000000000..7daed8dc9 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml @@ -0,0 +1,23 @@ +import QtQuick 2.3 +import QtQuick.Window 2.0 + +Window { + visible: true + width: 640 + height: 480 + title: qsTr("Hello World") //The method qsTr() is used for translations from one language to other. + + Text { + id: helloText + text: qsTr("Click me!") + anchors.centerIn: parent + font.pointSize: 24; font.bold: true + + MouseArea { + anchors.fill: parent + onClicked: { + helloText.text = "Hello World!"; + } + } + } +} \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py new file mode 100644 index 000000000..ebeb67d04 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQuick import QQuickView +from PyQt5.QtCore import QUrl + + +app = QGuiApplication([]) + +qml_file = Path(__file__).parent.resolve() / "hello_text.qml" +url_qml_file = QUrl.fromLocalFile(str(qml_file)) + +view = QQuickView() +view.setTitle("Hello World!") +view.setSource(url_qml_file) +view.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py new file mode 100644 index 000000000..64ffdd123 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from PyQt5.QtGui import QGuiApplication # NOTE: For only QML, QGuiApplication is enough +from PyQt5.QtQml import QQmlApplicationEngine +from PyQt5.QtCore import QUrl + + +app = QGuiApplication([]) + +qml_file_window = Path(__file__).parent.resolve() / "hello_text_window.qml" +url_qml_file_window = QUrl.fromLocalFile(str(qml_file_window)) + +engine = QQmlApplicationEngine(url_qml_file_window) + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py new file mode 100644 index 000000000..00fe3caf7 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime +from pathlib import Path + +from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout +from PyQt5.QtQuick import QQuickView, QQuickItem +from PyQt5.QtCore import QUrl + + +QML_FILE = Path(__file__).parent.resolve() / "hello_text.qml" +URL_QML_FILE = QUrl.fromLocalFile(str(QML_FILE)) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Hello World! (QML + Qt)") + + self.view = QQuickView() + self.view.setSource(URL_QML_FILE) + + pb_click = QPushButton("Click me!") + pb_click.clicked.connect(self._on_clicked) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QWidget.createWindowContainer(self.view)) + main_layout.addWidget(pb_click) + + def _on_clicked(self) -> None: + qml_hello_text = self.view.rootObject().findChild(QQuickItem, "helloText") + qml_hello_text.setProperty("text", f"Now: {datetime.now().time():%H:%M:%S}") + + +if __name__ == "__main__": + app = QApplication([]) + + view = QQuickView() + view.setTitle("Hello World! (single)") + view.setSource(URL_QML_FILE) + view.show() + + main_window = MainWindow() + main_window.resize(500, 500) + main_window.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py b/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py index 7a5b0acc8..8584615d1 100644 --- a/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py +++ b/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py @@ -1,21 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiSubWindow, QMdiArea, QTextEdit, QPushButton +from PyQt5.QtWidgets import ( + QApplication, + QMainWindow, + QMdiSubWindow, + QMdiArea, + QTextEdit, + QPushButton, +) class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.sub_window_1 = QMdiSubWindow() self.sub_window_1.setWidget(QTextEdit("

    Hello World!

    ")) self.sub_window_2 = QMdiSubWindow() - self.sub_window_2.setWidget(QPushButton('Click!')) + self.sub_window_2.setWidget(QPushButton("Click!")) self.mdi_area = QMdiArea() self.mdi_area.addSubWindow(self.sub_window_1) @@ -24,7 +31,7 @@ def __init__(self): self.setCentralWidget(self.mdi_area) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py new file mode 100644 index 000000000..1db1bf643 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtGui import QPainter, QImage +from PyQt5.QtCore import Qt + + +img = QImage(r"video-game-192x192.png") +img2 = QImage(img.size(), QImage.Format_ARGB32) +img2.fill(Qt.transparent) + +p = QPainter(img2) +p.setOpacity(0.85) +p.drawImage(img.rect(), img) + +img2.save(r"video-game-192x192__transparent.png") diff --git a/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192.png b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192.png new file mode 100644 index 000000000..557d4503e Binary files /dev/null and b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192.png differ diff --git a/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192__transparent.png b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192__transparent.png new file mode 100644 index 000000000..2b97416f7 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/video-game-192x192__transparent.png differ diff --git a/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.jpg b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.jpg new file mode 100644 index 000000000..620279e59 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.jpg differ diff --git a/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.png b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.png new file mode 100644 index 000000000..1c52cea10 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/cat.png differ diff --git a/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py new file mode 100644 index 000000000..e31020d56 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtGui import QGuiApplication, QPixmap +from PyQt5.QtCore import Qt + + +app = QGuiApplication([]) + +pixmap = QPixmap("cat.jpg") +pixmap.setMask(pixmap.createHeuristicMask(Qt.transparent)) +pixmap.save("cat.png") diff --git a/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py b/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py index 22be23e1e..512d88dc0 100644 --- a/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py +++ b/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.sb_font_size = Qt.QSpinBox() self.sb_font_size.setRange(5, 40) self.sb_font_size.valueChanged.connect(self._on_font_size_changed) - self.text_edit = Qt.QTextEdit('Test this!') + self.text_edit = Qt.QTextEdit("Test this!") layout = Qt.QVBoxLayout() layout.addWidget(self.sb_font_size) @@ -26,13 +26,13 @@ def __init__(self): self.setCentralWidget(central_widget) - def _on_font_size_changed(self, value): + def _on_font_size_changed(self, value) -> None: text_char_format = Qt.QTextCharFormat() text_char_format.setFontPointSize(value) self.merge_format_on_word_or_selection(text_char_format) - def merge_format_on_word_or_selection(self, text_char_format): + def merge_format_on_word_or_selection(self, text_char_format) -> None: cursor = self.text_edit.textCursor() if not cursor.hasSelection(): cursor.select(Qt.QTextCursor.WordUnderCursor) @@ -40,7 +40,7 @@ def merge_format_on_word_or_selection(self, text_char_format): cursor.mergeCharFormat(text_char_format) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py b/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py new file mode 100644 index 000000000..af48f9ec5 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import threading +import time +import random + +from PyQt5.QtCore import QThreadPool, QRunnable + + +lock = threading.Lock() + + +class HelloWorldTask(QRunnable): + def __init__(self, idx: int) -> None: + super().__init__() + + self.idx = idx + + def run(self) -> None: + with lock: + print(f"[{self.idx}] Hello world from thread: {threading.current_thread()}") + + ms = random.randint(1, 10) / 1000 + time.sleep(ms) + + +# QThreadPool takes ownership and deletes 'hello' automatically +pool = QThreadPool.globalInstance() + +for i in range(1, 100 + 1): + hello = HelloWorldTask(idx=i) + pool.start(hello) + +pool.waitForDone() diff --git a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py new file mode 100644 index 000000000..405f622ee --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QGridLayout, + QComboBox, + QPushButton, + QLabel, +) +from PyQt5.QtCore import QThread, QTimer, QWaitCondition, QMutex + + +class Thread(QThread): + def __init__(self, parent=None) -> None: + super().__init__(parent) + + self._is_pause = False + self.condition = QWaitCondition() + self.mutex = QMutex() + self.sum = 0 + + def is_pause(self) -> bool: + return self._is_pause + + def pause(self) -> None: + self._is_pause = True + + def resume(self) -> None: + self._is_pause = False + self.condition.wakeAll() + + def run(self) -> None: + while True: + self.mutex.lock() + if self._is_pause: + self.condition.wait(self.mutex) + self.sum += 1 + time.sleep(2) + self.mutex.unlock() + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.combobox_thread = QComboBox() + self.combobox_thread.currentIndexChanged.connect(self._update_states) + + self.label_result = QLabel() + + self.button_add = QPushButton("Add") + self.button_add.clicked.connect(self.add) + + self.button_pause = QPushButton("Pause") + self.button_pause.clicked.connect(self.pause) + + self.button_pause_all = QPushButton("Pause all") + self.button_pause_all.clicked.connect(self.pause_all) + + self.button_resume = QPushButton("Resume") + self.button_resume.clicked.connect(self.resume) + + self.button_resume_all = QPushButton("Resume all") + self.button_resume_all.clicked.connect(self.resume_all) + + layout = QGridLayout(self) + layout.addWidget(self.combobox_thread, 0, 0) + layout.addWidget(self.button_add, 0, 1, 1, 2) + layout.addWidget(self.label_result, 1, 0) + layout.addWidget(self.button_pause, 1, 1) + layout.addWidget(self.button_pause_all, 1, 2) + layout.addWidget(self.button_resume, 2, 1) + layout.addWidget(self.button_resume_all, 2, 2) + layout.setRowStretch(layout.rowCount(), 1) + + self.timer = QTimer() + self.timer.timeout.connect(self.update_info) + self.timer.start(1000) + + self._update_states() + + def _update_states(self) -> None: + threads_num = self.combobox_thread.count() + self.setWindowTitle(f"Threads: {threads_num}") + + idx = self.combobox_thread.currentIndex() + ok = idx != -1 + + self.button_pause.setEnabled(ok and not self.get_thread(idx).is_pause()) + self.button_resume.setEnabled(ok and self.get_thread(idx).is_pause()) + + if ok: + title = self.combobox_thread.itemText(idx) + self.button_pause.setText(f"Pause - {title!r}") + self.button_resume.setText(f"Resume - {title!r}") + + self.button_pause_all.setEnabled(threads_num) + self.button_resume_all.setEnabled(threads_num) + + def get_thread(self, idx: int) -> Thread: + return self.combobox_thread.itemData(idx) + + def get_all_thread(self) -> list[Thread]: + return [self.get_thread(i) for i in range(self.combobox_thread.count())] + + def update_info(self) -> None: + total = sum(thread.sum for thread in self.get_all_thread()) + self.label_result.setText(f"SUM: {total}") + + def add(self) -> None: + thread = Thread(self) + thread.start() + + self.combobox_thread.addItem( + f"Thread #{self.combobox_thread.count() + 1}", thread + ) + self._update_states() + + def pause(self) -> None: + thread = self.combobox_thread.currentData() + if thread: + thread.pause() + + self._update_states() + + def pause_all(self) -> None: + for thread in self.get_all_thread(): + thread.pause() + + self._update_states() + + def resume(self) -> None: + thread = self.combobox_thread.currentData() + if thread: + thread.resume() + + self._update_states() + + def resume_all(self) -> None: + for thread in self.get_all_thread(): + thread.resume() + + self._update_states() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + mw = MainWindow() + mw.resize(320, 240) + mw.show() + + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py new file mode 100644 index 000000000..228b77fce --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import sys +import time + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QTableWidget, + QTableWidgetItem, + QPushButton, + QLabel, + QCheckBox, +) +from PyQt5.QtCore import QThread, QTimer, QWaitCondition, QMutex, pyqtSignal, Qt + + +def get_pretty_int(num: int) -> str: + return f"{num:,}".replace(",", " ") + + +class Thread(QThread): + about_pause = pyqtSignal(bool) + about_sum = pyqtSignal(int) + + def __init__(self, parent=None) -> None: + super().__init__(parent) + + self._is_pause = False + self.condition = QWaitCondition() + self.mutex = QMutex() + self._sum = 0 + + @property + def sum(self) -> int: + return self._sum + + @sum.setter + def sum(self, value: int) -> None: + self._sum = value + self.about_sum.emit(value) + + def get_state(self) -> str: + return "PAUSED" if self._is_pause else "WORKING" + + def set_pause(self, pause: bool) -> None: + self._is_pause = pause + self.about_pause.emit(pause) + + if not pause: + self.condition.wakeAll() + + def is_pause(self) -> bool: + return self._is_pause + + def pause(self) -> None: + self.set_pause(True) + + def resume(self) -> None: + self.set_pause(False) + + def run(self) -> None: + while True: + self.mutex.lock() + if self._is_pause: + self.condition.wait(self.mutex) + self.sum += 1 + time.sleep(2) + self.mutex.unlock() + + +class MainWindow(QWidget): + headers = ["NAME", "STATE", "SUM"] + + def __init__(self) -> None: + super().__init__() + + self.started: dt.datetime = None + self._sum = 0 + + self.table_thread = QTableWidget() + self.table_thread.setColumnCount(len(self.headers)) + self.table_thread.setHorizontalHeaderLabels(self.headers) + self.table_thread.horizontalHeader().setStretchLastSection(True) + self.table_thread.setAlternatingRowColors(True) + self.table_thread.setSelectionBehavior(QTableWidget.SelectRows) + self.table_thread.setSelectionMode(QTableWidget.SingleSelection) + self.table_thread.setMinimumHeight(250) + self.table_thread.selectionModel().currentRowChanged.connect( + self._update_states + ) + + self.label_result = QLabel() + + self.button_add = QPushButton("Add") + self.button_add.clicked.connect(self.add) + + self.button_pause = QPushButton("Pause") + self.button_pause.clicked.connect(self.pause) + + self.button_pause_all = QPushButton("Pause all") + self.button_pause_all.clicked.connect(self.pause_all) + + self.button_resume = QPushButton("Resume") + self.button_resume.clicked.connect(self.resume) + + self.button_resume_all = QPushButton("Resume all") + self.button_resume_all.clicked.connect(self.resume_all) + + self.cb_reset_sum = QCheckBox("Reset sum") + self.cb_reset_sum.setChecked(True) + + left_layout = QVBoxLayout() + left_layout.addWidget(self.table_thread) + left_layout.addWidget(self.label_result) + + right_layout = QGridLayout() + right_layout.addWidget(self.button_add, 0, 0, 1, 2) + right_layout.addWidget(self.button_pause, 1, 0) + right_layout.addWidget(self.button_pause_all, 1, 1) + right_layout.addWidget(self.button_resume, 2, 0) + right_layout.addWidget(self.button_resume_all, 2, 1) + right_layout.addWidget(self.cb_reset_sum, 3, 0, 1, 2) + right_layout.setRowStretch(right_layout.rowCount(), 1) + + main_layout = QHBoxLayout(self) + main_layout.addLayout(left_layout) + main_layout.addLayout(right_layout) + + self.timer = QTimer() + self.timer.timeout.connect(self.update_info) + self.timer.start(1000) + + self._update_states() + + def _update_window_title(self) -> None: + threads_num = self.table_thread.rowCount() + if self.started: + elapsed = str(dt.datetime.now() - self.started).split(".")[0] + self.setWindowTitle(f"Threads: {threads_num}. Elapsed: {elapsed}") + else: + self.setWindowTitle(f"Threads: {threads_num}") + + def _update_states(self) -> None: + self._update_window_title() + + row = self.table_thread.currentRow() + ok = row != -1 + + self.button_pause.setEnabled(ok and not self.get_thread(row).is_pause()) + self.button_resume.setEnabled(ok and self.get_thread(row).is_pause()) + + if ok: + title = self.table_thread.item(row, 0).text() + self.button_pause.setText(f"Pause - {title!r}") + self.button_resume.setText(f"Resume - {title!r}") + + threads_num = self.table_thread.rowCount() + self.button_pause_all.setEnabled(threads_num) + self.button_resume_all.setEnabled(threads_num) + + def get_thread(self, row: int) -> Thread: + return self.table_thread.item(row, 0).data(Qt.UserRole) + + def get_all_thread(self) -> list[Thread]: + return [self.get_thread(row) for row in range(self.table_thread.rowCount())] + + def update_info(self) -> None: + self._update_window_title() + + if self.cb_reset_sum.isChecked(): + self._sum = 0 + + self._sum += sum(thread.sum for thread in self.get_all_thread()) + self.label_result.setText(f"TOTAL SUM: {get_pretty_int(self._sum)}") + + def _on_thread_changed(self, thread: Thread) -> None: + for row in range(self.table_thread.rowCount()): + if thread == self.get_thread(row): + self.table_thread.item(row, 1).setText(thread.get_state()) + self.table_thread.item(row, 2).setText(get_pretty_int(thread.sum)) + + def add(self) -> None: + if not self.started: + self.started = dt.datetime.now() + + thread = Thread(self) + thread.about_sum.connect( + lambda _, thread=thread: self._on_thread_changed(thread) + ) + thread.about_pause.connect( + lambda _, thread=thread: self._on_thread_changed(thread) + ) + + row = self.table_thread.rowCount() + self.table_thread.setRowCount(row + 1) + + title = f"Thread #{row + 1}" + item_title = QTableWidgetItem(title) + item_title.setData(Qt.UserRole, thread) + + item_state = QTableWidgetItem(thread.get_state()) + item_sum = QTableWidgetItem(str(thread.sum)) + + self.table_thread.setItem(row, 0, item_title) + self.table_thread.setItem(row, 1, item_state) + self.table_thread.setItem(row, 2, item_sum) + + thread.start() + + self._update_states() + + def pause(self) -> None: + item = self.table_thread.currentItem() + if item: + thread = self.get_thread(item.row()) + thread.pause() + + self._update_states() + + def pause_all(self) -> None: + for thread in self.get_all_thread(): + thread.pause() + + self._update_states() + + def resume(self) -> None: + item = self.table_thread.currentItem() + if item: + thread = self.get_thread(item.row()) + thread.resume() + + self._update_states() + + def resume_all(self) -> None: + for thread in self.get_all_thread(): + thread.resume() + + self._update_states() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + mw = MainWindow() + mw.resize(800, 300) + mw.show() + + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py b/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py index cb4b1f682..6f7f69feb 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py +++ b/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/971228/201445 @@ -18,24 +18,26 @@ def createWindow(self, _type): page.urlChanged.connect(self.on_url_changed) return page - def on_url_changed(self, url: QUrl): + def on_url_changed(self, url: QUrl) -> None: page = self.sender() self.setUrl(url) page.deleteLater() class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.browser = QWebEngineView() self.browser.setPage(WebEnginePage(self.browser)) - self.browser.load(QUrl("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target")) + self.browser.load( + QUrl("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target") + ) self.setCentralWidget(self.browser) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py index 3fd41c545..3da012a4c 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -12,7 +12,7 @@ app = QApplication([]) -with open('js/jquery-3.1.1.min.js') as f: +with open("js/jquery-3.1.1.min.js") as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -20,7 +20,7 @@ view = QWebEngineView() -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: view.page().runJavaScript(jquery_text) # Test jQuery @@ -29,7 +29,7 @@ def _on_load_finished(ok: bool): view.loadFinished.connect(_on_load_finished) -view.load(QUrl('https://store.steampowered.com/')) +view.load(QUrl("https://store.steampowered.com/")) view.show() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py index 98b0247fd..6018c88a7 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl, Qt @@ -10,41 +10,42 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.view = QWebEngineView() self.view.loadFinished.connect(self._on_load_finished) - with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: + file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" + with open(file_name) as f: self.jquery_text = f.read() self.jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" - tool_bar = self.addToolBar('General') - action = tool_bar.addAction('Change background-color') + tool_bar = self.addToolBar("General") + action = tool_bar.addAction("Change background-color") action.triggered.connect(self._change_background_color) self.setCentralWidget(self.view) - def _on_load_finished(self, ok: bool): + def _on_load_finished(self, ok: bool) -> None: self.view.page().runJavaScript(self.jquery_text) - def _change_background_color(self): + def _change_background_color(self) -> None: color = QColorDialog.getColor(Qt.yellow) if not color.isValid(): return - code = "qt.jQuery('a').css('background-color', '{}');".format(color.name()) + code = f"qt.jQuery('a').css('background-color', '{color.name()}');" self.view.page().runJavaScript(code) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() mw.resize(400, 400) mw.show() - mw.view.load(QUrl('https://store.steampowered.com/')) + mw.view.load(QUrl("https://store.steampowered.com/")) app.exec() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py index f0e597de5..e5e174f41 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -9,11 +9,12 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView -LOGIN = '' -PASSWORD = '' +LOGIN = "" +PASSWORD = "" -with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: +file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" +with open(file_name) as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -21,31 +22,35 @@ app = QApplication([]) view = QWebEngineView() -view.load(QUrl('https://github.com/login')) +view.load(QUrl("https://github.com/login")) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() url = page.url().toString() print(url) - if not url.endswith('login'): + if not url.endswith("login"): return page.runJavaScript(jquery_text) - page.runJavaScript(f""" + page.runJavaScript( + f""" qt.jQuery('#login_field').val('{LOGIN}'); qt.jQuery('#password').val('{PASSWORD}'); qt.jQuery('input[name="commit"]').click(); - """) + """ + ) print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py index 2080e7ead..ca05eaeb8 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py @@ -1,43 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys + from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage -import sys - class MyWebEnginePage(QWebEnginePage): - def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: str, line_number: int, source_id: str): - print(f'javascript_console_message: {level}, {message}, {line_number}, {source_id}', file=sys.stderr) + def javaScriptConsoleMessage( + self, + level: "JavaScriptConsoleMessage", + message: str, + line_number: int, + source_id: str, + ) -> None: + print( + f"javascript_console_message: {level}, {message}, {line_number}, {source_id}", + file=sys.stderr, + ) app = QApplication([]) page = MyWebEnginePage() -page.load(QUrl('https://ya.ru')) +page.load(QUrl("https://ya.ru")) view = QWebEngineView() view.setPage(page) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() print(page.url().toString()) - page.runJavaScript('console.log(document.title)') - page.runJavaScript('console.log(this)') + page.runJavaScript("console.log(document.title)") + page.runJavaScript("console.log(this)") page.runJavaScript('console.log("Hello World!")') - page.runJavaScript('console.log(2 + 2 * 2)') + page.runJavaScript("console.log(2 + 2 * 2)") print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py index 8115f3373..a3a1287be 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl, QEventLoop @@ -12,12 +12,10 @@ def run_js_code(page: QWebEnginePage, code: str) -> object: loop = QEventLoop() - result_value = { - 'value': None - } + result_value = {"value": None} - def _on_callback(result: object): - result_value['value'] = result + def _on_callback(result: object) -> None: + result_value["value"] = result loop.quit() @@ -25,10 +23,11 @@ def _on_callback(result: object): loop.exec() - return result_value['value'] + return result_value["value"] -with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: +file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" +with open(file_name) as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -36,17 +35,17 @@ def _on_callback(result: object): app = QApplication([]) view = QWebEngineView() -view.load(QUrl('https://гибдд.рф/request_main')) +view.load(QUrl("https://гибдд.рф/request_main")) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() print(page.url().toString()) page.runJavaScript(jquery_text) result = run_js_code(page, "document.title") - print('run_java_script:', result) + print("run_java_script:", result) # Клик на флажок "С информацией ознакомлен" run_js_code(page, """qt.jQuery('input[name="agree"]').click();""") @@ -57,7 +56,9 @@ def _on_load_finished(ok: bool): print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py b/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py index 255b9b8a9..05d200bd9 100644 --- a/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py +++ b/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QPixmap, QLabel diff --git a/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py b/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py index 5b9f99bdb..130249ff8 100644 --- a/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py +++ b/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) menu_bar = Qt.QMenuBar() - menu_file = menu_bar.addMenu('File') - action_exit = menu_file.addAction('Exit') + menu_file = menu_bar.addMenu("File") + action_exit = menu_file.addAction("Exit") action_exit.triggered.connect(self.close) layout = Qt.QVBoxLayout() - layout.addWidget(Qt.QLabel('Test')) - layout.addWidget(Qt.QPushButton('Click!')) + layout.addWidget(Qt.QLabel("Test")) + layout.addWidget(Qt.QPushButton("Click!")) layout.setMenuBar(menu_bar) self.setLayout(layout) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py b/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py index f4d66695e..841364e42 100644 --- a/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py +++ b/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -12,17 +12,19 @@ from PySide.QtNetwork import * -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) # Чтобы не было проблем запуска компов с прокси: QNetworkProxyFactory.setUseSystemConfiguration(True) - QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + QWebSettings.globalSettings().setAttribute( + QWebSettings.DeveloperExtrasEnabled, True + ) view = QWebView() # view.show() - view.load('http://www.myscore.ru/match/nqD8D0j4/#match-statistics;0') + view.load("http://www.myscore.ru/match/nqD8D0j4/#match-statistics;0") # Ждем пока прогрузится страница loop = QEventLoop() @@ -35,8 +37,8 @@ print(doc.findFirst("#statistics-2-statistic a").toPlainText()) table = doc.findFirst("#tab-statistics-0-statistic .parts") - for tr in table.findAll('tr'): - l, text, r = tr.toPlainText().split('\t') + for tr in table.findAll("tr"): + l, text, r = tr.toPlainText().split("\t") print(l, text, r) # sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py b/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py index 875a53607..9ecac1a74 100644 --- a/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py +++ b/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py @@ -2,19 +2,27 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGraphicsScene, \ - QGraphicsView, QGraphicsLinearLayout, QGraphicsWidget, QHBoxLayout +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QGraphicsScene, + QGraphicsView, + QGraphicsLinearLayout, + QGraphicsWidget, + QHBoxLayout, +) from PyQt5.QtGui import QPainter from PyQt5.QtCore import Qt class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.scene = QGraphicsScene() @@ -56,7 +64,7 @@ def __init__(self): scene_layout.addItem(proxy_rb3) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) w = Window() diff --git a/qt__pyqt__pyside__pyqode/RunFuncThread.py b/qt__pyqt__pyside__pyqode/RunFuncThread.py index ae35408b0..7329a61cf 100644 --- a/qt__pyqt__pyside__pyqode/RunFuncThread.py +++ b/qt__pyqt__pyside__pyqode/RunFuncThread.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QThread, pyqtSignal @@ -10,17 +10,18 @@ class RunFuncThread(QThread): run_finished = pyqtSignal(object) - def __init__(self, func): + def __init__(self, func) -> None: super().__init__() self.func = func - def run(self): + def run(self) -> None: self.run_finished.emit(self.func()) -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtCore import QCoreApplication + app = QCoreApplication([]) thread = RunFuncThread(func=lambda: 2 + 2 * 2) diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/README.md b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/README.md new file mode 100644 index 000000000..1655da9af --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/README.md @@ -0,0 +1 @@ +![](screenshot.png) \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/screenshot.png b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/screenshot.png new file mode 100644 index 000000000..7377fa06c Binary files /dev/null and b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/screenshot.png differ diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py new file mode 100644 index 000000000..7d3cbacd3 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stackoverflow.com/a/51825815/5909792 + + +from PyQt5.QtWidgets import ( + QAbstractButton, + QApplication, + QHBoxLayout, + QSizePolicy, + QWidget, +) +from PyQt5.QtCore import QPropertyAnimation, QRectF, QSize, Qt, pyqtProperty +from PyQt5.QtGui import QPainter + + +class Switch(QAbstractButton): + def __init__(self, parent=None, track_radius=10, thumb_radius=8) -> None: + super().__init__(parent=parent) + self.setCheckable(True) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self._track_radius = track_radius + self._thumb_radius = thumb_radius + + self._margin = max(0, self._thumb_radius - self._track_radius) + self._base_offset = max(self._thumb_radius, self._track_radius) + self._end_offset = { + True: lambda: self.width() - self._base_offset, + False: lambda: self._base_offset, + } + self._offset = self._base_offset + + palette = self.palette() + if self._thumb_radius > self._track_radius: + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._thumb_color = { + True: palette.highlight(), + False: palette.light(), + } + self._text_color = { + True: palette.highlightedText().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: "", + False: "", + } + self._track_opacity = 0.5 + else: + self._thumb_color = { + True: palette.highlightedText(), + False: palette.light(), + } + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._text_color = { + True: palette.highlight().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: "✔", + False: "✕", + } + self._track_opacity = 1 + + @pyqtProperty(int) + def offset(self): + return self._offset + + @offset.setter + def offset(self, value) -> None: + self._offset = value + self.update() + + def sizeHint(self): # pylint: disable=invalid-name + return QSize( + 4 * self._track_radius + 2 * self._margin, + 2 * self._track_radius + 2 * self._margin, + ) + + def setChecked(self, checked) -> None: + super().setChecked(checked) + self.offset = self._end_offset[checked]() + + def resizeEvent(self, event) -> None: + super().resizeEvent(event) + self.offset = self._end_offset[self.isChecked()]() + + def paintEvent(self, event) -> None: # pylint: disable=invalid-name, unused-argument + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + p.setPen(Qt.NoPen) + track_opacity = self._track_opacity + thumb_opacity = 1.0 + text_opacity = 1.0 + if self.isEnabled(): + track_brush = self._track_color[self.isChecked()] + thumb_brush = self._thumb_color[self.isChecked()] + text_color = self._text_color[self.isChecked()] + else: + track_opacity *= 0.8 + track_brush = self.palette().shadow() + thumb_brush = self.palette().mid() + text_color = self.palette().shadow().color() + + p.setBrush(track_brush) + p.setOpacity(track_opacity) + p.drawRoundedRect( + self._margin, + self._margin, + self.width() - 2 * self._margin, + self.height() - 2 * self._margin, + self._track_radius, + self._track_radius, + ) + p.setBrush(thumb_brush) + p.setOpacity(thumb_opacity) + p.drawEllipse( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ) + p.setPen(text_color) + p.setOpacity(text_opacity) + font = p.font() + font.setPixelSize(int(1.5 * self._thumb_radius)) + p.setFont(font) + p.drawText( + QRectF( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ), + Qt.AlignCenter, + self._thumb_text[self.isChecked()], + ) + + def mouseReleaseEvent(self, event) -> None: # pylint: disable=invalid-name + super().mouseReleaseEvent(event) + if event.button() == Qt.LeftButton: + anim = QPropertyAnimation(self, b"offset", self) + anim.setDuration(120) + anim.setStartValue(self.offset) + anim.setEndValue(self._end_offset[self.isChecked()]()) + anim.start() + + def enterEvent(self, event) -> None: # pylint: disable=invalid-name + self.setCursor(Qt.PointingHandCursor) + super().enterEvent(event) + + +def main() -> None: + app = QApplication([]) + + # Thumb size < track size (Gitlab style) + s1 = Switch() + s1.toggled.connect(lambda c: print("toggled", c)) + s1.clicked.connect(lambda c: print("clicked", c)) + s1.pressed.connect(lambda: print("pressed")) + s1.released.connect(lambda: print("released")) + s2 = Switch() + s2.setEnabled(False) + + # Thumb size > track size (Android style) + s3 = Switch(thumb_radius=11, track_radius=8) + s4 = Switch(thumb_radius=11, track_radius=8) + s4.setEnabled(False) + + l = QHBoxLayout() + l.addWidget(s1) + l.addWidget(s2) + l.addWidget(s3) + l.addWidget(s4) + w = QWidget() + w.setLayout(l) + w.show() + + app.exec() + + +if __name__ == "__main__": + main() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py new file mode 100644 index 000000000..2a9dfecad --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel +from switch_button import Switch + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + switch_btn1 = Switch() + label_switch_btn1 = QLabel(f"Checked: {switch_btn1.isChecked()}") + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}" + ) + ) + + switch_btn2 = Switch() + label_switch_btn2 = QLabel(f"Checked: {switch_btn2.isChecked()}") + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}" + ) + ) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(switch_btn1) + main_layout.addWidget(label_switch_btn1) + + main_layout.addWidget(switch_btn2) + main_layout.addWidget(label_switch_btn2) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/README.md b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/README.md new file mode 100644 index 000000000..1655da9af --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/README.md @@ -0,0 +1 @@ +![](screenshot.png) \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/screenshot.png b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/screenshot.png new file mode 100644 index 000000000..fda99abcf Binary files /dev/null and b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/screenshot.png differ diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py new file mode 100644 index 000000000..1db714f21 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://ru.stackoverflow.com/a/1355594/201445 + + +import sys + +from PyQt5.QtWidgets import QCheckBox +from PyQt5.QtGui import QPainter, QColor +from PyQt5.QtCore import ( + Qt, + QEasingCurve, + QPropertyAnimation, + pyqtProperty, + QPoint, + QRect, +) + + +class ToggleButton(QCheckBox): + def __init__( + self, + width=70, + height=40, + bg_color="#777", + circle_color="#DDD", + active_color="#00BCff", + animation_curve=QEasingCurve.OutBounce, + ) -> None: + super().__init__() + + self.setFixedSize(width, height) + self.setCursor(Qt.PointingHandCursor) + + self._bg_color = bg_color + self._circle_color = circle_color + self._active_color = active_color + + self._circle_margin = 3 + self._circle_position = self._circle_margin + self._circle_size = self.height() - self._circle_margin + + self.animation = QPropertyAnimation(self, b"circle_position") + self.animation.setEasingCurve(animation_curve) + self.animation.setDuration(500) + self.stateChanged.connect(self.start_transition) + + @pyqtProperty(int) + def circle_position(self) -> int: + return self._circle_position + + @circle_position.setter + def circle_position(self, pos: int) -> None: + self._circle_position = pos + self.update() + + def start_transition(self, value) -> None: + self.animation.setStartValue(self.circle_position) + if value: + self.animation.setEndValue(self.width() - self._circle_size) + else: + self.animation.setEndValue(self._circle_margin) + self.animation.start() + + def hitButton(self, pos: QPoint) -> bool: + return self.contentsRect().contains(pos) + + def paintEvent(self, e) -> None: + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing) + + p.setPen(Qt.NoPen) + + rect = QRect(0, 0, self.width(), self.height()) + + p.setBrush(QColor(self._active_color if self.isChecked() else self._bg_color)) + p.drawRoundedRect( + 0, 0, rect.width(), self.height(), self.height() / 2, self.height() / 2 + ) + + p.setBrush(QColor(self._circle_color)) + + x, y = self._circle_position, self._circle_margin + p.drawEllipse( + x, + y, + self._circle_size - self._circle_margin, + self._circle_size - self._circle_margin, + ) + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout + + class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Анимация кнопки переключения") + self.toggleBtn = ToggleButton() + + self.layout = QVBoxLayout(self) + self.layout.addWidget(self.toggleBtn, Qt.AlignCenter, Qt.AlignCenter) + + app = QApplication(sys.argv) + w = MainWindow() + w.resize(500, 500) + w.show() + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py new file mode 100644 index 000000000..90a7515ac --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel +from switch_button import ToggleButton + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + switch_btn1 = ToggleButton() + label_switch_btn1 = QLabel(f"Checked: {switch_btn1.isChecked()}") + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}" + ) + ) + + switch_btn2 = ToggleButton(width=200) + label_switch_btn2 = QLabel(f"Checked: {switch_btn2.isChecked()}") + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}" + ) + ) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(switch_btn1) + main_layout.addWidget(label_switch_btn1) + + main_layout.addWidget(switch_btn2) + main_layout.addWidget(label_switch_btn2) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/README.md b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/README.md new file mode 100644 index 000000000..1655da9af --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/README.md @@ -0,0 +1 @@ +![](screenshot.png) \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/screenshot.png b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/screenshot.png new file mode 100644 index 000000000..39cec53be Binary files /dev/null and b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/screenshot.png differ diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py new file mode 100644 index 000000000..d48da89c2 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stackoverflow.com/a/51023362/5909792 + + +from PyQt5 import QtWidgets, QtCore, QtGui + + +class SwitchButton(QtWidgets.QWidget): + clicked = QtCore.pyqtSignal(bool) + + def __init__(self, parent=None, w1="Yes", l1=12, w2="No", l2=33, width=60) -> None: + super(SwitchButton, self).__init__(parent) + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.__labeloff = QtWidgets.QLabel(self) + self.__labeloff.setText(w2) + self.__labeloff.setStyleSheet( + """color: rgb(120, 120, 120); font-weight: bold;""" + ) + self.__background = Background(self) + self.__labelon = QtWidgets.QLabel(self) + self.__labelon.setText(w1) + self.__labelon.setStyleSheet( + """color: rgb(255, 255, 255); font-weight: bold;""" + ) + self.__circle = Circle(self) + self.__circlemove = None + self.__ellipsemove = None + self.__enabled = True + self.__duration = 100 + self.__value = False + self.setFixedSize(width, 24) + + self.__background.resize(20, 20) + self.__background.move(2, 2) + self.__circle.move(2, 2) + self.__labelon.move(l1, 5) + self.__labeloff.move(l2, 5) + + def isChecked(self) -> bool: + return self.__value + + def valueText(self) -> str: + return self.__labelon.text() if self.isChecked() else self.__labeloff.text() + + def setDuration(self, time) -> None: + self.__duration = time + + def mousePressEvent(self, event) -> None: + if not self.__enabled: + return + + self.__circlemove = QtCore.QPropertyAnimation(self.__circle, b"pos") + self.__circlemove.setDuration(self.__duration) + + self.__ellipsemove = QtCore.QPropertyAnimation(self.__background, b"size") + self.__ellipsemove.setDuration(self.__duration) + + xs = 2 + y = 2 + xf = self.width() - 22 + hback = 20 + isize = QtCore.QSize(hback, hback) + bsize = QtCore.QSize(self.width() - 4, hback) + if self.__value: + xf = 2 + xs = self.width() - 22 + bsize = QtCore.QSize(hback, hback) + isize = QtCore.QSize(self.width() - 4, hback) + + self.__circlemove.setStartValue(QtCore.QPoint(xs, y)) + self.__circlemove.setEndValue(QtCore.QPoint(xf, y)) + + self.__ellipsemove.setStartValue(isize) + self.__ellipsemove.setEndValue(bsize) + + self.__circlemove.start() + self.__ellipsemove.start() + self.__value = not self.__value + + self.clicked.emit(self.isChecked()) + + def paintEvent(self, event) -> None: + s = self.size() + qp = QtGui.QPainter() + qp.begin(self) + qp.setRenderHint(QtGui.QPainter.Antialiasing, True) + pen = QtGui.QPen(QtCore.Qt.NoPen) + qp.setPen(pen) + qp.setBrush(QtGui.QColor(120, 120, 120)) + qp.drawRoundedRect(0, 0, s.width(), s.height(), 12, 12) + lg = QtGui.QLinearGradient(35, 30, 35, 0) + lg.setColorAt(0, QtGui.QColor(210, 210, 210, 255)) + lg.setColorAt(0.25, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(0.82, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(1, QtGui.QColor(210, 210, 210, 255)) + qp.setBrush(lg) + qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 10, 10) + + qp.setBrush(QtGui.QColor(210, 210, 210)) + qp.drawRoundedRect(2, 2, s.width() - 4, s.height() - 4, 10, 10) + + if self.__enabled: + lg = QtGui.QLinearGradient(50, 30, 35, 0) + lg.setColorAt(0, QtGui.QColor(230, 230, 230, 255)) + lg.setColorAt(0.25, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(0.82, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(1, QtGui.QColor(230, 230, 230, 255)) + qp.setBrush(lg) + qp.drawRoundedRect(3, 3, s.width() - 6, s.height() - 6, 7, 7) + else: + lg = QtGui.QLinearGradient(50, 30, 35, 0) + lg.setColorAt(0, QtGui.QColor(200, 200, 200, 255)) + lg.setColorAt(0.25, QtGui.QColor(230, 230, 230, 255)) + lg.setColorAt(0.82, QtGui.QColor(230, 230, 230, 255)) + lg.setColorAt(1, QtGui.QColor(200, 200, 200, 255)) + qp.setBrush(lg) + qp.drawRoundedRect(3, 3, s.width() - 6, s.height() - 6, 7, 7) + qp.end() + + +class Circle(QtWidgets.QWidget): + def __init__(self, parent=None) -> None: + super(Circle, self).__init__(parent) + self.__enabled = True + self.setFixedSize(20, 20) + + def paintEvent(self, event) -> None: + s = self.size() + qp = QtGui.QPainter() + qp.begin(self) + qp.setRenderHint(QtGui.QPainter.Antialiasing, True) + qp.setPen(QtCore.Qt.NoPen) + qp.setBrush(QtGui.QColor(120, 120, 120)) + qp.drawEllipse(0, 0, 20, 20) + rg = QtGui.QRadialGradient(int(self.width() / 2), int(self.height() / 2), 12) + rg.setColorAt(0, QtGui.QColor(255, 255, 255)) + rg.setColorAt(0.6, QtGui.QColor(255, 255, 255)) + rg.setColorAt(1, QtGui.QColor(205, 205, 205)) + qp.setBrush(QtGui.QBrush(rg)) + qp.drawEllipse(1, 1, 18, 18) + + qp.setBrush(QtGui.QColor(210, 210, 210)) + qp.drawEllipse(2, 2, 16, 16) + + if self.__enabled: + lg = QtGui.QLinearGradient(3, 18, 20, 4) + lg.setColorAt(0, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(0.55, QtGui.QColor(230, 230, 230, 255)) + lg.setColorAt(0.72, QtGui.QColor(255, 255, 255, 255)) + lg.setColorAt(1, QtGui.QColor(255, 255, 255, 255)) + qp.setBrush(lg) + qp.drawEllipse(3, 3, 14, 14) + else: + lg = QtGui.QLinearGradient(3, 18, 20, 4) + lg.setColorAt(0, QtGui.QColor(230, 230, 230)) + lg.setColorAt(0.55, QtGui.QColor(210, 210, 210)) + lg.setColorAt(0.72, QtGui.QColor(230, 230, 230)) + lg.setColorAt(1, QtGui.QColor(230, 230, 230)) + qp.setBrush(lg) + qp.drawEllipse(3, 3, 14, 14) + qp.end() + + +class Background(QtWidgets.QWidget): + def __init__(self, parent=None) -> None: + super(Background, self).__init__(parent) + self.__enabled = True + self.setFixedHeight(20) + + def paintEvent(self, event) -> None: + s = self.size() + qp = QtGui.QPainter() + qp.begin(self) + qp.setRenderHint(QtGui.QPainter.Antialiasing, True) + pen = QtGui.QPen(QtCore.Qt.NoPen) + qp.setPen(pen) + qp.setBrush(QtGui.QColor(154, 205, 50)) + if self.__enabled: + qp.setBrush(QtGui.QColor(154, 190, 50)) + qp.drawRoundedRect(0, 0, s.width(), s.height(), 10, 10) + + lg = QtGui.QLinearGradient(0, 25, 70, 0) + lg.setColorAt(0, QtGui.QColor(154, 184, 50)) + lg.setColorAt(0.35, QtGui.QColor(154, 210, 50)) + lg.setColorAt(0.85, QtGui.QColor(154, 184, 50)) + qp.setBrush(lg) + qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 8, 8) + else: + qp.setBrush(QtGui.QColor(150, 150, 150)) + qp.drawRoundedRect(0, 0, s.width(), s.height(), 10, 10) + + lg = QtGui.QLinearGradient(5, 25, 60, 0) + lg.setColorAt(0, QtGui.QColor(190, 190, 190)) + lg.setColorAt(0.35, QtGui.QColor(230, 230, 230)) + lg.setColorAt(0.85, QtGui.QColor(190, 190, 190)) + qp.setBrush(lg) + qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 8, 8) + qp.end() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py new file mode 100644 index 000000000..33534f734 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel +from switch_button import SwitchButton + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + switch_btn1 = SwitchButton(self, "On", 15, "Off", 31, width=60) + label_switch_btn1 = QLabel( + f"Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}" + ) + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}" + ) + ) + + switch_btn2 = SwitchButton(self, "Вкл", 15, "Откл", 31, width=120) + label_switch_btn2 = QLabel( + f"Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}" + ) + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}" + ) + ) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(switch_btn1) + main_layout.addWidget(label_switch_btn1) + + main_layout.addWidget(switch_btn2) + main_layout.addWidget(label_switch_btn2) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/ToolBarArea/main.py b/qt__pyqt__pyside__pyqode/ToolBarArea/main.py index b1ed9c8a3..d7f4625c3 100644 --- a/qt__pyqt__pyside__pyqode/ToolBarArea/main.py +++ b/qt__pyqt__pyside__pyqode/ToolBarArea/main.py @@ -1,43 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('ToolBarArea') + self.setWindowTitle("ToolBarArea") - self.toolbar = self.addToolBar('Panel2') - self.toolbar.addAction('1') - self.toolbar.addAction('2') - self.toolbar.addAction('3') + self.toolbar = self.addToolBar("Panel2") + self.toolbar.addAction("1") + self.toolbar.addAction("2") + self.toolbar.addAction("3") - self.toolbar2 = self.addToolBar('Panel1') + self.toolbar2 = self.addToolBar("Panel1") self.addToolBar(Qt.Qt.LeftToolBarArea, self.toolbar2) - self.toolbar2.addAction('a') - self.toolbar2.addAction('b') - self.toolbar2.addAction('c') + self.toolbar2.addAction("a") + self.toolbar2.addAction("b") + self.toolbar2.addAction("c") - self.toolbar3 = Qt.QToolBar('Panel3') + self.toolbar3 = Qt.QToolBar("Panel3") self.addToolBar(Qt.Qt.RightToolBarArea, self.toolbar3) - self.toolbar3.addAction('x') - self.toolbar3.addAction('y') - self.toolbar3.addAction('z') + self.toolbar3.addAction("x") + self.toolbar3.addAction("y") + self.toolbar3.addAction("z") - self.toolbar4 = Qt.QToolBar('Panel4') + self.toolbar4 = Qt.QToolBar("Panel4") self.addToolBar(Qt.Qt.BottomToolBarArea, self.toolbar4) - self.toolbar4.addAction('+') - self.toolbar4.addAction('-') - self.toolbar4.addAction('=') + self.toolbar4.addAction("+") + self.toolbar4.addAction("-") + self.toolbar4.addAction("=") -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py index 8e1c4a706..3a584fbc8 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class URLView(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -29,20 +29,20 @@ def __init__(self, parent=None): self.nam = Qt.QNetworkAccessManager() self.nam.finished.connect(self.finish_request) - def on_load(self): + def on_load(self) -> None: print("Load image") url = self.urlEdit.text() self.nam.get(Qt.QNetworkRequest(Qt.QUrl(url))) - def finish_request(self, reply): + def finish_request(self, reply) -> None: img = Qt.QPixmap() img.loadFromData(reply.readAll()) self.imageLabel.setPixmap(img) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py index 6502cd07c..bf3431aad 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5 import Qt from urllib import request +from PyQt5 import Qt class LoadImageThread(Qt.QThread): about_new_image = Qt.pyqtSignal(Qt.QPixmap) - def run(self): + def run(self) -> None: data = request.urlopen(self.url).read() pixmap = Qt.QPixmap() pixmap.loadFromData(data) @@ -21,13 +21,13 @@ def run(self): class URLView(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -47,7 +47,7 @@ def __init__(self, parent=None): self.thread.started.connect(lambda: self.loadButton.setEnabled(False)) self.thread.finished.connect(lambda: self.loadButton.setEnabled(True)) - def on_load(self): + def on_load(self) -> None: print("Load image") # Запускаем поток @@ -55,7 +55,7 @@ def on_load(self): self.thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py index 6547604f6..8c12217f3 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py @@ -1,22 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5 import Qt from urllib import request +from PyQt5 import Qt class URLView(Qt.QWidget): - - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -28,7 +27,7 @@ def __init__(self, parent=None): self.loadButton.clicked.connect(self.on_load) - def on_load(self): + def on_load(self) -> None: print("Load image") url = self.urlEdit.text() @@ -38,7 +37,7 @@ def on_load(self): self.imageLabel.setPixmap(pixmap) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/advanced_list_widget.py b/qt__pyqt__pyside__pyqode/advanced_list_widget.py new file mode 100644 index 000000000..4da454915 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/advanced_list_widget.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QListWidgetItem, QListWidget, QWidget, QVBoxLayout, QToolBar + + +class AdvancedListWidget(QWidget): + def __init__(self) -> None: + super().__init__() + + self.list_widget = QListWidget() + self.list_widget.itemSelectionChanged.connect(self._update_states) + + tool_bar = QToolBar() + self.action_append = tool_bar.addAction("➕", self._append) + self.action_remove = tool_bar.addAction("➖", self._remove) + self.action_move_up = tool_bar.addAction("🔼", self._move_up) + self.action_move_down = tool_bar.addAction("🔽", self._move_down) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(tool_bar) + main_layout.addWidget(self.list_widget) + + self._update_states() + + def _update_states(self) -> None: + current_row: int = self.list_widget.currentRow() + + is_selected: bool = current_row != -1 + self.action_remove.setVisible(is_selected) + + self.action_move_up.setVisible(is_selected and current_row > 0) + self.action_move_down.setVisible(is_selected and current_row < self.count() - 1) + + def _append(self) -> None: + item = self.append("New item") + + self.list_widget.setCurrentItem(item) + self.list_widget.editItem(item) + + def _remove(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row == -1: + return + + self.list_widget.takeItem(current_row) + + def _move_item(self, from_row: int, to_row: int) -> None: + item = self.list_widget.takeItem(from_row) + self.list_widget.insertItem(to_row, item) + self.list_widget.setCurrentItem(item) + + def _move_up(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row < 1: + return + + self._move_item(current_row, current_row - 1) + + def _move_down(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row != -1 and current_row >= self.count() - 1: + return + + self._move_item(current_row, current_row + 1) + + def set_items(self, items: list[str]) -> None: + self.clear() + self.append_all(items) + + def append(self, text: str) -> QListWidgetItem: + item = QListWidgetItem(text) + item.setFlags(item.flags() | Qt.ItemIsEditable) + + self.list_widget.addItem(item) + + return item + + def append_all(self, items: list[str]) -> None: + for text in items: + self.append(text) + + def clear(self) -> None: + self.list_widget.clear() + + def count(self) -> int: + return self.list_widget.count() + + def get(self, i: int) -> str: + return self.list_widget.item(i).text() + + def items(self) -> list[str]: + return [self.get(i) for i in range(self.count())] + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication([]) + + mw = AdvancedListWidget() + mw.set_items( + [ + "FOO", + "BAR", + "GO", + "ABC", + "123", + "!!!", + ] + ) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py b/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py new file mode 100644 index 000000000..75cee8a60 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QListWidgetItem, QListWidget, QWidget, QVBoxLayout, QToolBar + + +class AdvancedListWidget(QWidget): + def __init__(self): + super().__init__() + + self.list_widget = QListWidget() + self.list_widget.itemSelectionChanged.connect(self._update_states) + + tool_bar = QToolBar() + self.action_append = tool_bar.addAction("➕", self._append) + self.action_remove = tool_bar.addAction("➖", self._remove) + self.action_move_up = tool_bar.addAction("🔼", self._move_up) + self.action_move_down = tool_bar.addAction("🔽", self._move_down) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(tool_bar) + main_layout.addWidget(self.list_widget) + + self._update_states() + + def _update_states(self): + current_row: int = self.list_widget.currentRow() + + is_selected: bool = current_row != -1 + self.action_remove.setVisible(is_selected) + + self.action_move_up.setVisible(is_selected and current_row > 0) + self.action_move_down.setVisible(is_selected and current_row < self.count() - 1) + + def _append(self): + item = self.append("New item") + + self.list_widget.setCurrentItem(item) + self.list_widget.editItem(item) + + def _remove(self): + current_row: int = self.list_widget.currentRow() + if current_row == -1: + return + + self.list_widget.takeItem(current_row) + + def _move_item(self, from_row: int, to_row: int): + item = self.list_widget.takeItem(from_row) + self.list_widget.insertItem(to_row, item) + self.list_widget.setCurrentItem(item) + + def _move_up(self): + current_row: int = self.list_widget.currentRow() + if current_row < 1: + return + + self._move_item(current_row, current_row - 1) + + def _move_down(self): + current_row: int = self.list_widget.currentRow() + if current_row != -1 and current_row >= self.count() - 1: + return + + self._move_item(current_row, current_row + 1) + + def set_items(self, items: list[str]): + self.clear() + self.append_all(items) + + def append(self, text: str) -> QListWidgetItem: + item = QListWidgetItem(text) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) + + self.list_widget.addItem(item) + + return item + + def append_all(self, items: list[str]): + for text in items: + self.append(text) + + def clear(self): + return self.list_widget.clear() + + def count(self) -> int: + return self.list_widget.count() + + def get(self, i: int) -> str: + return self.list_widget.item(i).text() + + def items(self) -> list[str]: + return [self.get(i) for i in range(self.count())] + + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + + app = QApplication([]) + + mw = AdvancedListWidget() + mw.set_items( + [ + "FOO", + "BAR", + "GO", + "ABC", + "123", + "!!!", + ] + ) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py new file mode 100644 index 000000000..dd7ca64ea --- /dev/null +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import math + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QTransform, QPolygonF, QPen +from PyQt5.QtCore import QPointF, Qt, QTimer + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.angle = 0 + self.x = 0 + self.points = [] + + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._do_tick) + self.timer.start() + + def _do_tick(self) -> None: + self.angle += 5 + + self.x += 5 + if self.x > self.width(): + self.x = 0 + self.points.clear() + + # Перерисование окна + self.update() + + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + height = 500 + y0 = height / 4 + amplitude = height / 2 + frequency = 0.04 + + x = self.x + y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 + + triangle_points = [ + QPointF(0, 0), + QPointF(0, 50), + QPointF(50, 50), + QPointF(50, 0), + ] + for p in triangle_points: + p.setX(p.x() + x) + p.setY(p.y() + y) + + polygon = QPolygonF(triangle_points) + + center = polygon.boundingRect().center() + self.points.append(center) + + painter.save() + painter.setPen(QPen(Qt.red, 2)) + p1 = self.points[0] + for p2 in self.points[1:]: + painter.drawLine(p1, p2) + p1 = p2 + painter.restore() + + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) + .translate(-center.x(), -center.y()) + ) + + p = transform.map(polygon) + + painter.setBrush(Qt.blue) + painter.drawPolygon(p) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py new file mode 100644 index 000000000..27e1c4c38 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import math + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QTransform, QPolygonF, QPen +from PyQt5.QtCore import QPointF, Qt, QTimer + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.angle = 0 + self.x = 0 + + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._do_tick) + self.timer.start() + + def _do_tick(self) -> None: + self.angle += 5 + + self.x += 5 + if self.x > self.width(): + self.x = 0 + + # Перерисование окна + self.update() + + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + y0 = self.height() / 4 + amplitude = self.height() / 2 + frequency = 0.02 + + painter.save() + painter.setPen(QPen(Qt.black, 2)) + for x in range(self.width()): + y = y0 + amplitude / 2 + painter.drawPoint(x, y) + + painter.setPen(QPen(Qt.red, 2)) + for x in range(self.width()): + y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 + painter.drawPoint(x, y) + painter.restore() + + painter.setBrush(Qt.blue) + + x = self.x + y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 + + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] + for p in triangle_points: + p.setX(p.x() + x) + p.setY(p.y() + y) + + p = QPolygonF(triangle_points) + + center = p.boundingRect().center() + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) + .translate(-center.x(), -center.y()) + ) + + p = transform.map(p) + painter.drawPolygon(p) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py new file mode 100644 index 000000000..05f066d5e --- /dev/null +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import math + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QTransform, QPolygonF, QPen +from PyQt5.QtCore import QPointF, Qt, QTimer + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.angle = 0 + self.x = 0 + self.points = [] + + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._do_tick) + self.timer.start() + + def _do_tick(self) -> None: + self.angle += 5 + + self.x += 5 + if self.x > self.width(): + self.x = 0 + self.points.clear() + + # Перерисование окна + self.update() + + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + height = 500 + y0 = height / 4 + amplitude = height / 2 + frequency = 0.04 + + x = self.x + y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 + + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] + for p in triangle_points: + p.setX(p.x() + x) + p.setY(p.y() + y) + + polygon = QPolygonF(triangle_points) + + center = polygon.boundingRect().center() + self.points.append(center) + + painter.save() + painter.setPen(QPen(Qt.red, 2)) + p1 = self.points[0] + for p2 in self.points[1:]: + painter.drawLine(p1, p2) + p1 = p2 + painter.restore() + + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) + .translate(-center.x(), -center.y()) + ) + + p = transform.map(polygon) + + painter.setBrush(Qt.blue) + painter.drawPolygon(p) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py new file mode 100644 index 000000000..38ad62cf4 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QPainterPath, QTransform +from PyQt5.QtCore import QPointF, Qt, QRectF, QTimer + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.angle = 0 + + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._do_tick) + self.timer.start() + + def _do_tick(self) -> None: + self.angle += 5 + + # Перерисования окна + self.update() + + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + x, y = 100, 100 + + rect = QRectF(0, 0, 50, 50) + + path = QPainterPath() + path.moveTo(rect.left() + (rect.width() / 2), rect.top()) + path.lineTo(rect.bottomLeft()) + path.lineTo(rect.bottomRight()) + path.lineTo(rect.left() + (rect.width() / 2), rect.top()) + + for i in range(path.elementCount()): + point = QPointF(path.elementAt(i)) + point += QPointF(x, y) + path.setElementPositionAt(i, point.x(), point.y()) + + center = rect.center() + transform = ( + QTransform() + .translate(center.x() + x, center.y() + y) + .rotate(self.angle) + .translate(-center.x() - x, -center.y() - y) + ) + + path = transform.map(path) + + painter.fillPath(path, Qt.blue) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py new file mode 100644 index 000000000..5749c31fc --- /dev/null +++ b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QTransform, QPolygonF +from PyQt5.QtCore import QPointF, Qt, QTimer + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.angle = 0 + + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._do_tick) + self.timer.start() + + def _do_tick(self) -> None: + self.angle += 5 + + # Перерисования окна + self.update() + + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + x, y = 100, 100 + + painter.setBrush(Qt.blue) + + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] + for p in triangle_points: + p.setX(p.x() + x) + p.setY(p.y() + y) + + p = QPolygonF(triangle_points) + + center = p.boundingRect().center() + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) + .translate(-center.x(), -center.y()) + ) + + p = transform.map(p) + painter.drawPolygon(p) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py b/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py index 9efdfca20..43224f8e8 100644 --- a/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py +++ b/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.indicator = Qt.QLabel() self.indicator.hide() self.indicator.setFixedSize(32, 32) self.indicator.setScaledContents(True) - self.indicator.setPixmap(Qt.QPixmap('light-bulb-icon_34400.png')) + self.indicator.setPixmap(Qt.QPixmap("light-bulb-icon_34400.png")) self.line_edit = Qt.QLineEdit() self.line_edit.textEdited.connect(self._on_text_edited) @@ -25,14 +25,17 @@ def __init__(self): self.status_bar.setSizeGripEnabled(False) # Убираем снизу-справа уголок main_layout = Qt.QVBoxLayout() - main_layout.setContentsMargins(5, 5, 5, 0) # Сверху, слева и справа отступ 5, внизу его нет + # Сверху, слева и справа отступ 5, внизу его нет + main_layout.setContentsMargins( + 5, 5, 5, 0 + ) main_layout.addWidget(self.line_edit) main_layout.addStretch() main_layout.addWidget(self.status_bar) self.setLayout(main_layout) - def _on_text_edited(self, text): + def _on_text_edited(self, text) -> None: # Показываем сообщение на 2 секунды self.status_bar.showMessage(text, msecs=2000) @@ -43,7 +46,7 @@ def _on_text_edited(self, text): Qt.QTimer.singleShot(2000, self.indicator.hide) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py b/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py index 77eb3a201..cda5582f3 100644 --- a/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py +++ b/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.line_edit = Qt.QLineEdit() @@ -19,13 +19,13 @@ def __init__(self): self.setLayout(layout) - def on_text_changed(self, text): + def on_text_changed(self, text) -> None: # Рассчитываем ширину текст по шрифту width = self.line_edit.fontMetrics().width(text) self.line_edit.setMinimumWidth(width) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = Widget() diff --git a/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py b/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py index 609467bc7..064c10a6a 100644 --- a/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py +++ b/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -12,7 +12,7 @@ class MainWidow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = self.append_series() @@ -48,6 +48,7 @@ def append_series(self): if __name__ == "__main__": import sys + app = QApplication(sys.argv) mw = MainWidow() diff --git a/qt__pyqt__pyside__pyqode/buttons_setShortcut.py b/qt__pyqt__pyside__pyqode/buttons_setShortcut.py new file mode 100644 index 000000000..d2150a9cc --- /dev/null +++ b/qt__pyqt__pyside__pyqode/buttons_setShortcut.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, QLabel + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + label_result = QLabel() + + button_left = QPushButton("Left") + button_left.setShortcut("Ctrl+Alt+Left") + button_left.clicked.connect(lambda: label_result.setText("left")) + + button_right = QPushButton("Right") + button_right.setShortcut("Ctrl+Alt+Right") + button_right.clicked.connect(lambda: label_result.setText("right")) + + main_layout = QHBoxLayout(self) + main_layout.addWidget(button_left) + main_layout.addWidget(label_result) + main_layout.addWidget(button_right) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/chart_line__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__QtChart.py index 020e2e975..1035e2e11 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -34,7 +34,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py index 666007d6f..62e431635 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -35,7 +35,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py index 837eb065e..0c5545dcd 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qtcharts-datetimeaxis-example.html @@ -10,7 +10,6 @@ from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis - from PyQt5.QtCore import QDateTime, Qt @@ -25,7 +24,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -43,12 +42,12 @@ def __init__(self): axisX = QDateTimeAxis() axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') + axisX.setTitleText("Date") chart.addAxis(axisX, Qt.AlignBottom) series.attachAxis(axisX) axisY = QValueAxis() - axisY.setTitleText('Value') + axisY.setTitleText("Value") chart.addAxis(axisY, Qt.AlignLeft) series.attachAxis(axisY) @@ -59,7 +58,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py index f8aa9f212..e29a2278e 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py @@ -1,19 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qtcharts-datetimeaxis-example.html -import datetime as DT - +import datetime as dt from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis - from PyQt5.QtCore import Qt @@ -28,13 +26,13 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() for date, value in DATA: - date_value = DT.datetime(*date).timestamp() * 1000 + date_value = dt.datetime(*date).timestamp() * 1000 series.append(date_value, value) chart = QChart() @@ -46,13 +44,13 @@ def __init__(self): axisX = QDateTimeAxis() axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') + axisX.setTitleText("Date") # axisX.setTickCount(10) chart.addAxis(axisX, Qt.AlignBottom) series.attachAxis(axisX) axisY = QValueAxis() - axisY.setTitleText('Value') + axisY.setTitleText("Value") chart.addAxis(axisY, Qt.AlignLeft) series.attachAxis(axisY) @@ -63,7 +61,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py index 9c9219212..59aa609b7 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -31,7 +31,7 @@ def create_QChartView() -> QChartView: return chart_view -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = create_QChartView() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py index da5c51836..906164059 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py @@ -1,18 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List, Tuple - from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries -def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: - NAME = 'ChartTheme' +def get_themes() -> list[tuple[str, QChart.ChartTheme]]: + NAME = "ChartTheme" themes = [] for theme_name in dir(QChart): @@ -20,7 +18,7 @@ def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: continue theme = getattr(QChart, theme_name) - theme_name = theme_name.replace(NAME, '') + theme_name = theme_name.replace(NAME, "") themes.append((theme_name, theme)) @@ -29,13 +27,13 @@ def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() tab_widget = QTabWidget() chart_view = self.create_QChartView() - tab_widget.addTab(chart_view, '') + tab_widget.addTab(chart_view, "") for name, theme in get_themes(): chart_view = self.create_QChartView() @@ -67,7 +65,7 @@ def create_QChartView(self) -> QChartView: return chart_view -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py index b8f0f56a9..75a63f16a 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -37,7 +37,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py index 81277edde..ed1edad19 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py @@ -1,21 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math from PyQt5.QtWidgets import QApplication -from PyQt5.QtGui import QPainter -from PyQt5.QtCore import QRectF, QPoint, QSizeF -from PyQt5.QtChart import QChart, QChartView, QLineSeries +from PyQt5.QtChart import QChart, QLineSeries from chart_line__show_tooltip_on_series__QtChart import Callout, ChartViewToolTips class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -40,7 +38,7 @@ def __init__(self): self.setChart(self._chart) - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: if not self._tooltip: self._tooltip = Callout(self._chart) @@ -53,7 +51,7 @@ def show_series_tooltip(self, point, state: bool): + (p.y() - point.y()) * (p.y() - point.y()) ) if current_distance < distance: - self._tooltip.setText("X: {}\nY: {}".format(p.x(), p.y())) + self._tooltip.setText(f"X: {p.x()}\nY: {p.y()}") self._tooltip.setAnchor(p) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -62,7 +60,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip.hide() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py index fd900bfa7..81aa59b79 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -13,7 +13,7 @@ class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -38,7 +38,7 @@ def __init__(self): self.setChart(self._chart) - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: # value -> pos point = self._chart.mapToPosition(point) @@ -56,7 +56,7 @@ def show_series_tooltip(self, point, state: bool): + (p.y() - point.y()) * (p.y() - point.y()) ) if current_distance < distance: - self._tooltip.setText("X: {}\nY: {}".format(p.x(), p.y())) + self._tooltip.setText(f"X: {p.x()}\nY: {p.y()}") self._tooltip.setAnchor(p_value) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -65,7 +65,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip.hide() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py index 9fa752c33..9faf17c25 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsSceneMouseEvent @@ -12,7 +12,7 @@ # SOURCE: https://doc-snapshots.qt.io/qt5-5.12/qtcharts-callout-callout-h.html class Callout(QGraphicsItem): - def __init__(self, chart, parent=None): + def __init__(self, chart: QChart, parent=None) -> None: super().__init__(parent) self.hide() @@ -20,29 +20,47 @@ def __init__(self, chart, parent=None): self._chart = chart self._chart.scene().addItem(self) - self._text = '' + self._text = "" self._textRect = QRectF() self._rect = QRectF() self._anchor = QPointF() self._font = QFont() - def setText(self, text): + def setText(self, text: str) -> None: self._text = text metrics = QFontMetrics(self._font) - self._textRect = QRectF(metrics.boundingRect( - QRect(0, 0, 150, 150), Qt.AlignLeft, self._text - )) + self._textRect = QRectF( + metrics.boundingRect(QRect(0, 0, 150, 150), Qt.AlignLeft, self._text) + ) self._textRect.translate(5, 5) self.prepareGeometryChange() self._rect = QRectF(self._textRect.adjusted(-5, -5, 5, 5)) - def setAnchor(self, point): + def setAnchor(self, point: QPointF) -> None: self._anchor = point - def updateGeometry(self): + def updateGeometry(self) -> None: self.prepareGeometryChange() self.setPos(self._chart.mapToPosition(self._anchor) + QPoint(10, -50)) + rect = self.sceneBoundingRect() + + # Correction position by top + if rect.top() < 0: + self.setY(0) + + # Correction position by right + view_width = self._chart.rect().width() + if rect.right() > view_width: + rect.moveRight(view_width) + self.setX(rect.x()) + + # Correction position by bottom + view_height = self._chart.rect().height() + if rect.bottom() > view_height: + rect.moveBottom(view_height) + self.setY(rect.y()) + def boundingRect(self): anchor = self.mapFromParent(self._chart.mapToPosition(self._anchor)) rect = QRectF() @@ -52,42 +70,77 @@ def boundingRect(self): rect.setBottom(max(self._rect.bottom(), anchor.y())) return rect - def paint(self, painter: QPainter, option, widget = None): + def paint(self, painter: QPainter, option, widget=None) -> None: path = QPainterPath() path.addRoundedRect(self._rect, 5, 5) - + anchor = self.mapFromParent(self._chart.mapToPosition(self._anchor)) if not self._rect.contains(anchor): point1 = QPointF() point2 = QPointF() - + # establish the position of the anchor point in relation to self._rect above = anchor.y() <= self._rect.top() - aboveCenter = anchor.y() > self._rect.top() and anchor.y() <= self._rect.center().y() - belowCenter = anchor.y() > self._rect.center().y() and anchor.y() <= self._rect.bottom() + aboveCenter = ( + anchor.y() > self._rect.top() and anchor.y() <= self._rect.center().y() + ) + belowCenter = ( + anchor.y() > self._rect.center().y() + and anchor.y() <= self._rect.bottom() + ) below = anchor.y() > self._rect.bottom() - + onLeft = anchor.x() <= self._rect.left() - leftOfCenter = anchor.x() > self._rect.left() and anchor.x() <= self._rect.center().x() - rightOfCenter = anchor.x() > self._rect.center().x() and anchor.x() <= self._rect.right() + leftOfCenter = ( + anchor.x() > self._rect.left() and anchor.x() <= self._rect.center().x() + ) + rightOfCenter = ( + anchor.x() > self._rect.center().x() + and anchor.x() <= self._rect.right() + ) onRight = anchor.x() > self._rect.right() - + # get the nearest self._rect corner. x = (onRight + rightOfCenter) * self._rect.width() y = (below + belowCenter) * self._rect.height() - cornerCase = (above and onLeft) or (above and onRight) or (below and onLeft) or (below and onRight) + cornerCase = ( + (above and onLeft) + or (above and onRight) + or (below and onLeft) + or (below and onRight) + ) vertical = abs(anchor.x() - x) > abs(anchor.y() - y) - - x1 = x + leftOfCenter * 10 - rightOfCenter * 20 + cornerCase * (not vertical) * (onLeft * 10 - onRight * 20) - y1 = y + aboveCenter * 10 - belowCenter * 20 + cornerCase * vertical * (above * 10 - below * 20) + + x1 = ( + x + + leftOfCenter * 10 + - rightOfCenter * 20 + + cornerCase * (not vertical) * (onLeft * 10 - onRight * 20) + ) + y1 = ( + y + + aboveCenter * 10 + - belowCenter * 20 + + cornerCase * vertical * (above * 10 - below * 20) + ) point1.setX(x1) point1.setY(y1) - - x2 = x + leftOfCenter * 20 - rightOfCenter * 10 + cornerCase * (not vertical) * (onLeft * 20 - onRight * 10) - y2 = y + aboveCenter * 20 - belowCenter * 10 + cornerCase * vertical * (above * 20 - below * 10) + + x2 = ( + x + + leftOfCenter * 20 + - rightOfCenter * 10 + + cornerCase * (not vertical) * (onLeft * 20 - onRight * 10) + ) + y2 = ( + y + + aboveCenter * 20 + - belowCenter * 10 + + cornerCase * vertical * (above * 20 - below * 10) + ) point2.setX(x2) point2.setY(y2) - + path.moveTo(point1) path.lineTo(anchor) path.lineTo(point2) @@ -98,19 +151,21 @@ def paint(self, painter: QPainter, option, widget = None): painter.drawPath(path) painter.drawText(self._textRect, self._text) - def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: event.setAccepted(True) - def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): + def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: if event.buttons() & Qt.LeftButton: - self.setPos(self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton))) + self.setPos( + self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton)) + ) event.setAccepted(True) else: event.setAccepted(False) class ChartViewToolTips(QChartView): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setRenderHint(QPainter.Antialiasing) @@ -119,7 +174,7 @@ def __init__(self): self._callout_font_family = None self._callouts = [] - def clear_all_tooltips(self): + def clear_all_tooltips(self) -> None: if self._tooltip: self.scene().removeItem(self._tooltip) @@ -134,7 +189,7 @@ def _add_Callout(self) -> Callout: return callout - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: if not self.chart(): return @@ -142,7 +197,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip = self._add_Callout() if state: - self._tooltip.setText("X: {} \nY: {}".format(point.x(), point.y())) + self._tooltip.setText(f"X: {point.x()} \nY: {point.y()}") self._tooltip.setAnchor(point) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -150,7 +205,7 @@ def show_series_tooltip(self, point, state: bool): else: self._tooltip.hide() - def keepCallout(self, point): + def keepCallout(self, point) -> None: if not self.chart() or not self._tooltip: return @@ -159,7 +214,7 @@ def keepCallout(self, point): self._tooltip = self._add_Callout() - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event) -> None: if self.chart(): pos = event.pos() point = self.chart().mapToValue(pos) @@ -167,7 +222,7 @@ def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: if self.scene(): size = QSizeF(event.size()) @@ -181,7 +236,7 @@ def resizeEvent(self, event): class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -207,7 +262,7 @@ def __init__(self): self.setChart(self._chart) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/check_image_on_transparents/emoji-video-game-512x512.png b/qt__pyqt__pyside__pyqode/check_image_on_transparents/emoji-video-game-512x512.png new file mode 100644 index 000000000..344fb4a6a Binary files /dev/null and b/qt__pyqt__pyside__pyqode/check_image_on_transparents/emoji-video-game-512x512.png differ diff --git a/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py b/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py new file mode 100644 index 000000000..c79b07e5c --- /dev/null +++ b/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QPixmap +from PyQt5.QtCore import Qt, QRect + + +FILE_NAME = "emoji-video-game-512x512.png" + + +class Widget(QWidget): + def __init__(self) -> None: + super().__init__() + + self.img = QPixmap() + self.img.load(FILE_NAME) + + def paintEvent(self, event) -> None: + painter = QPainter(self) + + painter.setBrush(Qt.darkGreen) + painter.drawRect(self.rect()) + + img_rect = self.img.rect() + dev_rect = QRect(0, 0, painter.device().width(), painter.device().height()) + img_rect.moveCenter(dev_rect.center()) + + painter.drawPixmap(img_rect.topLeft(), self.img) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = Widget() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/check_urls.py b/qt__pyqt__pyside__pyqode/check_urls.py new file mode 100644 index 000000000..28ad3b224 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/check_urls.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import traceback +import sys + +import requests + +from PyQt5.QtWidgets import ( + QApplication, + QMainWindow, + QTableWidget, + QTableWidgetItem, + QMessageBox, + QPushButton, + QHBoxLayout, + QVBoxLayout, + QWidget, + QPlainTextEdit, +) +from PyQt5.QtCore import QThread, pyqtSignal, Qt + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print(text) + QMessageBox.critical(None, "Error", text) + sys.exit() + + +sys.excepthook = log_uncaught_exceptions + + +session = requests.session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0" + + +class CheckUrlThread(QThread): + about_check_url = pyqtSignal(str, str) + + def __init__(self, urls: list[str] = None) -> None: + super().__init__() + + self.urls = urls if urls else [] + + def run(self) -> None: + for url in self.urls: + try: + rs = session.get(url) + code = rs.status_code + + except Exception as e: + # Пусть будет исключение + code = e + + code = str(code) + + self.about_check_url.emit(url, code) + + +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__() + + self.urls = QPlainTextEdit() + + headers = ["URL", "CODE"] + + self.result_table = QTableWidget() + self.result_table.setColumnCount(len(headers)) + self.result_table.setHorizontalHeaderLabels(headers) + self.result_table.setAlternatingRowColors(True) + self.result_table.horizontalHeader().setStretchLastSection(True) + self.result_table.horizontalHeader().resizeSection(0, 200) + + self.pb_check = QPushButton("Check") + self.pb_check.clicked.connect(self._on_click_check) + + layout = QHBoxLayout() + layout.addWidget(self.urls) + layout.addWidget(self.result_table) + + main_layout = QVBoxLayout() + main_layout.addLayout(layout) + main_layout.addWidget(self.pb_check) + + central_widget = QWidget() + central_widget.setLayout(main_layout) + self.setCentralWidget(central_widget) + + self.thread = CheckUrlThread() + self.thread.about_check_url.connect(self._on_about_check_url) + self.thread.started.connect(lambda: self.pb_check.setEnabled(False)) + self.thread.finished.connect(lambda: self.pb_check.setEnabled(True)) + + def _on_click_check(self) -> None: + urls = self.urls.toPlainText().strip().splitlines() + + self.result_table.clearContents() + self.result_table.setRowCount(0) + + for url in urls: + row = self.result_table.rowCount() + self.result_table.setRowCount(row + 1) + + self.result_table.setItem(row, 0, QTableWidgetItem(url)) + self.result_table.setItem(row, 1, QTableWidgetItem()) + + self.thread.urls = urls + self.thread.start() + + def _on_about_check_url(self, url: str, code: str) -> None: + for item in self.result_table.findItems(url, Qt.MatchCaseSensitive): + row = item.row() + self.result_table.item(row, 1).setText(code) + + +if __name__ == "__main__": + app = QApplication([]) + + text = "\n".join( + [ + "https://ru.stackoverflow.com", + "https://ru.stackoverflow.com/questions/893436/", + "https://google.com", + "http://ya.ru", + "http://not_found_site.123", + ] + ) + + mw = MainWindow() + mw.urls.setPlainText(text) + mw.resize(800, 600) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/clickable_image/main.py b/qt__pyqt__pyside__pyqode/clickable_image/main.py index f2520e183..85792d283 100644 --- a/qt__pyqt__pyside__pyqode/clickable_image/main.py +++ b/qt__pyqt__pyside__pyqode/clickable_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -10,17 +10,17 @@ app = Qt.QApplication([]) button = Qt.QToolButton() -button.setIcon(Qt.QIcon('img.png')) +button.setIcon(Qt.QIcon("img.png")) button.setAutoRaise(True) button.setMinimumSize(40, 50) button.setIconSize(button.minimumSize()) layout = Qt.QVBoxLayout() -layout.addWidget(Qt.QLabel('Example:')) +layout.addWidget(Qt.QLabel("Example:")) layout.addWidget(button) central = Qt.QWidget() -central.setStyleSheet('background-color: green;') +central.setStyleSheet("background-color: green;") central.setLayout(layout) mw = Qt.QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/clipboard__example.py b/qt__pyqt__pyside__pyqode/clipboard__example.py index 1c8d522bd..c4e46d003 100644 --- a/qt__pyqt__pyside__pyqode/clipboard__example.py +++ b/qt__pyqt__pyside__pyqode/clipboard__example.py @@ -1,29 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QPushButton, QMessageBox +import sys +import traceback + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QGridLayout, + QLabel, + QPushButton, + QMessageBox, +) from PyQt5.QtCore import Qt -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlag(Qt.WindowStaysOnTopHint) @@ -36,13 +44,13 @@ def __init__(self): button = QPushButton(x) button.clicked.connect(lambda ok, x=x: QApplication.clipboard().setText(x)) - layout.addWidget(QLabel(f'#{i}'), 0, i, Qt.AlignHCenter) + layout.addWidget(QLabel(f"#{i}"), 0, i, Qt.AlignHCenter) layout.addWidget(button, 1, i) self.setLayout(layout) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py b/qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py similarity index 89% rename from qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py rename to qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py index 6c38e5d1d..d8c3c2d45 100644 --- a/qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py +++ b/qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QSystemTrayIcon, QStyle, QLabel, QWidgetAction, QMenu @@ -15,7 +15,7 @@ tray = QSystemTrayIcon(icon) -menu_widget = QLabel('Hello, World!') +menu_widget = QLabel("Hello, World!") menu_widget_action = QWidgetAction(menu_widget) menu_widget_action.setDefaultWidget(menu_widget) diff --git a/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py b/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py new file mode 100644 index 000000000..66b358a0b --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.Qt import QApplication, QSystemTrayIcon, QStyle, QMenu, QMessageBox, QWidget + + +app = QApplication([]) +# app.setQuitOnLastWindowClosed(False) + +mw = QWidget() +mw.show() + +icon = app.style().standardIcon(QStyle.SP_DirOpenIcon) +# OR +# icon = QIcon('favicon.ico') + +tray = QSystemTrayIcon(icon) + +menu = QMenu() + +menu_about = menu.addMenu("About") +action_about = menu_about.addAction("About") +action_about.triggered.connect( + lambda: QMessageBox.information(None, "Info", "Hello World!") +) + +action_about_qt = menu_about.addAction("About Qt") +action_about_qt.triggered.connect(lambda: QMessageBox.aboutQt(None)) + +menu.addSeparator() +action_quit = menu.addAction("Quit") +action_quit.triggered.connect(app.quit) + +tray.setContextMenu(menu) + + +def on_tray_activated(reason: QSystemTrayIcon.ActivationReason) -> None: + if reason == QSystemTrayIcon.ActivationReason.Context: + return + + mw.setVisible(not mw.isVisible()) + + +tray.activated.connect(on_tray_activated) + +tray.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/context_menu/table.py b/qt__pyqt__pyside__pyqode/context_menu/table.py new file mode 100644 index 000000000..f64040069 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/table.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from PyQt5.QtCore import QPoint, Qt, QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QTableView, + QMenu, +) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + header_labels = ["NAME", "URL"] + + self.table_widget = QTableWidget() + self.table_widget.setEditTriggers(QTableView.NoEditTriggers) + self.table_widget.setSelectionBehavior(QTableView.SelectRows) + self.table_widget.setSelectionMode(QTableView.SingleSelection) + self.table_widget.setColumnCount(len(header_labels)) + self.table_widget.setHorizontalHeaderLabels(header_labels) + self.table_widget.horizontalHeader().setStretchLastSection(True) + self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_widget.customContextMenuRequested.connect( + self._custom_menu_requested + ) + + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(header_labels) + + self.table_view = QTableView() + self.table_view.setEditTriggers(QTableView.NoEditTriggers) + self.table_view.setSelectionBehavior(QTableView.SelectRows) + self.table_view.setSelectionMode(QTableView.SingleSelection) + self.table_view.horizontalHeader().setStretchLastSection(True) + self.table_view.setModel(self.model) + self.table_view.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_view.customContextMenuRequested.connect(self._custom_menu_requested) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(self.table_view) + main_layout.addWidget(self.table_widget) + + self.fill_tables() + + def fill_tables(self) -> None: + self.model.appendRow( + [QStandardItem("rutube"), QStandardItem("https://rutube.ru/")] + ) + self.model.appendRow( + [QStandardItem("yandex"), QStandardItem("https://yandex.ru/")] + ) + self.model.appendRow( + [QStandardItem("youtube"), QStandardItem("https://www.youtube.com")] + ) + self.model.appendRow( + [QStandardItem("google"), QStandardItem("https://google.com/")] + ) + + self.table_widget.setRowCount(self.model.rowCount()) + for row in range(self.model.rowCount()): + for column in range(self.model.columnCount()): + text = self.model.item(row, column).text() + self.table_widget.setItem(row, column, QTableWidgetItem(text)) + + def _custom_menu_requested(self, p: QPoint) -> None: + table = self.sender() + + index: QModelIndex = table.indexAt(p) + if not index.isValid(): + return + + row: int = index.row() + + if isinstance(table, QTableWidget): + name = table.item(row, 0).text() + url = table.item(row, 1).text() + else: + name = self.model.item(row, 0).text() + url = self.model.item(row, 1).text() + + menu = QMenu(self) + menu.addAction(table.__class__.__name__).setEnabled(False) + menu.addSeparator() + menu.addAction("Copy NAME", lambda: QApplication.clipboard().setText(name)) + menu.addAction("Copy URL", lambda: QApplication.clipboard().setText(url)) + menu.addSeparator() + menu.addAction("Open URL", lambda: os.startfile(url)) + menu.exec(table.viewport().mapToGlobal(p)) + + +if __name__ == "__main__": + app = QApplication([]) + + w = MainWindow() + w.resize(600, 500) + w.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py b/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py new file mode 100644 index 000000000..7722d202c --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +from typing import Callable + +from PyQt5.QtCore import QPoint, Qt, QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QTableView, + QMenu, + QAction, +) + + +def open_context_menu( + table: QTableView, + p: QPoint, + get_additional_actions_func: Callable[[QTableView, int], list[QAction]] = None, +) -> None: + index: QModelIndex = table.indexAt(p) + if not index.isValid(): + return + + menu = QMenu(table) + menu.addAction(table.__class__.__name__).setEnabled(False) + menu.addSeparator() + + row: int = index.row() + model = table.model() + + for column in range(model.columnCount()): + title = model.headerData(column, Qt.Horizontal) + + idx: QModelIndex = model.index(row, column) + value: str = str(model.data(idx)) + + menu.addAction( + f'Copy "{title}"', + lambda value=value: QApplication.clipboard().setText(value), + ) + + if get_additional_actions_func: + if actions := get_additional_actions_func(table, row): + menu.addSeparator() + menu.addActions(actions) + + menu.exec(table.viewport().mapToGlobal(p)) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + header_labels = ["NAME", "URL"] + + self.table_widget = QTableWidget() + self.table_widget.setEditTriggers(QTableView.NoEditTriggers) + self.table_widget.setSelectionBehavior(QTableView.SelectRows) + self.table_widget.setSelectionMode(QTableView.SingleSelection) + self.table_widget.setColumnCount(len(header_labels)) + self.table_widget.setHorizontalHeaderLabels(header_labels) + self.table_widget.horizontalHeader().setStretchLastSection(True) + self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_widget.customContextMenuRequested.connect( + self._custom_menu_requested + ) + + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(header_labels) + + self.table_view = QTableView() + self.table_view.setEditTriggers(QTableView.NoEditTriggers) + self.table_view.setSelectionBehavior(QTableView.SelectRows) + self.table_view.setSelectionMode(QTableView.SingleSelection) + self.table_view.horizontalHeader().setStretchLastSection(True) + self.table_view.setModel(self.model) + self.table_view.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_view.customContextMenuRequested.connect(self._custom_menu_requested) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(self.table_view) + main_layout.addWidget(self.table_widget) + + self.fill_tables() + + def fill_tables(self) -> None: + self.model.appendRow( + [QStandardItem("rutube"), QStandardItem("https://rutube.ru/")] + ) + self.model.appendRow( + [QStandardItem("yandex"), QStandardItem("https://yandex.ru/")] + ) + self.model.appendRow( + [QStandardItem("youtube"), QStandardItem("https://www.youtube.com")] + ) + self.model.appendRow( + [QStandardItem("google"), QStandardItem("https://google.com/")] + ) + + self.table_widget.setRowCount(self.model.rowCount()) + for row in range(self.model.rowCount()): + for column in range(self.model.columnCount()): + text = self.model.item(row, column).text() + self.table_widget.setItem(row, column, QTableWidgetItem(text)) + + def _custom_menu_requested(self, p: QPoint) -> None: + def _get_additional_actions(table: QTableView, row: int) -> list[QAction]: + model = table.model() + + actions = [] + for column in range(model.columnCount()): + idx = model.index(row, column) + value: str = str(model.data(idx)) + if not value.startswith("http"): + continue + + action_open_url = QAction(f'Open "{value}"') + action_open_url.triggered.connect(lambda: os.startfile(value)) + + actions.append(action_open_url) + + return actions + + table: QTableView = self.sender() + open_context_menu(table, p, _get_additional_actions) + + +if __name__ == "__main__": + app = QApplication([]) + + w = MainWindow() + w.resize(600, 500) + w.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/create_child_window_by_center.py b/qt__pyqt__pyside__pyqode/create_child_window_by_center.py index 5f3c7a3e4..b4835f743 100644 --- a/qt__pyqt__pyside__pyqode/create_child_window_by_center.py +++ b/qt__pyqt__pyside__pyqode/create_child_window_by_center.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QStyle @@ -9,19 +9,19 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - pb_1 = QPushButton('Create [1]') + pb_1 = QPushButton("Create [1]") pb_1.clicked.connect(self._create_new_window_1) - pb_2 = QPushButton('Create [2]') + pb_2 = QPushButton("Create [2]") pb_2.clicked.connect(self._create_new_window_2) - pb_3 = QPushButton('Create [3]') + pb_3 = QPushButton("Create [3]") pb_3.clicked.connect(self._create_new_window_3) - pb_4 = QPushButton('Create [4]') + pb_4 = QPushButton("Create [4]") pb_4.clicked.connect(self._create_new_window_4) layout = QVBoxLayout() @@ -33,12 +33,12 @@ def __init__(self): self.setLayout(layout) - def _create_new_window_1(self): + def _create_new_window_1(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) widget.show() - def _create_new_window_2(self): + def _create_new_window_2(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) @@ -47,7 +47,7 @@ def _create_new_window_2(self): widget.show() - def _create_new_window_3(self): + def _create_new_window_3(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) @@ -58,15 +58,19 @@ def _create_new_window_3(self): widget.show() - def _create_new_window_4(self): + def _create_new_window_4(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) - widget.setGeometry(QStyle.alignedRect(Qt.LeftToRight, Qt.AlignCenter, widget.size(), self.geometry())) + widget.setGeometry( + QStyle.alignedRect( + Qt.LeftToRight, Qt.AlignCenter, widget.size(), self.geometry() + ) + ) widget.show() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py b/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py index 995d3ea6e..1f275d954 100644 --- a/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py +++ b/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -9,27 +9,27 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/7c3e004f8038cf7942226283fe0f5c184008b561/xor_crypto.py def crypto_xor_1(message, secret): - return ''.join(chr(ord(c) ^ secret) for c in message) + return "".join(chr(ord(c) ^ secret) for c in message) class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('crypto / decrypto') + self.setWindowTitle("crypto / decrypto") - self.pte_text = Qt.QPlainTextEdit('Test / Тест') + self.pte_text = Qt.QPlainTextEdit("Test / Тест") self.sb_key = Qt.QSpinBox() self.sb_key.setRange(1, 1000000) self.sb_key.setValue(42) self.sb_key.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Preferred) - self.pb_convert = Qt.QPushButton('Convert') + self.pb_convert = Qt.QPushButton("Convert") self.pb_convert.clicked.connect(self._on_convert) layout_command = Qt.QHBoxLayout() - layout_command.addWidget(Qt.QLabel('Key:')) + layout_command.addWidget(Qt.QLabel("Key:")) layout_command.addWidget(self.sb_key) layout = Qt.QVBoxLayout() @@ -39,7 +39,7 @@ def __init__(self): self.setLayout(layout) - def _on_convert(self): + def _on_convert(self) -> None: text = self.pte_text.toPlainText() key = self.sb_key.value() @@ -48,7 +48,7 @@ def _on_convert(self): self.pte_text.setPlainText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py b/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py index cfcfae003..b2affa7f6 100644 --- a/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py +++ b/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -12,16 +12,16 @@ class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - uic.loadUi('mainwidget.ui', self) + uic.loadUi("mainwidget.ui", self) - self.cbPenStyle.addItem('Solid', Qt.Qt.SolidLine) - self.cbPenStyle.addItem('Dash', Qt.Qt.DashLine) - self.cbPenStyle.addItem('Dot', Qt.Qt.DotLine) - self.cbPenStyle.addItem('Dash Dot', Qt.Qt.DashDotLine) - self.cbPenStyle.addItem('Dash Dot Dot', Qt.Qt.DashDotDotLine) + self.cbPenStyle.addItem("Solid", Qt.Qt.SolidLine) + self.cbPenStyle.addItem("Dash", Qt.Qt.DashLine) + self.cbPenStyle.addItem("Dot", Qt.Qt.DotLine) + self.cbPenStyle.addItem("Dash Dot", Qt.Qt.DashDotLine) + self.cbPenStyle.addItem("Dash Dot Dot", Qt.Qt.DashDotDotLine) self.pen_color = Qt.Qt.green @@ -30,7 +30,7 @@ def __init__(self): self._update_pen_color() - def _choose_color(self): + def _choose_color(self) -> None: color = Qt.QColorDialog.getColor(self.pen_color) if not color.isValid(): return @@ -38,21 +38,21 @@ def _choose_color(self): self.pen_color = color self._update_pen_color() - def _update_pen_color(self): + def _update_pen_color(self) -> None: pixmap = Qt.QPixmap(self.pbPenColor.size()) pixmap.fill(self.pen_color) self.pbPenColor.setIcon(Qt.QIcon(pixmap)) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw1 = MainWindow() - mw1.setWindowTitle('Standard') + mw1.setWindowTitle("Standard") mw1.show() mw2 = MainWindow() - mw2.setWindowTitle('Dark') + mw2.setWindowTitle("Dark") mw2.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) mw2.show() diff --git a/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py b/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py index 4202eea32..029588bef 100644 --- a/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py +++ b/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QWidget, QVBoxLayout, QComboBox diff --git a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py index 9b100a1bd..1fa1f3891 100644 --- a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py @@ -1,23 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QPixmap from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem, QGraphicsTextItem + QApplication, + QWidget, + QVBoxLayout, + QGraphicsScene, + QGraphicsView, + QGraphicsPixmapItem, + QGraphicsTextItem, ) class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.scene = QGraphicsScene() - self.scene.addItem(QGraphicsTextItem('loading...')) + self.scene.addItem(QGraphicsTextItem("loading...")) self.view = QGraphicsView() self.view.setScene(self.scene) @@ -33,7 +39,7 @@ def __init__(self): self.setLayout(layout) - def finish_request(self, reply): + def finish_request(self, reply) -> None: self.scene.clear() img = QPixmap() @@ -43,7 +49,7 @@ def finish_request(self, reply): self.scene.addItem(item) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py index 92c684af3..0b06c223c 100644 --- a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QPixmap @@ -11,7 +11,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lbl = QLabel("loading...") @@ -28,14 +28,14 @@ def __init__(self): self.setLayout(layout) - def finish_request(self, reply): + def finish_request(self, reply) -> None: img = QPixmap() img.loadFromData(reply.readAll()) self.lbl.setPixmap(img) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/download_file/complex.py b/qt__pyqt__pyside__pyqode/download_file/complex.py index 474760e48..fb1e0f5b8 100644 --- a/qt__pyqt__pyside__pyqode/download_file/complex.py +++ b/qt__pyqt__pyside__pyqode/download_file/complex.py @@ -1,14 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path import traceback from urllib.request import urlretrieve -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLineEdit, QFormLayout, QTextEdit +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QLineEdit, + QFormLayout, + QTextEdit, +) from PyQt5.QtCore import QThread, pyqtSignal @@ -17,14 +24,14 @@ class ThreadDownload(QThread): about_file_name = pyqtSignal(str) about_error = pyqtSignal(str) - def __init__(self, url: str = None, file_name: str = None): + def __init__(self, url: str = None, file_name: str = None) -> None: super().__init__() self.url = url self.file_name = file_name # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/4692cbb36b5addb0f2bd5e93e69eb8c7c257bdf8/download_file/with_progress.py#L12 - def reporthook(self, blocknum, blocksize, totalsize): + def reporthook(self, blocknum, blocksize, totalsize) -> None: readsofar = blocknum * blocksize if totalsize > 0: percent = readsofar * 100.0 / totalsize @@ -32,16 +39,23 @@ def reporthook(self, blocknum, blocksize, totalsize): percent = 100 readsofar = totalsize - s = "%5.1f%% %*d / %d" % (percent, len(str(totalsize)), readsofar, totalsize) + s = "%5.1f%% %*d / %d" % ( + percent, + len(str(totalsize)), + readsofar, + totalsize, + ) self.about_progress.emit(s) # Total size is unknown else: self.about_progress.emit(f"read {readsofar}") - def run(self): + def run(self) -> None: try: - file_name, _ = urlretrieve(self.url, self.file_name, reporthook=self.reporthook) + file_name, _ = urlretrieve( + self.url, self.file_name, reporthook=self.reporthook + ) file_name = os.path.abspath(file_name) self.about_file_name.emit(file_name) @@ -50,20 +64,22 @@ def run(self): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.line_edit_url = QLineEdit('https://codeload.github.com/gil9red/SimplePyScripts/zip/master') - self.line_edit_file_name = QLineEdit('SimplePyScripts.zip') + self.line_edit_url = QLineEdit( + "https://codeload.github.com/gil9red/SimplePyScripts/zip/master" + ) + self.line_edit_file_name = QLineEdit("SimplePyScripts.zip") - self.button_download = QPushButton('Download') + self.button_download = QPushButton("Download") self.button_download.clicked.connect(self.download) self.text_edit_log = QTextEdit() layout = QFormLayout() - layout.addRow('URL:', self.line_edit_url) - layout.addRow('File Name:', self.line_edit_file_name) + layout.addRow("URL:", self.line_edit_url) + layout.addRow("File Name:", self.line_edit_file_name) layout.addWidget(self.button_download) layout.addWidget(self.text_edit_log) @@ -76,22 +92,24 @@ def __init__(self): self.setLayout(layout) - def download(self): + def download(self) -> None: self.thread.url = self.line_edit_url.text() self.thread.file_name = self.line_edit_file_name.text() self.thread.start() - def _handle_about_progress(self, text: str): + def _handle_about_progress(self, text: str) -> None: self.setWindowTitle(text) - def _handle_about_file_name(self, text: str): - self.text_edit_log.append(f"""

    File name: {text}

    """) + def _handle_about_file_name(self, text: str) -> None: + self.text_edit_log.append( + f"""

    File name: {text}

    """ + ) - def _handle_about_error(self, text: str): + def _handle_about_error(self, text: str) -> None: self.text_edit_log.append(f"""
    {text}
    """) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/download_file/simple.py b/qt__pyqt__pyside__pyqode/download_file/simple.py index baddf6f1e..9f62bc7b6 100644 --- a/qt__pyqt__pyside__pyqode/download_file/simple.py +++ b/qt__pyqt__pyside__pyqode/download_file/simple.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path -from threading import Thread import traceback + +from threading import Thread from urllib.request import urlretrieve from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLineEdit, QFormLayout -def download_file(url: str, file_name: str): +def download_file(url: str, file_name: str) -> None: try: local_file_name, _ = urlretrieve(url, file_name) print(os.path.abspath(local_file_name)) @@ -21,23 +22,25 @@ def download_file(url: str, file_name: str): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.line_edit_url = QLineEdit('https://codeload.github.com/gil9red/SimplePyScripts/zip/master') - self.line_edit_file_name = QLineEdit('SimplePyScripts.zip') + self.line_edit_url = QLineEdit( + "https://codeload.github.com/gil9red/SimplePyScripts/zip/master" + ) + self.line_edit_file_name = QLineEdit("SimplePyScripts.zip") - self.button_download = QPushButton('Download') + self.button_download = QPushButton("Download") self.button_download.clicked.connect(self.download) layout = QFormLayout() - layout.addRow('URL:', self.line_edit_url) - layout.addRow('File Name:', self.line_edit_file_name) + layout.addRow("URL:", self.line_edit_url) + layout.addRow("File Name:", self.line_edit_file_name) layout.addWidget(self.button_download) self.setLayout(layout) - def download(self): + def download(self) -> None: url = self.line_edit_url.text() file_name = self.line_edit_file_name.text() @@ -45,7 +48,7 @@ def download(self): thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py index eaa0d385b..e565d4012 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class CellWidget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.ball_size = 100 - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -30,10 +30,10 @@ def paintEvent(self, event: Qt.QPaintEvent): class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board') + self.setWindowTitle("draw__ellipse_board") self.cell_widget = CellWidget() @@ -41,7 +41,9 @@ def __init__(self): self.pb_ball_size.setRange(5, 1000) self.sl_ball_size = Qt.QSlider(Qt.Qt.Horizontal) - self.sl_ball_size.setRange(self.pb_ball_size.minimum(), self.pb_ball_size.maximum()) + self.sl_ball_size.setRange( + self.pb_ball_size.minimum(), self.pb_ball_size.maximum() + ) self.sl_ball_size.valueChanged.connect(self._set_ball_size) self.pb_ball_size.valueChanged.connect(self._set_ball_size) @@ -58,7 +60,7 @@ def __init__(self): self.setLayout(layout) - def _set_ball_size(self, value): + def _set_ball_size(self, value) -> None: self.sl_ball_size.setValue(value) self.pb_ball_size.setValue(value) @@ -66,7 +68,7 @@ def _set_ball_size(self, value): self.cell_widget.update() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py index 419e4c956..15fae80ce 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board__chess_order__decrease') + self.setWindowTitle("draw__ellipse_board__chess_order__decrease") - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -44,7 +44,7 @@ def paintEvent(self, event: Qt.QPaintEvent): y += h_indent -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py index e0b3e9383..cf6110eb3 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board__decrease') + self.setWindowTitle("draw__ellipse_board__decrease") - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -41,7 +41,7 @@ def paintEvent(self, event: Qt.QPaintEvent): y += h_indent -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#007396.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#007396.png index c003f6d03..45bab42b1 100644 Binary files a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#007396.png and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#007396.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#333.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#333.png new file mode 100644 index 000000000..f8efa947b Binary files /dev/null and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#333.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#a000ff00.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#a000ff00.png index 5a698d244..be3653d8c 100644 Binary files a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#a000ff00.png and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#a000ff00.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#ff8c69.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#ff8c69.png index b3cb3f859..65556b638 100644 Binary files a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#ff8c69.png and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/#ff8c69.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/green.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/green.png index 61c5b19d0..22163f812 100644 Binary files a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/green.png and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/green.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/no_rounded_#007396.png b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/no_rounded_#007396.png index 63904939a..44610812f 100644 Binary files a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/no_rounded_#007396.png and b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/images/no_rounded_#007396.png differ diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py index 03e7fbc7b..f077dd9d8 100644 --- a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py +++ b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py @@ -1,24 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Union import sys - -sys.path.append('..') - -from Good_text_foreground_color_for_a_given_background_color import get_good_text_foreground_color +from pathlib import Path from PyQt5.QtGui import QGuiApplication, QPainter, QImage, QColor, QFont, QFontMetrics from PyQt5.QtCore import Qt, QByteArray, QBuffer, QIODevice +sys.path.append("..") +from Good_text_foreground_color_for_a_given_background_color import ( + get_good_text_foreground_color, +) + APP = QGuiApplication([]) SIZE = 300 RADIUS = 20 -FAMILY_FONT = 'Courier New' # Need monospaced +FAMILY_FONT = "Courier New" # Need monospaced def get_optimal_font(family_font: str, w, h, text: str) -> QFont: @@ -27,40 +28,42 @@ def get_optimal_font(family_font: str, w, h, text: str) -> QFont: metrics = QFontMetrics(font) # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/add91e36e1ee59b3956b9fafdcffc9f4ff10ed3d/qt__pyqt__pyside__pyqode/pyqt__QPainter__draw_table.py#L98 - factor = w / metrics.boundingRect(0, 0, w, h, Qt.AlignCenter, text).width() + factor = ( + w / metrics.boundingRect(0, 0, int(w), int(h), Qt.AlignCenter, text).width() + ) if factor < 1 or factor > 1.25: font.setPointSizeF(font.pointSizeF() * factor) return font -def draw_hex(painter: QPainter, size: int, color: QColor): +def draw_hex(painter: QPainter, size: int, color: QColor) -> None: r, g, b, _ = color.getRgb() - value = ''.join(f'{x:02X}' for x in [r, g, b]) + value = "".join(f"{x:02X}" for x in [r, g, b]) text_color = get_good_text_foreground_color(color) x, y, w_hex, h_hex = size * 0.05, size * 0.1, size * 0.9, size * 0.25 - font = get_optimal_font(FAMILY_FONT, w_hex, h_hex, text='FFFFFF') + font = get_optimal_font(FAMILY_FONT, w_hex, h_hex, text="FFFFFF") painter.save() painter.setPen(text_color) painter.setFont(font) - painter.drawText(x, y, w_hex, h_hex, Qt.AlignCenter, value) + painter.drawText(int(x), int(y), int(w_hex), int(h_hex), Qt.AlignCenter, value) painter.restore() -def draw_rgb(painter: QPainter, size: int, color: QColor): +def draw_rgb(painter: QPainter, size: int, color: QColor) -> None: r, g, b, _ = color.getRgb() rgb = list(map(str, [r, g, b])) text_color, back_color = Qt.black, Qt.white w_rgb, h_rgb = size // 4, size // 6 - font = get_optimal_font(FAMILY_FONT, w_rgb, h_rgb, text='255') + font = get_optimal_font(FAMILY_FONT, w_rgb, h_rgb, text="255") font.setWeight(QFont.Bold) y = size - size // 4 @@ -69,6 +72,7 @@ def draw_rgb(painter: QPainter, size: int, color: QColor): for i, value in enumerate(rgb): x = indent + indent * i + w_rgb * i + x, y, w_rgb, h_rgb = map(int, (x, y, w_rgb, h_rgb)) painter.save() painter.setPen(Qt.NoPen) @@ -83,7 +87,9 @@ def draw_rgb(painter: QPainter, size: int, color: QColor): painter.restore() -def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=False) -> Union[QImage, bytes]: +def get_frame_with_color_info( + color: QColor, size=SIZE, rounded=True, as_bytes=False +) -> QImage | bytes: image = QImage(size, size, QImage.Format_ARGB32) image.fill(Qt.transparent) @@ -93,7 +99,9 @@ def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=F painter.setBrush(color) if rounded: - painter.drawRoundedRect(0, 0, image.width(), image.height(), RADIUS, RADIUS, Qt.RelativeSize) + painter.drawRoundedRect( + 0, 0, image.width(), image.height(), RADIUS, RADIUS, Qt.RelativeSize + ) else: painter.drawRect(0, 0, image.width(), image.height()) @@ -112,12 +120,15 @@ def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=F return image -if __name__ == '__main__': - for name in ['#007396', '#ff8c69', 'green', '#a000ff00']: +if __name__ == "__main__": + path = Path(".") / "images" + path.mkdir(parents=True, exist_ok=True) + + for name in ["#007396", "#ff8c69", "green", "#a000ff00", "#333"]: color = QColor(name) image = get_frame_with_color_info(color) - image.save(f'images/{name}.png') + image.save(f"{path}/{name}.png") - name = '#007396' + name = "#007396" image = get_frame_with_color_info(QColor(name), rounded=False) - image.save(f'images/no_rounded_{name}.png') + image.save(f"images/no_rounded_{name}.png") diff --git a/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py b/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py index d74febe31..7d29b5702 100644 --- a/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py +++ b/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py @@ -1,35 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QHBoxLayout, QScrollArea, QMessageBox +import sys +import traceback + +from PyQt5.QtWidgets import ( + QWidget, + QApplication, + QLabel, + QHBoxLayout, + QScrollArea, + QMessageBox, +) from PyQt5.QtGui import QPainter, QColor, QPixmap, QPalette from PyQt5.QtCore import Qt, QEvent -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.flag = False - self.image = QPixmap('image.jpg') + self.image = QPixmap("image.jpg") self.label = QLabel() self.label.setAlignment(Qt.AlignLeft) @@ -48,7 +56,7 @@ def __init__(self): # Рисовать будем на картинке self.painter = QPainter(self.image) - self.painter.setBrush(QColor('yellow')) + self.painter.setBrush(QColor("yellow")) def eventFilter(self, obj, e): # Если событие пришло от self.label @@ -69,12 +77,12 @@ def eventFilter(self, obj, e): # Стандартная обработка событий return super().eventFilter(obj, e) - def draw_ellipse(self, e): + def draw_ellipse(self, e) -> None: self.painter.drawEllipse(e.pos(), 20, 20) self.label.setPixmap(self.image) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Example() diff --git a/qt__pyqt__pyside__pyqode/draw_spiral.py b/qt__pyqt__pyside__pyqode/draw_spiral.py new file mode 100644 index 000000000..38ab2b73c --- /dev/null +++ b/qt__pyqt__pyside__pyqode/draw_spiral.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://doc.qt.io/qt-5/qtopengl-2dpainting-example.html + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import ( + QBrush, + QFont, + QPen, + QPainter, + QPaintEvent, + QLinearGradient, + QColor, +) +from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer + + +class Helper: + def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False) -> None: + super().__init__() + + self.is_reverse_rotation = is_reverse_rotation + self.is_draw_all_spiral = is_draw_all_spiral + + gradient = QLinearGradient(QPointF(50, -20), QPointF(80, 20)) + gradient.setColorAt(0.0, Qt.white) + gradient.setColorAt(1.0, QColor(0xA6, 0xCE, 0x39)) + + self.background: QBrush = QBrush(QColor(64, 32, 64)) + + self.circleBrush: QBrush = QBrush(gradient) + + self.circlePen: QPen = QPen(Qt.black) + self.circlePen.setWidth(1) + + self.textFont: QFont = QFont() + self.textFont.setPixelSize(50) + + self.textPen: QPen = QPen(Qt.white) + + def _draw_spiral(self, painter: QPainter, elapsed: int, is_reverse_rotation=False) -> None: + r = elapsed / 1000.0 + n = 30 + + painter.save() + painter.setBrush(self.circleBrush) + painter.setPen(self.circlePen) + + painter.rotate(elapsed * 0.030 * (-1 if is_reverse_rotation else 1)) + for i in range(n): + painter.rotate(30 * (-1 if is_reverse_rotation else 1)) + factor = (i + r) / n + radius = 0 + 120.0 * factor + circleRadius = 1 + factor * 20 + painter.drawEllipse( + QRectF(radius, -circleRadius, circleRadius * 2, circleRadius * 2) + ) + + painter.restore() + + def paint(self, painter: QPainter, event: QPaintEvent, elapsed: int) -> None: + painter.fillRect(event.rect(), self.background) + painter.translate(100, 100) + + self._draw_spiral(painter, elapsed, self.is_reverse_rotation) + if self.is_draw_all_spiral: + self._draw_spiral(painter, elapsed, not self.is_reverse_rotation) + + +class Widget(QWidget): + def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False) -> None: + super().__init__() + + self.setFixedSize(200, 200) + + self.elapsed: int = 0 + self.helper = Helper(is_reverse_rotation, is_draw_all_spiral) + + self.timer = QTimer() + self.timer.timeout.connect(self.animate) + self.timer.start(50) + + def animate(self) -> None: + self.elapsed = (self.elapsed + self.timer.interval()) % 1000 + self.update() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + self.helper.paint(painter, event, self.elapsed) + painter.end() + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QLabel, QHBoxLayout + + app = QApplication([]) + + mw = QWidget() + + w1 = Widget(is_reverse_rotation=False) + w2 = Widget(is_reverse_rotation=True) + w3 = Widget(is_reverse_rotation=False, is_draw_all_spiral=True) + + layout = QHBoxLayout(mw) + layout.addWidget(w1) + layout.addWidget(QLabel("+")) + layout.addWidget(w2) + layout.addWidget(QLabel("=")) + layout.addWidget(w3) + + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py b/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py new file mode 100644 index 000000000..fab8cc9d3 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import math + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QPen +from PyQt5.QtCore import Qt + + +class MainWindow(QWidget): + def paintEvent(self, event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + painter.setBrush(Qt.black) + painter.drawRect(self.rect()) + + y0 = self.height() / 4 + amplitude = self.height() / 2 + frequency = 0.02 + + painter.setPen(QPen(Qt.white, 2)) + for x in range(self.width()): + y = y0 + amplitude / 2 + painter.drawPoint(x, y) + + painter.setPen(QPen(Qt.red, 2)) + for x in range(self.width()): + y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 + painter.drawPoint(x, y) + + painter.setPen(QPen(Qt.green, 2)) + for x in range(self.width()): + y = y0 + math.cos(x * frequency) * amplitude / 2 + amplitude / 2 + painter.drawPoint(x, y) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py new file mode 100644 index 000000000..dae8ceada --- /dev/null +++ b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import ( + QApplication, + QVBoxLayout, + QHBoxLayout, + QWidget, + QPushButton, + QLabel, + QStackedWidget, +) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.stacked_widget = QStackedWidget() + self.stacked_widget.addWidget(QLabel("1234")) + self.stacked_widget.addWidget(QLabel("ABCD")) + self.stacked_widget.addWidget(QLabel("FOO_BAR")) + + self.button_123 = QPushButton("1234") + self.button_123.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0)) + + self.button_ABCD = QPushButton("ABCD") + self.button_ABCD.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(1)) + + self.button_FOO_BAR = QPushButton("FOO_BAR") + self.button_FOO_BAR.clicked.connect( + lambda: self.stacked_widget.setCurrentIndex(2) + ) + + layout_buttons = QVBoxLayout() + layout_buttons.addWidget(self.button_123) + layout_buttons.addWidget(self.button_ABCD) + layout_buttons.addWidget(self.button_FOO_BAR) + + main_layout = QHBoxLayout(self) + main_layout.addLayout(layout_buttons) + main_layout.addWidget(self.stacked_widget) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py new file mode 100644 index 000000000..0fcdaee31 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import ( + QApplication, + QHBoxLayout, + QWidget, + QLabel, + QStackedWidget, + QListWidget, +) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.stacked_widget = QStackedWidget() + self.stacked_widget.addWidget(QLabel("1234")) + self.stacked_widget.addWidget(QLabel("ABCD")) + self.stacked_widget.addWidget(QLabel("FOO_BAR")) + + self.control_list = QListWidget() + self.control_list.addItems(["1234", "ABCD", "FOO_BAR"]) + self.control_list.setFixedWidth(80) + self.control_list.clicked.connect( + lambda index: self.stacked_widget.setCurrentIndex(index.row()) + ) + + main_layout = QHBoxLayout(self) + main_layout.addWidget(self.control_list) + main_layout.addWidget(self.stacked_widget) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py b/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py deleted file mode 100644 index 1affddc74..000000000 --- a/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Основа взята из http://stackoverflow.com/a/37755811/5909792 -def get_html(url, check_content_func=None): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - - class ExtractorHtml: - def __init__(self, url): - self.html = None - - _app = QApplication([]) - self._page = QWebEnginePage() - self._page.load(QUrl(url)) - self._page.loadFinished.connect(self._load_finished_handler) - - # Ожидание загрузки страницы и получения его содержимого - # Этот цикл асинхронный код делает синхронным - while self.html is None: - _app.processEvents() - - _app.quit() - - self._page = None - - def _callable(self, data): - if check_content_func: - if check_content_func(data): - self.html = data - - else: - self.html = data - - def _load_finished_handler(self): - self._page.toHtml(self._callable) - - return ExtractorHtml(url).html - - -url = 'http://www.dns-shop.ru/' -html = get_html(url, lambda html: 'price-list-downloader' in html) -print(html) diff --git a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py index 86b35a181..d51d5a8ff 100644 --- a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py +++ b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,32 +10,34 @@ """ +import sys +import time +import traceback + from PyQt5.QtWidgets import QWidget, QListWidget, QApplication, QVBoxLayout from PyQt5.QtCore import QThread, pyqtSignal -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) + print("Error: ", text) - print('Error: ', text) -import sys sys.excepthook = log_uncaught_exceptions class MyThread(QThread): about_new_value = pyqtSignal(int) - def __init__(self): + def __init__(self) -> None: super().__init__() self.executed = False - def run(self): - print('start thread') + def run(self) -> None: + print("start thread") try: for i in range(1000000): @@ -46,11 +48,10 @@ def run(self): print(i) # Задержка в 5 миллисекунд - import time time.sleep(0.005) finally: - print('finish thread') + print("finish thread") def start(self, priority=QThread.InheritPriority): self.executed = True @@ -64,7 +65,7 @@ def exit(self, returnCode=0): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw = QListWidget() @@ -78,19 +79,19 @@ def __init__(self): self.thread = MyThread() self.thread.about_new_value.connect(lambda x: self.append_item(str(x))) - def append_item(self, item): + def append_item(self, item) -> None: self.lw.addItem(item) self.lw.scrollToBottom() - def fill(self): + def fill(self) -> None: self.thread.start() - def closeEvent(self, event): + def closeEvent(self, event) -> None: # После закрытия окна приложение не завершится пока список работает - quit() + sys.exit() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() @@ -98,4 +99,3 @@ def closeEvent(self, event): w.fill() app.exec_() - diff --git a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py index 90b83bf16..ef922ccf3 100644 --- a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py +++ b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,30 +10,30 @@ """ +import traceback +import sys + from PyQt5.QtWidgets import QWidget, QListWidget, QApplication, QVBoxLayout -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) + print("Error: ", text) - print('Error: ', text) -import sys sys.excepthook = log_uncaught_exceptions def generator_large_list(): for i in range(1000000): QApplication.processEvents() - yield i class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw = QListWidget() @@ -44,17 +44,17 @@ def __init__(self): self.setLayout(layout) - def fill(self): + def fill(self) -> None: for i in generator_large_list(): self.lw.addItem(str(i)) self.lw.scrollToBottom() - def closeEvent(self, event): + def closeEvent(self, event) -> None: # После закрытия окна приложение не завершится пока список работает - quit() + sys.exit() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen.py b/qt__pyqt__pyside__pyqode/full_black_screen.py index fd6715d2d..ac42ed33a 100644 --- a/qt__pyqt__pyside__pyqode/full_black_screen.py +++ b/qt__pyqt__pyside__pyqode/full_black_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget @@ -10,7 +10,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setMouseTracking(True) @@ -18,22 +18,22 @@ def __init__(self): self._enabledClose = False QTimer.singleShot(5000, self._setEnabledClose) - def _setEnabledClose(self): + def _setEnabledClose(self) -> None: self._enabledClose = True - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: if self._enabledClose: self.close() - def mousePressEvent(self, event): + def mousePressEvent(self, event) -> None: if self._enabledClose: self.close() - def keyPressEvent(self, event): + def keyPressEvent(self, event) -> None: if self._enabledClose: self.close() - def paintEvent(self, event): + def paintEvent(self, event) -> None: color = Qt.black painter = QPainter(self) @@ -43,7 +43,7 @@ def paintEvent(self, event): painter.drawRect(self.rect()) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py new file mode 100644 index 000000000..251972571 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QToolButton, + QSizePolicy, + QGraphicsOpacityEffect, +) +from PyQt5.QtGui import QKeyEvent +from PyQt5.QtCore import QPropertyAnimation, QSequentialAnimationGroup, Qt + + +class MainWindow(QWidget): + def __init__(self, background_color: str = "black", text_color: str = "white") -> None: + super().__init__() + + self.setStyleSheet( + f""" + MainWindow {{ + background-color: {background_color}; + }} + + QToolButton {{ + color: {text_color}; + background-color: {background_color}; + border: 1px solid darkgray; + }} + """ + ) + + self.button_close = QToolButton() + self.button_close.setText("ЗАКРЫТЬ") + self.button_close.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + self.button_close.setFixedHeight(60) + self.button_close.clicked.connect(self.close) + + self._add_animations() + + main_layout = QVBoxLayout(self) + main_layout.addStretch() + main_layout.addWidget(self.button_close) + + def _add_animations(self) -> None: + self.button_close.setGraphicsEffect(QGraphicsOpacityEffect()) + + animation_object = self.button_close.graphicsEffect() + animation_property = b"opacity" + duration = 5000 + start_value = 1.0 + end_value = 0.0 + + animation1 = QPropertyAnimation(animation_object, animation_property) + animation1.setDuration(duration) + animation1.setStartValue(start_value) + animation1.setEndValue(end_value) + + animation2 = QPropertyAnimation(animation_object, animation_property) + animation2.setDuration(duration) + animation2.setStartValue(end_value) + animation2.setEndValue(start_value) + + self.animation_group = QSequentialAnimationGroup() + self.animation_group.setLoopCount(-1) + self.animation_group.addAnimation(animation1) + self.animation_group.addAnimation(animation2) + self.animation_group.start() + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() in [Qt.Key_Escape, Qt.Key_Return, Qt.Key_Enter, Qt.Key_Space]: + self.close() + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.resize(600, 600) + # mw.show() + mw.showFullScreen() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py new file mode 100644 index 000000000..dfc1dd7cf --- /dev/null +++ b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +from random import randint, choice + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QPaintEvent +from PyQt5.QtCore import QTimer + +from full_black_screen_close_manual import MainWindow as BaseMainWindow +from pyq5__simple_balls__with_part_transparent_body import ( + Ball as BaseBall, + get_random_vector, + get_random_color, +) + + +class Animation: + def __init__(self, owner: QWidget = None) -> None: + self.owner = owner + + def set_owner(self, owner: QWidget) -> None: + self.owner = owner + + def prepare(self) -> None: + pass + + def tick(self) -> None: + pass + + def draw(self, painter: QPainter) -> None: + pass + + +class DirectionEnum(enum.IntEnum): + UP = 1 + DOWN = -1 + + +class Ball(BaseBall): + def __init__(self, x, y, r, v_x, v_y, color) -> None: + super().__init__(x, y, r, v_x, v_y, color) + + self.animation_alpha_direction = choice(list(DirectionEnum)) + + +class AnimationBalls(Animation): + def __init__( + self, + owner: QWidget = None, + number_balls: int = 50, + min_ball_alpha_color: int = 35, # Минимальное значение может быть 0 + max_ball_alpha_color: int = 160, # Максимальное значение может быть 255 + animation_ball_alpha_color: bool = True, + ) -> None: + super().__init__(owner) + + self.balls: list[Ball] = [] + self.number_balls = number_balls + self.min_ball_alpha_color = min_ball_alpha_color + self.max_ball_alpha_color = max_ball_alpha_color + self.animation_ball_alpha_color = animation_ball_alpha_color + + def prepare(self) -> None: + for _ in range(self.number_balls): + self.append_random_ball() + + def append_random_ball(self) -> None: + x = self.owner.width() // 2 + randint( + -self.owner.width() // 3, self.owner.width() // 3 + ) + y = self.owner.height() // 2 + randint( + -self.owner.height() // 3, self.owner.height() // 3 + ) + v_x, v_y = get_random_vector() + r, g, b = get_random_color() + a = randint(self.min_ball_alpha_color, self.max_ball_alpha_color) + + ball = Ball( + x, + y, + r=randint(50, 70), + v_x=v_x, + v_y=v_y, + color=(r, g, b, a), + ) + self.balls.append(ball) + + def tick(self) -> None: + for ball in self.balls: + ball.update() + + # Условия отскакивания шарика от левого и правого края + if ball.left <= 0 or ball.right >= self.owner.width(): + ball.v_x = -ball.v_x + + # Условия отскакивания шарика верхнего и нижнего края + if ball.top <= 0 or ball.bottom >= self.owner.height(): + ball.v_y = -ball.v_y + + if self.animation_ball_alpha_color: + alpha = ball.color[3] + ball.animation_alpha_direction + + if alpha >= self.max_ball_alpha_color: + ball.animation_alpha_direction = DirectionEnum.DOWN + + if alpha <= self.min_ball_alpha_color: + ball.animation_alpha_direction = DirectionEnum.UP + + ball.color = ball.color[:3] + (alpha,) + + def draw(self, painter: QPainter) -> None: + for ball in self.balls: + ball.draw(painter) + + +class MainWindow(BaseMainWindow): + def __init__(self, animations: list[Animation] = None) -> None: + super().__init__() + + self.animations: list[Animation] = animations + + # Таймер для обновления анимаций + self.timer = QTimer() + self.timer.setInterval(1000 // 20) + self.timer.timeout.connect(self.tick) + + def tick(self) -> None: + if not self.animations: + return + + for animation in self.animations: + animation.tick() + + self.update() + + def start_animations(self) -> None: + if not self.animations: + return + + for animation in self.animations: + animation.set_owner(self) + animation.prepare() + + self.timer.start() + + def paintEvent(self, event: QPaintEvent) -> None: + super().paintEvent(event) + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + for animation in self.animations: + animation.draw(painter) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow( + animations=[ + AnimationBalls(), + ] + ) + mw.resize(600, 600) + mw.showFullScreen() + mw.start_animations() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py b/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py new file mode 100644 index 000000000..b03d41b33 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication + +from full_black_screen_close_manual import MainWindow + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow(background_color="white", text_color="black") + mw.resize(600, 600) + mw.showFullScreen() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py b/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py index 4d234ff76..b88e0790b 100644 --- a/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py +++ b/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() self._page.loadFinished.connect(self._load_finished_handler) @@ -35,10 +36,10 @@ def __init__(self, url): # Чтобы избежать падений скрипта self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self, _): + def _load_finished_handler(self, _) -> None: self._counter_finished += 1 if self._counter_finished == 2: @@ -47,6 +48,6 @@ def _load_finished_handler(self, _): return ExtractorHtml(url).html -if __name__ == '__main__': - url = 'http://gama-gama.ru/search/?searchField=titan' +if __name__ == "__main__": + url = "http://gama-gama.ru/search/?searchField=titan" print(get_html(url)) diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/default_mouse.png b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/default_mouse.png new file mode 100644 index 000000000..56c681536 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/default_mouse.png differ diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py new file mode 100644 index 000000000..46db7da3a --- /dev/null +++ b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import win32gui + +from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import QTimer +from PyQt5.QtWinExtras import QtWin + + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.DEFAULT_MOUSE_PIXMAP = QPixmap("default_mouse.png").scaledToWidth(16) + + self.label = QLabel() + + layout = QVBoxLayout(self) + layout.addWidget(self.label) + + self.timer = QTimer() + self.timer.timeout.connect(self._on_tick) + self.timer.setInterval(100) + self.timer.start() + + def _on_tick(self) -> None: + # flags, hcursor, (x, y) = + _, hcursor, _ = win32gui.GetCursorInfo() + + # fIcon, xHotspot, yHotspot, hbmMask, hbmColor = + _, _, _, _, hbmColor = win32gui.GetIconInfo(hcursor) + + pixmap = QtWin.fromHBITMAP(hbmColor.handle, QtWin.HBitmapPremultipliedAlpha) + if pixmap.isNull(): + pixmap = self.DEFAULT_MOUSE_PIXMAP + + self.label.setPixmap(pixmap) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/img.png b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/img.png new file mode 100644 index 000000000..02a5c90c2 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/img.png differ diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py new file mode 100644 index 000000000..cdd1fb928 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import win32gui + +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWinExtras import QtWin + + +app = QApplication([]) + +# flags, hcursor, (x, y) = +_, hcursor, _ = win32gui.GetCursorInfo() + +# fIcon, xHotspot, yHotspot, hbmMask, hbmColor = +_, _, _, _, hbmColor = win32gui.GetIconInfo(hcursor) + +pixmap = QtWin.fromHBITMAP(hbmColor.handle, QtWin.HBitmapPremultipliedAlpha) +print(pixmap.size(), pixmap) + +pixmap.save("img.png") diff --git a/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py b/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py index f7a3f7096..f4628243c 100644 --- a/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py +++ b/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -14,7 +14,7 @@ view = QWebEngineView() view.show() -url = 'https://www.google.com/search?q=hello world' +url = "https://www.google.com/search?q=hello world" view.load(QUrl(url)) app.exec() diff --git a/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py b/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py index 5b7f50463..09e1b1718 100644 --- a/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py +++ b/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,25 +14,25 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.prev_tab_index = -1 self.curr_tab_index = 0 self.tab_widget = QTabWidget() - self.tab_widget.addTab(QLabel('1'), '1') - self.tab_widget.addTab(QLabel('2'), '2') - self.tab_widget.addTab(QLabel('3'), '3') - self.tab_widget.addTab(QLabel('4'), '4') - self.tab_widget.addTab(QLabel('5'), '5') + self.tab_widget.addTab(QLabel("1"), "1") + self.tab_widget.addTab(QLabel("2"), "2") + self.tab_widget.addTab(QLabel("3"), "3") + self.tab_widget.addTab(QLabel("4"), "4") + self.tab_widget.addTab(QLabel("5"), "5") self.tab_widget.setCurrentIndex(self.curr_tab_index) self.tab_widget.currentChanged.connect(self.current_tab_changed) self.setCentralWidget(self.tab_widget) - def current_tab_changed(self, i): + def current_tab_changed(self, i) -> None: # Запоминание индексов предыдущей и текущей вкладки. # При переключении вкладок: # У предыдущей убирается выделение @@ -45,12 +45,12 @@ def current_tab_changed(self, i): self.prev_tab_index = self.curr_tab_index prev_text = self.tab_widget.tabText(self.prev_tab_index) - self.tab_widget.setTabText(self.prev_tab_index, '_' + prev_text + '_') + self.tab_widget.setTabText(self.prev_tab_index, "_" + prev_text + "_") self.curr_tab_index = i -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py b/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py index 26f99c2a6..17305c9de 100644 --- a/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py +++ b/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py @@ -1,32 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + from PyQt5.QtWidgets import * # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions app = QApplication([]) w = QLineEdit() -w.textChanged.conneect(lambda x: 1 + '1') +w.textChanged.conneect(lambda x: 1 + "1") # Emit textChanged signal -w.setText('!!!') +w.setText("!!!") diff --git a/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py b/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py index f8c5cf186..5a8ee92f8 100644 --- a/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py +++ b/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + try: from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -14,17 +17,15 @@ # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions @@ -36,7 +37,7 @@ def create_item(img): class MyDelegate_1(QStyledItemDelegate): - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: img = index.model().data(index, Qt.DecorationRole) if img is None: super().paint(painter, option, index) @@ -68,7 +69,7 @@ def paint(self, painter, option, index): class MyDelegate_2(QStyledItemDelegate): - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: img = index.model().data(index, Qt.DecorationRole) if img is None: super().paint(painter, option, index) @@ -106,7 +107,7 @@ def paint(self, painter, option, index): # super().paint(painter, option, index) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) table = QTableWidget() @@ -114,15 +115,15 @@ def paint(self, painter, option, index): table.show() table.resize(400, 200) - headers = ['Normal', 'Delegate v1', 'Delegate v1 (without img)', 'Delegate v2'] + headers = ["Normal", "Delegate v1", "Delegate v1 (without img)", "Delegate v2"] table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(3) table.verticalHeader().hide() - pix_1 = QPixmap('favicon_google.png') - pix_2 = QPixmap('favicon_prog_org.png') - pix_3 = QPixmap('favicon_google_tr.png') + pix_1 = QPixmap("favicon_google.png") + pix_2 = QPixmap("favicon_prog_org.png") + pix_3 = QPixmap("favicon_google_tr.png") for col in range(table.columnCount()): if col == 2: diff --git a/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py b/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py index 88281062c..27ba3cf83 100644 --- a/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py +++ b/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py @@ -1,25 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt from glob import glob from pathlib import Path from PyQt5.QtWidgets import ( - QApplication, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QStyledItemDelegate, QStyle + QApplication, + QListWidget, + QListWidgetItem, + QTableWidget, + QTableWidgetItem, + QStyledItemDelegate, + QStyle, ) from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtCore import QSize, Qt class ImageDelegate(QStyledItemDelegate): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: pixmap = index.data(Qt.DecorationRole) if pixmap: # pixmap = pixmap.scaled( @@ -40,14 +46,15 @@ def paint(self, painter, option, index): ICON_HEIGHT = 128 -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - file_names = glob(r'D:\все фотки\**\*.jpg', recursive=True) - print(f'Files: {len(file_names)}') + file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) + print(f"Files: {len(file_names)}") list_widget = QListWidget() - list_widget.setStyleSheet(""" + list_widget.setStyleSheet( + """ QListWidget::item { border: 1px solid gray; } @@ -55,7 +62,8 @@ def paint(self, painter, option, index): color: black; background-color: #0087BD; } - """) + """ + ) list_widget.setMovement(QListWidget.Static) list_widget.setDragEnabled(False) list_widget.setDragDropMode(QListWidget.NoDragDrop) @@ -70,7 +78,7 @@ def paint(self, painter, option, index): list_widget.resize(800, 600) list_widget.show() - headers = ['IMAGE', 'FILE NAME', 'DIRECTORY'] + headers = ["IMAGE", "FILE NAME", "DIRECTORY"] table_widget = QTableWidget() table_widget.setColumnCount(len(headers)) table_widget.setHorizontalHeaderLabels(headers) @@ -82,12 +90,16 @@ def paint(self, painter, option, index): table_widget.resize(800, 600) table_widget.show() - start_time = DT.datetime.now() + start_time = dt.datetime.now() row = 0 for i, file_name in enumerate(file_names, 1): - list_widget.setWindowTitle(f'{i} / {len(file_names)} ({i / len(file_names):.0%})') - table_widget.setWindowTitle(f'{i} / {len(file_names)} ({i / len(file_names):.0%})') + list_widget.setWindowTitle( + f"{i} / {len(file_names)} ({i / len(file_names):.0%})" + ) + table_widget.setWindowTitle( + f"{i} / {len(file_names)} ({i / len(file_names):.0%})" + ) pixmap = QPixmap(file_name) if pixmap.isNull(): @@ -98,10 +110,7 @@ def paint(self, painter, option, index): ) base_file_name = Path(file_name).name - item = QListWidgetItem( - QIcon(pixmap), - base_file_name - ) + item = QListWidgetItem(QIcon(pixmap), base_file_name) item.setTextAlignment(Qt.AlignHCenter | Qt.AlignBottom) item.setSizeHint(QSize(ICON_WIDTH, ICON_HEIGHT + 20)) list_widget.addItem(item) @@ -119,10 +128,14 @@ def paint(self, painter, option, index): QApplication.processEvents() list_widget.setWindowTitle( - list_widget.windowTitle() + '. Elapsed: ' + str(DT.datetime.now() - start_time).split('.')[0] + list_widget.windowTitle() + + ". Elapsed: " + + str(dt.datetime.now() - start_time).split(".")[0] ) table_widget.setWindowTitle( - table_widget.windowTitle() + '. Elapsed: ' + str(DT.datetime.now() - start_time).split('.')[0] + table_widget.windowTitle() + + ". Elapsed: " + + str(dt.datetime.now() - start_time).split(".")[0] ) app.exec() diff --git a/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py b/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py index 29ee8612e..68c79ca65 100644 --- a/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py +++ b/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import requests + try: from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -20,13 +22,12 @@ def get_img_urls(): - import requests rs = requests.get("https://api.vk.com/method/photos.search?v=5.64") img_urls = rs.json()["response"]["items"] - return [url['photo_130'] for url in img_urls] + return [url["photo_130"] for url in img_urls] -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QScrollArea() @@ -40,9 +41,9 @@ def get_img_urls(): columns = 4 i, j = 0, 0 - mw.setWindowTitle('Loading...') + mw.setWindowTitle("Loading...") - print('Images:', len(img_urls)) + print("Images:", len(img_urls)) url_by_label = dict() @@ -65,7 +66,6 @@ def get_img_urls(): mw.setWidget(w) for n, (url, label) in enumerate(url_by_label.items()): - import requests rs = requests.get(url) image = QImage.fromData(rs.content) @@ -73,9 +73,9 @@ def get_img_urls(): label.setPixmap(pixmap) - mw.setWindowTitle('{}/{} Loading...'.format(n, len(url_by_label))) + mw.setWindowTitle(f"{n}/{len(url_by_label)} Loading...") QApplication.processEvents() - mw.setWindowTitle('Ok!') + mw.setWindowTitle("Ok!") app.exec() diff --git a/qt__pyqt__pyside__pyqode/install_translator.py b/qt__pyqt__pyside__pyqode/install_translator.py new file mode 100644 index 000000000..7ac4ed192 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/install_translator.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtCore import QTranslator, QLibraryInfo + + +def question() -> None: + QMessageBox.question( + None, + "TITLE", + "TEXT", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Abort, + ) + + +app = QApplication([]) + +# NOTE: EN +question() + +translator = QTranslator() +if translator.load("qtbase_ru", directory=QLibraryInfo.location(QLibraryInfo.TranslationsPath)): + app.installTranslator(translator) + +# NOTE: RU +question() diff --git a/qt__pyqt__pyside__pyqode/label_framed.py b/qt__pyqt__pyside__pyqode/label_framed.py index c0caf5682..3f249fa00 100644 --- a/qt__pyqt__pyside__pyqode/label_framed.py +++ b/qt__pyqt__pyside__pyqode/label_framed.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() layout = Qt.QVBoxLayout() - layout.addWidget(self.create_label_framed('Hello')) - layout.addWidget(self.create_label_framed('World')) - layout.addWidget(self.create_label_framed('999')) + layout.addWidget(self.create_label_framed("Hello")) + layout.addWidget(self.create_label_framed("World")) + layout.addWidget(self.create_label_framed("999")) self.setLayout(layout) @@ -26,7 +26,7 @@ def create_label_framed(text): return label -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py b/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py index 51630af6a..47019a532 100644 --- a/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py +++ b/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/839750/ @@ -14,26 +14,26 @@ class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - loadUi('main.ui', self) + loadUi("main.ui", self) self.btn_solve.clicked.connect(self.solve) self.btn_clear.clicked.connect(self.textEdit_words.clear) - self.textEdit_text.setPlainText('СВЕТА РОЕТ РОВ, ВОВКА СЕЕТ ОВЁС') + self.textEdit_text.setPlainText("СВЕТА РОЕТ РОВ, ВОВКА СЕЕТ ОВЁС") - def solve(self): + def solve(self) -> None: # Исходный текст text = self.textEdit_text.toPlainText() # Буквы, совпадающие с английскими - for word in re.findall(r'\b[АВСТРХОНКМУЕ]+\b', text): + for word in re.findall(r"\b[АВСТРХОНКМУЕ]+\b", text): self.textEdit_words.append(word) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py b/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py index b0ad05da2..05ba470bc 100644 --- a/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py +++ b/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QFrame class HorizontalLineWidget(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setFrameShape(QFrame.HLine) @@ -17,7 +17,7 @@ def __init__(self): class VerticalLineWidget(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setFrameShape(QFrame.VLine) @@ -25,18 +25,26 @@ def __init__(self): self.setLineWidth(1) -if __name__ == '__main__': - from PyQt5.QtWidgets import QApplication, QFormLayout, QHBoxLayout, QLineEdit, QCheckBox, QWidget, QLabel +if __name__ == "__main__": + from PyQt5.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QCheckBox, + QWidget, + QLabel, + ) from PyQt5.QtCore import Qt app = QApplication([]) layout = QFormLayout() - layout.addRow('First', QLineEdit()) - layout.addRow('Last', QLineEdit()) + layout.addRow("First", QLineEdit()) + layout.addRow("Last", QLineEdit()) layout.addRow(HorizontalLineWidget()) - layout.addRow('Phone', QLineEdit()) + layout.addRow("Phone", QLineEdit()) layout.addRow(HorizontalLineWidget()) h_layout = QHBoxLayout() @@ -56,7 +64,7 @@ def __init__(self): layout.addItem(h_layout) - layout.addRow('Ok?', QCheckBox()) + layout.addRow("Ok?", QCheckBox()) mw = QWidget() mw.setLayout(layout) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md b/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md new file mode 100644 index 000000000..d79a879b3 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md @@ -0,0 +1,3 @@ +# Screenshot + +![screenshot.png](screenshot.png) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py new file mode 100644 index 000000000..f3a07c642 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/agateau/columnresizer + + +from dataclasses import dataclass + +from PyQt5.QtCore import QEvent, QTimer, QSize, QRect, Qt, QObject, qCritical +from PyQt5.QtWidgets import ( + QFormLayout, + QGridLayout, + QWidget, + QWidgetItem, + QLayout, + QLayoutItem, +) + + +class FormLayoutWidgetItem(QWidgetItem): + def __init__( + self, + widget: QWidget, + formLayout: QFormLayout, + itemRole: QFormLayout.ItemRole, + ) -> None: + super().__init__(widget) + + self.m_width: int = -1 + self.m_formLayout: QFormLayout = formLayout + self.m_itemRole: QFormLayout.ItemRole = itemRole + + def sizeHint(self) -> QSize: + size: QSize = super().sizeHint() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def minimumSize(self) -> QSize: + size: QSize = super().minimumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def maximumSize(self) -> QSize: + size: QSize = super().maximumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def setWidth(self, width: int) -> None: + if width != self.m_width: + self.m_width = width + self.invalidate() + + def setGeometry(self, _rect: QRect) -> None: + rect: QRect = _rect + width = self.widget().sizeHint().width() + if ( + self.m_itemRole == QFormLayout.LabelRole + and self.m_formLayout.labelAlignment() & Qt.AlignRight + ): + rect.setLeft(rect.right() - width) + + super().setGeometry(rect) + + def formLayout(self) -> QFormLayout: + return self.m_formLayout + + +@dataclass +class GridColumnInfo: + layout: QGridLayout + column: int + + +class ColumnResizerPrivate: + def __init__(self, q_ptr: "ColumnResizer") -> None: + self.q: ColumnResizer = q_ptr + + self.m_widgets: list[QWidget] = [] + self.m_wrWidgetItemList: list[FormLayoutWidgetItem] = [] + self.m_gridColumnInfoList: list[GridColumnInfo] = [] + + self.m_updateTimer: QTimer = QTimer(self.q) + self.m_updateTimer.setSingleShot(True) + self.m_updateTimer.setInterval(0) + self.m_updateTimer.timeout.connect(self.q.updateWidth) + + def scheduleWidthUpdate(self) -> None: + self.m_updateTimer.start() + + +class ColumnResizer(QObject): + def __init__(self, parent: QObject) -> None: + super().__init__(parent) + + self.d = ColumnResizerPrivate(self) + + def addWidget(self, widget: QWidget) -> None: + self.d.m_widgets.append(widget) + widget.installEventFilter(self) + self.d.scheduleWidthUpdate() + + # NOTE: Here the logic is changed relative to the original + def updateWidth(self) -> None: + width: int = 0 + x: int = 0 + for widget in self.d.m_widgets: + x = max(widget.pos().x(), x) + width = max(widget.sizeHint().width(), width) + + width += x + + for item in self.d.m_wrWidgetItemList: + item.setWidth(width - item.widget().pos().x()) + item.formLayout().update() + + for info in self.d.m_gridColumnInfoList: + info.layout.setColumnMinimumWidth(info.column, width) + + def eventFilter(self, _: QObject, event: QEvent) -> bool: + if event.type() == QEvent.Resize: + self.d.scheduleWidthUpdate() + + return False + + def addWidgetsFromLayout(self, layout: QLayout, column: int) -> None: + assert column >= 0 + + if isinstance(layout, QGridLayout): + self.addWidgetsFromGridLayout(layout, column) + elif isinstance(layout, QFormLayout): + if column > QFormLayout.ItemRole.SpanningRole: + qCritical( + f"column should not be more than {QFormLayout.ItemRole.SpanningRole} for QFormLayout" + ) + return + + role: QFormLayout.ItemRole = QFormLayout.ItemRole(column) + self.addWidgetsFromFormLayout(layout, role) + else: + qCritical(f"Don't know how to handle layout {layout}") + + def addWidgetsFromGridLayout(self, layout: QGridLayout, column: int) -> None: + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAtPosition(row, column) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + self.addWidget(widget) + + self.d.m_gridColumnInfoList.append(GridColumnInfo(layout, column)) + + def addWidgetsFromFormLayout(self, layout: QFormLayout, role: QFormLayout.ItemRole) -> None: + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAt(row, role) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + layout.removeItem(item) + newItem: FormLayoutWidgetItem = FormLayoutWidgetItem(widget, layout, role) + layout.setItem(row, role, newItem) + self.addWidget(widget) + self.d.m_wrWidgetItemList.append(newItem) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py new file mode 100644 index 000000000..f3e1756ec --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/agateau/columnresizer + + +from dataclasses import dataclass + +from PyQt6.QtCore import QEvent, QTimer, QSize, QRect, Qt, QObject, qCritical +from PyQt6.QtWidgets import ( + QFormLayout, + QGridLayout, + QWidget, + QWidgetItem, + QLayout, + QLayoutItem, +) + + +class FormLayoutWidgetItem(QWidgetItem): + def __init__( + self, + widget: QWidget, + formLayout: QFormLayout, + itemRole: QFormLayout.ItemRole, + ): + super().__init__(widget) + + self.m_width: int = -1 + self.m_formLayout: QFormLayout = formLayout + self.m_itemRole: QFormLayout.ItemRole = itemRole + + def sizeHint(self) -> QSize: + size: QSize = super().sizeHint() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def minimumSize(self) -> QSize: + size: QSize = super().minimumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def maximumSize(self) -> QSize: + size: QSize = super().maximumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def setWidth(self, width: int): + if width != self.m_width: + self.m_width = width + self.invalidate() + + def setGeometry(self, _rect: QRect): + rect: QRect = _rect + width = self.widget().sizeHint().width() + if ( + self.m_itemRole == QFormLayout.ItemRole.LabelRole + and self.m_formLayout.labelAlignment() & Qt.AlignmentFlag.AlignRight + ): + rect.setLeft(rect.right() - width) + + super().setGeometry(rect) + + def formLayout(self) -> QFormLayout: + return self.m_formLayout + + +@dataclass +class GridColumnInfo: + layout: QGridLayout + column: int + + +class ColumnResizerPrivate: + def __init__(self, q_ptr: "ColumnResizer"): + self.q: ColumnResizer = q_ptr + + self.m_widgets: list[QWidget] = [] + self.m_wrWidgetItemList: list[FormLayoutWidgetItem] = [] + self.m_gridColumnInfoList: list[GridColumnInfo] = [] + + self.m_updateTimer: QTimer = QTimer(self.q) + self.m_updateTimer.setSingleShot(True) + self.m_updateTimer.setInterval(0) + self.m_updateTimer.timeout.connect(self.q.updateWidth) + + def scheduleWidthUpdate(self): + self.m_updateTimer.start() + + +class ColumnResizer(QObject): + def __init__(self, parent: QObject): + super().__init__(parent) + + self.d = ColumnResizerPrivate(self) + + def addWidget(self, widget: QWidget): + self.d.m_widgets.append(widget) + widget.installEventFilter(self) + self.d.scheduleWidthUpdate() + + # NOTE: Here the logic is changed relative to the original + def updateWidth(self): + width: int = 0 + x: int = 0 + for widget in self.d.m_widgets: + x = max(widget.pos().x(), x) + width = max(widget.sizeHint().width(), width) + + width += x + + for item in self.d.m_wrWidgetItemList: + item.setWidth(width - item.widget().pos().x()) + item.formLayout().update() + + for info in self.d.m_gridColumnInfoList: + info.layout.setColumnMinimumWidth(info.column, width) + + def eventFilter(self, _: QObject, event: QEvent) -> bool: + if event.type() == QEvent.Type.Resize: + self.d.scheduleWidthUpdate() + + return False + + def addWidgetsFromLayout(self, layout: QLayout, column: int): + assert column >= 0 + + if isinstance(layout, QGridLayout): + self.addWidgetsFromGridLayout(layout, column) + elif isinstance(layout, QFormLayout): + if column > QFormLayout.ItemRole.SpanningRole.value: + qCritical( + f"column should not be more than {QFormLayout.ItemRole.SpanningRole.value} for QFormLayout" + ) + return + + role: QFormLayout.ItemRole = QFormLayout.ItemRole(column) + self.addWidgetsFromFormLayout(layout, role) + else: + qCritical(f"Don't know how to handle layout {layout}") + + def addWidgetsFromGridLayout(self, layout: QGridLayout, column: int): + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAtPosition(row, column) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + self.addWidget(widget) + + self.d.m_gridColumnInfoList.append(GridColumnInfo(layout, column)) + + def addWidgetsFromFormLayout(self, layout: QFormLayout, role: QFormLayout.ItemRole): + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAt(row, role) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + layout.removeItem(item) + newItem: FormLayoutWidgetItem = FormLayoutWidgetItem(widget, layout, role) + layout.setItem(row, role, newItem) + self.addWidget(widget) + self.d.m_wrWidgetItemList.append(newItem) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py new file mode 100644 index 000000000..f22f01688 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import platform + +from datetime import datetime +from pathlib import Path + +from PyQt5.QtWidgets import ( + QDialog, + QWidget, + QFormLayout, + QLabel, + QDialogButtonBox, + QGroupBox, + QVBoxLayout, + QScrollArea, +) + +from column_resizer import ColumnResizer + + +DIR: Path = Path(__file__).resolve().parent + +PROGRAM_NAME: str = DIR.name + + +class About(QDialog): + def __init__(self, title: str, use_column_resizer: bool) -> None: + super().__init__() + + self.setWindowTitle(title) + + self._started: datetime = datetime.now() + + gb_python = QGroupBox("Python:") + gb_python_layout = QFormLayout(gb_python) + gb_python_layout.addRow( + "Version:", + QLabel(sys.version), + ) + gb_python_layout.addRow( + "Implementation:", + QLabel(platform.python_implementation()), + ) + gb_python_layout.addRow("Executable:", QLabel(sys.executable)) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + fields_layout = QFormLayout() + fields_layout.addRow( + "Version:", + QLabel("1.0.0"), + ) + fields_layout.addRow( + "Directory:", + QLabel(str(DIR)), + ) + fields_layout.addRow( + "Argv:", + QLabel(" ".join(sys.argv[1:])), + ) + fields_layout.addRow(gb_python) + fields_layout.addRow( + "Platform:", + QLabel(platform.platform()), + ) + + fields_widget = QWidget() + fields_layout.setContentsMargins(0, 0, 0, 0) + fields_widget.setLayout(fields_layout) + + scroll_area = QScrollArea() + scroll_area.setWidget(fields_widget) + scroll_area.setWidgetResizable(True) + scroll_area.setFrameStyle(QScrollArea.NoFrame) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QLabel(f"

    {PROGRAM_NAME}

    ")) + main_layout.addWidget(scroll_area) + main_layout.addWidget(button_box) + + if use_column_resizer: + resizer = ColumnResizer(self) + resizer.addWidgetsFromLayout(fields_layout, 0) + resizer.addWidgetsFromLayout(gb_python_layout, 0) + + self.resize(500, 300) + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + w1 = About(f"use_column_resizer=False", use_column_resizer=False) + w1.show() + + w2 = About(f"use_column_resizer=True", use_column_resizer=True) + w2.show() + w2.move(w1.x() + w1.width(), w1.y()) + + sys.exit(app.exec()) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py new file mode 100644 index 000000000..7c8997c32 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import platform + +from datetime import datetime +from pathlib import Path + +from PyQt6.QtWidgets import ( + QDialog, + QWidget, + QFormLayout, + QLabel, + QDialogButtonBox, + QGroupBox, + QVBoxLayout, + QScrollArea, +) + +from column_resizer_pyqt6 import ColumnResizer + + +DIR: Path = Path(__file__).resolve().parent + +PROGRAM_NAME: str = DIR.name + + +class About(QDialog): + def __init__(self, title: str, use_column_resizer: bool) -> None: + super().__init__() + + self.setWindowTitle(title) + + self._started: datetime = datetime.now() + + gb_python = QGroupBox("Python:") + gb_python_layout = QFormLayout(gb_python) + gb_python_layout.addRow( + "Version:", + QLabel(sys.version), + ) + gb_python_layout.addRow( + "Implementation:", + QLabel(platform.python_implementation()), + ) + gb_python_layout.addRow("Executable:", QLabel(sys.executable)) + + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + fields_layout = QFormLayout() + fields_layout.addRow( + "Version:", + QLabel("1.0.0"), + ) + fields_layout.addRow( + "Directory:", + QLabel(str(DIR)), + ) + fields_layout.addRow( + "Argv:", + QLabel(" ".join(sys.argv[1:])), + ) + fields_layout.addRow(gb_python) + fields_layout.addRow( + "Platform:", + QLabel(platform.platform()), + ) + + fields_widget = QWidget() + fields_layout.setContentsMargins(0, 0, 0, 0) + fields_widget.setLayout(fields_layout) + + scroll_area = QScrollArea() + scroll_area.setWidget(fields_widget) + scroll_area.setWidgetResizable(True) + scroll_area.setFrameStyle(QScrollArea.Shape.NoFrame) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QLabel(f"

    {PROGRAM_NAME}

    ")) + main_layout.addWidget(scroll_area) + main_layout.addWidget(button_box) + + if use_column_resizer: + resizer = ColumnResizer(self) + resizer.addWidgetsFromLayout(fields_layout, 0) + resizer.addWidgetsFromLayout(gb_python_layout, 0) + + self.resize(500, 300) + + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + + app = QApplication(sys.argv) + + w1 = About(f"use_column_resizer=False", use_column_resizer=False) + w1.show() + + w2 = About(f"use_column_resizer=True", use_column_resizer=True) + w2.show() + w2.move(w1.x() + w1.width(), w1.y()) + + sys.exit(app.exec()) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png b/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png new file mode 100644 index 000000000..b47a455c6 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png differ diff --git a/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py b/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py index 3c6d1d147..3a01b50c5 100644 --- a/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py +++ b/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5.12/qtwidgets-itemviews-fetchmore-example.html @@ -10,16 +10,30 @@ from pathlib import Path from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QListView, QGridLayout, QLabel, QWidget, - QLineEdit, QTextBrowser, QSizePolicy + QApplication, + QMainWindow, + QListView, + QGridLayout, + QLabel, + QWidget, + QLineEdit, + QTextBrowser, + QSizePolicy, +) +from PyQt5.QtCore import ( + QAbstractListModel, + QModelIndex, + Qt, + pyqtSignal, + QVariant, + QLibraryInfo, ) -from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, pyqtSignal, QVariant, QLibraryInfo class FileListModel(QAbstractListModel): numberPopulated = pyqtSignal(int) - def __init__(self, batch_size=50, parent=None): + def __init__(self, batch_size=50, parent=None) -> None: super().__init__(parent) self.fileList = [] @@ -51,13 +65,15 @@ def data(self, index: QModelIndex, role=Qt.DisplayRole) -> QVariant: def canFetchMore(self, parent: QModelIndex = None) -> bool: return self.fileCount < len(self.fileList) - def fetchMore(self, parent: QModelIndex = None): + def fetchMore(self, parent: QModelIndex = None) -> None: remainder = len(self.fileList) - self.fileCount itemsToFetch = min(self.batch_size, remainder) if itemsToFetch <= 0: return - self.beginInsertRows(QModelIndex(), self.fileCount, self.fileCount + itemsToFetch - 1) + self.beginInsertRows( + QModelIndex(), self.fileCount, self.fileCount + itemsToFetch - 1 + ) self.fileCount += itemsToFetch @@ -65,7 +81,7 @@ def fetchMore(self, parent: QModelIndex = None): self.numberPopulated.emit(itemsToFetch) - def setDirPath(self, path: str): + def setDirPath(self, path: str) -> None: self.beginResetModel() # Recursive @@ -76,7 +92,7 @@ def setDirPath(self, path: str): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Fetch More Example") @@ -110,11 +126,11 @@ def __init__(self): self.setCentralWidget(QWidget()) self.centralWidget().setLayout(layout) - def updateLog(self, number: int): + def updateLog(self, number: int) -> None: self.logViewer.append(f"{number} items added.") -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py index cda726932..fbb1e2770 100644 --- a/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = Qt.QPushButton('Load url!') + self.button = Qt.QPushButton("Load url!") self.button.clicked.connect(self.on_clicked) self.manager = Qt.QNetworkAccessManager(self) @@ -19,19 +19,19 @@ def __init__(self): self.setCentralWidget(self.button) - def on_reply_finished(self, reply): - self.setWindowTitle('After load: {}'.format(reply)) + def on_reply_finished(self, reply) -> None: + self.setWindowTitle(f"After load: {reply}") - def on_clicked(self): - url = 'https://github.com/gil9red/SimplePyScripts' + def on_clicked(self) -> None: + url = "https://github.com/gil9red/SimplePyScripts" - self.setWindowTitle('Before load') + self.setWindowTitle("Before load") # Отправляем запрос self.manager.get(Qt.QNetworkRequest(Qt.QUrl(url))) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py b/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py index a2dd23952..a5b9efb71 100644 --- a/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py +++ b/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py @@ -1,43 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import requests from PyQt5 import Qt class LoadUrlThread(Qt.QThread): load_finished = Qt.pyqtSignal(object) - def __init__(self, url): + def __init__(self, url) -> None: super().__init__() self.url = url - def run(self): - import requests + def run(self) -> None: rs = requests.get(self.url) - self.load_finished.emit(rs) class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = Qt.QPushButton('Load url!') + self.button = Qt.QPushButton("Load url!") self.button.clicked.connect(self.on_clicked) self.setCentralWidget(self.button) - def on_finished_load_url(self, rs): - self.setWindowTitle('After load: {}'.format(rs)) + def on_finished_load_url(self, rs) -> None: + self.setWindowTitle(f"After load: {rs}") - def on_clicked(self): - url = 'https://github.com/gil9red/SimplePyScripts' + def on_clicked(self) -> None: + url = "https://github.com/gil9red/SimplePyScripts" - self.setWindowTitle('Before load') + self.setWindowTitle("Before load") self.thread = LoadUrlThread(url) self.thread.load_finished.connect(self.on_finished_load_url) @@ -45,7 +44,7 @@ def on_clicked(self): self.thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py new file mode 100644 index 000000000..9535232e3 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from string import printable +from typing import Iterator + +from PyQt5.QtWidgets import QApplication, QWidget, QListView, QGroupBox, QHBoxLayout + +from iterator_list_model import IteratorListModel + + +def get_infinity_generator(): + i = 0 + while True: + yield i + i += 1 + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + main_layout = QHBoxLayout(self) + main_layout.addWidget( + self._add_view_with_it("infinity_generator", get_infinity_generator()) + ) + main_layout.addWidget( + self._add_view_with_it("range(1_000_000)", range(1_000_000)) + ) + main_layout.addWidget( + self._add_view_with_it("list of pow2", [str(i**i) for i in range(1_000)]) + ) + main_layout.addWidget(self._add_view_with_it("str", printable * 100)) + main_layout.addWidget(self._add_view_with_it("dict", dict.fromkeys(dir(self)))) + + def _add_view_with_it(self, title: str, it: Iterator) -> QWidget: + group_box = QGroupBox() + group_box.setTitle("1111") + + model = IteratorListModel(it=it) + model.rowsInserted.connect( + lambda: group_box.setTitle(f"[{title}] rows: {model.rowCount()}") + ) + + view = QListView() + view.setModel(model) + + layout = QHBoxLayout(group_box) + layout.addWidget(view) + + return group_box + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py new file mode 100644 index 000000000..f09e6fbc0 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Iterator + +from PyQt5.QtWidgets import QApplication, QListView +from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant + + +class IteratorListModel(QAbstractListModel): + def __init__(self, it: Iterator, prefetch=100, parent=None) -> None: + super().__init__(parent) + + self._at_end = False + self._it = iter(it) + self._items = [] + self._prefetch = prefetch + + def canFetchMore(self, parent: QModelIndex = None) -> bool: + return not self._at_end + + def fetchMore(self, parent: QModelIndex = None) -> None: + if self._at_end: + return + + old_rows = len(self._items) + for _ in range(self._prefetch): + try: + value = next(self._it) + self._items.append(value) + + # Если данные закончились + except StopIteration: + self._at_end = True + break + + new_rows = len(self._items) + if old_rows != new_rows: + self.beginInsertRows(QModelIndex(), old_rows, new_rows) + self.endInsertRows() + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: + if not index.isValid(): + return QVariant() + + if index.row() >= len(self._items) or index.row() < 0: + return QVariant() + + if role == Qt.DisplayRole: + return self._items[index.row()] + + return QVariant() + + def rowCount(self, parent: QModelIndex = None) -> int: + return len(self._items) + + +if __name__ == "__main__": + app = QApplication([]) + + model = IteratorListModel(it=range(1_000_000)) + model.rowsInserted.connect(lambda: mw.setWindowTitle(f"Rows: {model.rowCount()}")) + + mw = QListView() + mw.setModel(model) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py b/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py new file mode 100644 index 000000000..a92a23f4b --- /dev/null +++ b/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QWidget + + +app = QApplication([]) + +mw = QWidget() + +rect = mw.frameGeometry() +# This is where QDesktopWidget is used +center = app.desktop().availableGeometry().center() +rect.moveCenter(center) +mw.move(rect.topLeft()) + +mw.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py index dfcf28be6..614e5b281 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py @@ -1,54 +1,56 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process +import tkinter as tk +from PyQt5.Qt import QApplication, Qt, QLabel -def go_qt(name): - from PyQt5.Qt import QApplication, Qt, QLabel + +def go_qt(name) -> None: app = QApplication([]) mw = QLabel() mw.setAlignment(Qt.AlignCenter) mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) + mw.setText("Hello, " + name) mw.show() app.exec() -def go_tk(name): - import tkinter as tk +def go_tk(name) -> None: app = tk.Tk() app.minsize(150, 50) - mw = tk.Label(app, text='Hello, ' + name) - mw.pack(fill='both', expand=True) + mw = tk.Label(app, text="Hello, " + name) + mw.pack(fill="both", expand=True) app.mainloop() -def create_qt(): - p = Process(target=go_qt, args=('Qt',)) +def create_qt() -> None: + p = Process(target=go_qt, args=("Qt",)) p.start() -def create_tk(): - p = Process(target=go_tk, args=('Tk',)) +def create_tk() -> None: + p = Process(target=go_tk, args=("Tk",)) p.start() -if __name__ == '__main__': - from PyQt5.Qt import QApplication, QPushButton, QWidget, QVBoxLayout +if __name__ == "__main__": + from PyQt5.Qt import QPushButton, QWidget, QVBoxLayout + app = QApplication([]) - button_qt = QPushButton('Create Qt') + button_qt = QPushButton("Create Qt") button_qt.clicked.connect(create_qt) - button_tk = QPushButton('Create Tk') + button_tk = QPushButton("Create Tk") button_tk.clicked.connect(create_tk) layout = QVBoxLayout() diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py index 9d70cf0ae..e216d0e33 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(name): - from PyQt5.Qt import QApplication, Qt, QLabel +from PyQt5.Qt import QApplication, Qt, QLabel + + +def go(name) -> None: app = QApplication([]) mw = QLabel() mw.setAlignment(Qt.AlignCenter) mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) + mw.setText("Hello, " + name) mw.show() app.exec() -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process - p = Process(target=go, args=('bob',)) + + p = Process(target=go, args=("bob",)) p.start() p.join() diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py index 7a17434fa..c27cae3ae 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Pool from multiprocessing__QApplication__in_other_process import go with Pool() as p: - p.map(go, ['Alice', 'Bob', 'World']) + p.map(go, ["Alice", "Bob", "World"]) diff --git a/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py b/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py new file mode 100644 index 000000000..412adc556 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Maximum durations. + +""" + + +import sys + +from bs4 import BeautifulSoup + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from PyQt4.QtWebKit import * + + +QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + +video_time_list = set() +video_link_list = set() + + +def get_total_seconds(time_str): + """ + Return seconds from strings: "hh:mm:ss", "mm:ss" and "ss". + + """ + + total_seconds = 0 + + time_part = list(map(int, time_str.split(":"))) + time_part_number = len(time_part) + + if time_part_number == 1: + total_seconds += time_part[0] + + elif time_part_number == 2: + total_seconds += time_part[0] * 60 + time_part[1] + + elif time_part_number == 3: + total_seconds += time_part[0] * 3600 + time_part[1] * 60 + time_part[2] + + return total_seconds + + +def load_finished_handler(ok) -> None: + if not ok: + return + + doc = view.page().mainFrame().documentElement() + + def get_video_time_list(): + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + return [title.text for title in root.select(".video-time")] + + def get_video_link_list(): + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + return {a["href"] for a in root.select(".yt-uix-tile-link")} + + # Get video time and append to list + global video_time_list + video_time_list.update(get_video_time_list()) + + # Get video link and append to list + global video_link_list + video_link_list.update(get_video_link_list()) + + # Every 5 seconds + timer = QTimer() + timer.setInterval(5000) + + def timeout_handler() -> None: + doc = view.page().mainFrame().documentElement() + button = doc.findFirst(".load-more-button") + + # If button "More" visible + is_load_more = not button.isNull() + if is_load_more: + # Get video time and append global list + global video_time_list + video_time_list.update(get_video_time_list()) + + # Get video link and append to list + global video_link_list + video_link_list.update(get_video_link_list()) + + else: + timer.stop() + return + + # Click on button "More" + print("More") + button.evaluateJavaScript("this.click()") + + timer.timeout.connect(timeout_handler) + timer.start() + + # Wait loading all video + while timer.isActive(): + QApplication.instance().processEvents() + + print() + print("Total:") + print(f" Videos: {len(video_link_list)}, {video_link_list}") + print(f" Durations: {len(video_time_list)}, {video_time_list}") + + # Find maximum duration + max_total_seconds = 0 + max_time_str = None + + for time_str in video_time_list: + total_seconds = get_total_seconds(time_str) + if total_seconds > max_total_seconds: + max_total_seconds = total_seconds + max_time_str = time_str + + print(f" Max durations: {max_time_str}") + + sys.exit() + + +if __name__ == "__main__": + app = QApplication([]) + + view = QWebView() + # view.show() + + view.loadFinished.connect(load_finished_handler) + + url = QUrl("https://www.youtube.com/channel/UCWvMAvbk23gFzZ3BRUfACRA/videos") + view.load(url) + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py b/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py new file mode 100644 index 000000000..8302eb998 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Summary durations all video. + +""" + + +import sys + +from bs4 import BeautifulSoup + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from PyQt4.QtWebKit import * + + +QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + +url_video_by_time_dict = dict() + + +def get_total_seconds(time_str): + """ + Return seconds from strings: "hh:mm:ss", "mm:ss" and "ss". + + """ + + total_seconds = 0 + + time_part = list(map(int, time_str.split(":"))) + time_part_number = len(time_part) + + if time_part_number == 1: + total_seconds += time_part[0] + + elif time_part_number == 2: + total_seconds += time_part[0] * 60 + time_part[1] + + elif time_part_number == 3: + total_seconds += time_part[0] * 3600 + time_part[1] * 60 + time_part[2] + + return total_seconds + + +def load_finished_handler(ok) -> None: + if not ok: + return + + doc = view.page().mainFrame().documentElement() + + def update_url_video_by_time_dict() -> None: + global url_video_by_time_dict + + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + for item in root.select(".channels-content-item"): + url = item.select_one(".yt-uix-tile-link")["href"] + time_str = item.select_one(".video-time").text + + url_video_by_time_dict[url] = time_str + + # Fill dict + update_url_video_by_time_dict() + + # Every 5 seconds + timer = QTimer() + timer.setInterval(5000) + + def timeout_handler() -> None: + doc = view.page().mainFrame().documentElement() + button = doc.findFirst(".load-more-button") + + # If button "More" visible + is_load_more = not button.isNull() + if is_load_more: + # Fill dict + update_url_video_by_time_dict() + + else: + timer.stop() + return + + # Click on button "More" + print("More") + button.evaluateJavaScript("this.click()") + + timer.timeout.connect(timeout_handler) + timer.start() + + # Wait loading all video + while timer.isActive(): + QApplication.instance().processEvents() + + print() + print("Total:") + print(f" Videos: {len(url_video_by_time_dict)}") + print(f" Durations: {list(url_video_by_time_dict.values())}") + + # Summary duration + sum_seconds = 0 + + for time_str in url_video_by_time_dict.values(): + sum_seconds += get_total_seconds(time_str) + + mm, ss = divmod(sum_seconds, 60) + hh, mm = divmod(mm, 60) + sum_time_str = "%d:%02d:%02d" % (hh, mm, ss) + + print(f" Summary duration (secs): {sum_seconds}") + print(f" Summary duration: {sum_time_str}") + + sys.exit() + + +if __name__ == "__main__": + app = QApplication([]) + + view = QWebView() + # view.show() + + view.loadFinished.connect(load_finished_handler) + + url = QUrl("https://www.youtube.com/channel/UCWvMAvbk23gFzZ3BRUfACRA/videos") + view.load(url) + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py b/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py index 85a92914b..518bc7668 100644 --- a/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py +++ b/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt -def _on_media_status_changed(status): +def _on_media_status_changed(status) -> None: if status == Qt.QMediaPlayer.EndOfMedia: Qt.QCoreApplication.instance().quit() @@ -15,7 +15,7 @@ def _on_media_status_changed(status): app = Qt.QCoreApplication([]) player = Qt.QMediaPlayer() -file_name = Qt.QUrl.fromLocalFile('example.mp3') +file_name = Qt.QUrl.fromLocalFile("example.mp3") player.setMedia(Qt.QMediaContent(file_name)) player.mediaStatusChanged.connect(_on_media_status_changed) player.play() diff --git a/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py b/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py index 11f1a81be..8578f4927 100644 --- a/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py +++ b/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEngineView -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) view = QWebEngineView() view.show() - view.setHtml("""\ + view.setHtml( + """\ - """) + """ + ) app.exec() diff --git a/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py b/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py index 8dd5fded4..104fd428e 100644 --- a/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py +++ b/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -14,7 +14,7 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) printer = Qt.QPrinter() diff --git a/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py b/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py index ed4a0bc22..449ae3993 100644 --- a/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py +++ b/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -14,7 +14,7 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) printer = Qt.QPrinter() @@ -23,7 +23,7 @@ te.setHtml(html) te.show() - printer.setOutputFileName('result.pdf') + printer.setOutputFileName("result.pdf") te.print(printer) app.exec() diff --git a/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py b/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py index d7193ee4c..89dd73810 100644 --- a/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py +++ b/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py @@ -1,17 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + -from PyQt5.Qt import QApplication, QMainWindow, QSplitter, QTreeView, QTextEdit, QFileSystemModel, QDir import os +from PyQt5.Qt import ( + QApplication, + QMainWindow, + QSplitter, + QTreeView, + QTextEdit, + QFileSystemModel, + QDir, +) class MainWindow(QMainWindow): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - self.setWindowTitle('Direct tree') + self.setWindowTitle("Direct tree") self.model = QFileSystemModel() self.model.setRootPath(QDir.rootPath()) @@ -34,10 +43,10 @@ def __init__(self, parent=None): self.setCentralWidget(splitter) - def _on_double_clicked(self, index): + def _on_double_clicked(self, index) -> None: file_name = self.model.filePath(index) - with open(file_name, encoding='utf-8') as f: + with open(file_name, encoding="utf-8") as f: text = f.read() self.textEdit.setPlainText(text) diff --git a/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py b/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py index e9392d508..c9d857d1e 100644 --- a/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py +++ b/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from random import randint -from typing import List -from PyQt5.Qt import * +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton +from PyQt5.QtGui import QPainter, QColor, QPen +from PyQt5.QtCore import QTimer, Qt # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1d6226ea5545e49b533e71c92d77856a7b2e171d/pygame__examples/simple_balls.py @@ -29,8 +30,15 @@ def update(self): self.y += self.v_y def draw(self, painter: QPainter): - r, g, b = self.color - color = QColor(r, g, b) + if isinstance(self.color, tuple): + if len(self.color) == 4: + r, g, b, a = self.color + color = QColor(r, g, b, a) + else: + r, g, b = self.color + color = QColor(r, g, b) + else: + color = self.color painter.save() painter.setPen(Qt.black) @@ -59,8 +67,21 @@ def right(self): return self.x + self.r +def get_random_vector() -> tuple[int, int]: + pos = 0, 0 + # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться + while pos == (0, 0): + pos = randint(-3, 3), randint(-3, 3) + + return pos + + +def get_random_color() -> tuple[int, int, int]: + return randint(0, 255), randint(0, 255), randint(0, 255) + + class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlags(Qt.FramelessWindowHint) @@ -69,7 +90,7 @@ def __init__(self): self._old_pos = None self.frame_color = Qt.darkCyan - self.balls: List[Ball] = [] + self.balls: list[Ball] = [] timeout = 1000 // 60 @@ -84,22 +105,22 @@ def __init__(self): self.setLayout(layout) - def mousePressEvent(self, event): + def mousePressEvent(self, event) -> None: if event.button() == Qt.LeftButton: self._old_pos = event.pos() - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event) -> None: if event.button() == Qt.LeftButton: self._old_pos = None - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: if not self._old_pos: return delta = event.pos() - self._old_pos self.move(self.pos() + delta) - def tick(self): + def tick(self) -> None: for ball in self.balls: ball.update() @@ -114,7 +135,7 @@ def tick(self): # Вызов перерисовки self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(QColor(0, 0, 0, 1)) @@ -124,18 +145,7 @@ def paintEvent(self, event): for ball in self.balls: ball.draw(painter) - def append_random_ball(self): - def get_random_vector(): - pos = 0, 0 - # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться - while pos == (0, 0): - pos = randint(-3, 3), randint(-3, 3) - - return pos - - def get_random_color(): - return randint(0, 255), randint(0, 255), randint(0, 255) - + def append_random_ball(self) -> None: x = self.width() // 2 + randint(-self.width() // 4, self.width() // 4) y = self.height() // 2 + randint(-self.height() // 4, self.height() // 4) r = randint(10, 20) @@ -146,10 +156,10 @@ def get_random_color(): self.balls.append(ball) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - WIDTH = 600 # ширина экрана + WIDTH = 600 # ширина экрана HEIGHT = 600 # высота экрана BALL_NUMBER = 1000 diff --git a/qt__pyqt__pyside__pyqode/pyqode_example/main.py b/qt__pyqt__pyside__pyqode/pyqode_example/main.py index a98fd2ea1..d08cc1266 100644 --- a/qt__pyqt__pyside__pyqode/pyqode_example/main.py +++ b/qt__pyqt__pyside__pyqode/pyqode_example/main.py @@ -2,15 +2,16 @@ # -*- coding: utf-8 -*- -if __name__ == '__main__': +if __name__ == "__main__": import sys - from PySide.QtGui import QApplication, QMainWindow - app = QApplication(sys.argv) + from PySide.QtGui import QApplication, QMainWindow from pyqode.core import api from pyqode.core import modes + app = QApplication(sys.argv) + editor = api.CodeEdit() # Добавление модов: подсветка кода, подсветка текущей строки и @@ -19,10 +20,10 @@ editor.modes.append(modes.CaretLineHighlighterMode()) editor.modes.append(modes.IndenterMode()) - editor.setPlainText(open(__file__, encoding='utf-8').read(), None, None) + editor.setPlainText(open(__file__, encoding="utf-8").read(), None, None) mw = QMainWindow() - mw.setWindowTitle('pyqode') + mw.setWindowTitle("pyqode") mw.setCentralWidget(editor) mw.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py index b661bb814..38a464657 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py +++ b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py @@ -1,36 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import QtWidgets as qtw from PyQt5.QtCore import QRect, QTimer -def show_tooltip(parent, widget): - qtw.QToolTip.showText(parent.mapToGlobal(widget.pos()), widget.toolTip(), widget, QRect()) +def show_tooltip(parent, widget) -> None: + qtw.QToolTip.showText( + parent.mapToGlobal(widget.pos()), widget.toolTip(), widget, QRect() + ) -if __name__ == '__main__': +if __name__ == "__main__": app = qtw.QApplication([]) line_edit = qtw.QLineEdit() - line_edit.setToolTip('This my LINE EDIT!') + line_edit.setToolTip("This my LINE EDIT!") - button = qtw.QPushButton('My button!') - button.setToolTip('Simple button...') + button = qtw.QPushButton("My button!") + button.setToolTip("Simple button...") text_edit = qtw.QTextEdit() - text_edit.setToolTip('TextEdit!') + text_edit.setToolTip("TextEdit!") layout = qtw.QFormLayout() - layout.addRow('Line edit:', line_edit) - layout.addRow('Button:', button) - layout.addRow('Text edit:', text_edit) + layout.addRow("Line edit:", line_edit) + layout.addRow("Button:", button) + layout.addRow("Text edit:", text_edit) w = qtw.QWidget() - w.setWindowTitle('Tooltip example') + w.setWindowTitle("Tooltip example") w.setLayout(layout) w.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py index bbe097d44..d2219b5ea 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py +++ b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import QtWidgets as qtw @@ -9,29 +9,32 @@ from PyQt5.QtGui import QHelpEvent -def show_tooltip(parent, widget): - app.notify(widget, QHelpEvent(QHelpEvent.ToolTip, widget.pos(), parent.mapToGlobal(widget.pos()))) +def show_tooltip(parent, widget) -> None: + app.notify( + widget, + QHelpEvent(QHelpEvent.ToolTip, widget.pos(), parent.mapToGlobal(widget.pos())), + ) -if __name__ == '__main__': +if __name__ == "__main__": app = qtw.QApplication([]) line_edit = qtw.QLineEdit() - line_edit.setToolTip('This my LINE EDIT!') + line_edit.setToolTip("This my LINE EDIT!") - button = qtw.QPushButton('My button!') - button.setToolTip('Simple button...') + button = qtw.QPushButton("My button!") + button.setToolTip("Simple button...") text_edit = qtw.QTextEdit() - text_edit.setToolTip('TextEdit!') + text_edit.setToolTip("TextEdit!") layout = qtw.QFormLayout() - layout.addRow('Line edit:', line_edit) - layout.addRow('Button:', button) - layout.addRow('Text edit:', text_edit) + layout.addRow("Line edit:", line_edit) + layout.addRow("Button:", button) + layout.addRow("Text edit:", text_edit) w = qtw.QWidget() - w.setWindowTitle('Tooltip example') + w.setWindowTitle("Tooltip example") w.setLayout(layout) w.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py b/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py index d58201587..4dc2d35f8 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import * class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.tb_result = QTextBrowser() self.cb_pets = QComboBox() self.cb_pets.currentIndexChanged.connect(self._on_pet_changed) - self.cb_pets.addItem('Собаки', userData='dogs') - self.cb_pets.addItem('Коты', userData='cats') + self.cb_pets.addItem("Собаки", userData="dogs") + self.cb_pets.addItem("Коты", userData="cats") layout = QVBoxLayout() layout.addWidget(self.cb_pets) @@ -24,7 +24,7 @@ def __init__(self): self.setLayout(layout) - def _on_pet_changed(self, index): + def _on_pet_changed(self, index) -> None: # print(index) # 0 # print(self.cb_pets.itemText(index)) # Собаки # print(self.cb_pets.itemData(index)) # dogs @@ -34,17 +34,17 @@ def _on_pet_changed(self, index): # print(self.cb_pets.currentData()) # dogs data = self.cb_pets.itemData(index) - if data == 'cats': + if data == "cats": text = "Вы любите кошек" - elif data == 'dogs': + elif data == "dogs": text = "Вы любите собак" else: - text = '' + text = "" self.tb_result.setHtml(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py b/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py index 2663bfd4b..4919e88a7 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py @@ -1,17 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -from PyQt5.Qt import ( - QWidget, QDir, QFileSystemModel, QTreeView, QListWidget, - QPushButton, QSplitter, Qt, QVBoxLayout, QApplication +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import ( + QWidget, + QFileSystemModel, + QTreeView, + QListWidget, + QPushButton, + QSplitter, + QVBoxLayout, + QApplication, ) +from PyQt5.QtCore import QDir, Qt class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() path = QDir.rootPath() @@ -22,11 +29,13 @@ def __init__(self): self.tree_view = QTreeView() self.tree_view.setModel(self.model) self.tree_view.setSelectionMode(QTreeView.ExtendedSelection) - self.tree_view.selectionModel().selectionChanged.connect(self._on_selection_changed) + self.tree_view.selectionModel().selectionChanged.connect( + self._on_selection_changed + ) self.list_files = QListWidget() - self.button_add = QPushButton('Добавить!') + self.button_add = QPushButton("Добавить!") self.button_add.setEnabled(False) self.button_add.clicked.connect(self._on_add) @@ -38,17 +47,17 @@ def __init__(self): main_layout.addWidget(splitter) main_layout.addWidget(self.button_add) - def _on_selection_changed(self, selected, deselected): + def _on_selection_changed(self, selected, deselected) -> None: has = self.tree_view.selectionModel().hasSelection() self.button_add.setEnabled(has) - def _on_add(self): + def _on_add(self) -> None: for row in self.tree_view.selectionModel().selectedRows(): path = self.model.filePath(row) self.list_files.addItem(path) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py b/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py index 602fe569f..3b269007d 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime @@ -9,7 +9,7 @@ class MessagesWindow(QtWidgets.QTableView): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setModel(ModelMessages()) @@ -18,30 +18,49 @@ def __init__(self, parent=None): class ModelMessages(QtGui.QStandardItemModel): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) class ItemDateFormat(QtWidgets.QItemDelegate): - def paint(self, painter, opt: QtWidgets.QStyleOptionViewItem, index): + def paint(self, painter, opt: QtWidgets.QStyleOptionViewItem, index) -> None: self.drawBackground(painter, opt, index) self.drawFocus(painter, opt, opt.rect) try: - opt.text = '{0:%d %b %H:%M}'.format(datetime.strptime(index.model().data(index), '%Y-%m-%d %H:%M:%S.%f')) + value = index.model().data(index) + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') + opt.text = f"{dt:%d %b %H:%M}" except TypeError: pass opt.font.setItalic(True) opt.backgroundBrush = QtCore.Qt.yellow - QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, opt, painter) + QtWidgets.QApplication.style().drawControl( + QtWidgets.QStyle.CE_ItemViewItem, opt, painter + ) -if __name__ == '__main__': +if __name__ == "__main__": app = QtWidgets.QApplication([]) w = MessagesWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py b/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py index a96a0cf0a..919573d60 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py index 3ccf32644..0d8831cda 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.Qt import * +from PyQt5.QtCore import Qt, QRegularExpression +from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QFont +from PyQt5.QtWidgets import QApplication, QPlainTextEdit, QTextEdit class MyHighlighter(QSyntaxHighlighter): - def highlightBlock(self, text): + def highlightBlock(self, text) -> None: char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setForeground(Qt.darkMagenta) @@ -20,14 +22,23 @@ def highlightBlock(self, text): self.setFormat(match.capturedStart(), match.capturedLength(), char_format) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - mw = QTextEdit() - mw.setText("x = sin(1)\nb = cos(sin(PI));") - - highlighter = MyHighlighter(mw.document()) - - mw.show() + text_edit = QTextEdit() + text_edit.setWindowTitle("QTextEdit") + text_edit.setText("x = sin(1)\nb = cos(sin(PI));") + highlighter_text_edit = MyHighlighter(text_edit.document()) + text_edit.show() + + plain_text_edit = QPlainTextEdit() + plain_text_edit.setWindowTitle("QPlainTextEdit") + plain_text_edit.setPlainText(text_edit.toPlainText()) + highlighter_plain_text_edit = MyHighlighter(plain_text_edit.document()) + plain_text_edit.show() + + pos = text_edit.pos() + pos.setX(pos.x() + text_edit.width() + 5) + plain_text_edit.move(pos) app.exec() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py index 7d1a9283f..0c3db8a08 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import * class MyHighlighter(QSyntaxHighlighter): - def __init__(self, parent): + def __init__(self, parent) -> None: super().__init__(parent) self.regexp_by_format = dict() @@ -16,24 +16,26 @@ def __init__(self, parent): char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setForeground(Qt.darkMagenta) - self.regexp_by_format[r'\bsin\b'] = char_format + self.regexp_by_format[r"\bsin\b"] = char_format char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setFontItalic(True) char_format.setForeground(Qt.darkCyan) - self.regexp_by_format[r'\bcos\b'] = char_format + self.regexp_by_format[r"\bcos\b"] = char_format - def highlightBlock(self, text): + def highlightBlock(self, text) -> None: for regexp, char_format in self.regexp_by_format.items(): expression = QRegularExpression(regexp) it = expression.globalMatch(text) while it.hasNext(): match = it.next() - self.setFormat(match.capturedStart(), match.capturedLength(), char_format) + self.setFormat( + match.capturedStart(), match.capturedLength(), char_format + ) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QTextEdit() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py index 1cfb3a9bc..79cf2fd54 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QMainWindow, QTableWidget, QTableWidgetItem, QApplication @@ -9,17 +9,17 @@ class Widget(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Table') + self.setWindowTitle("Table") self.table_widget = QTableWidget() self.table_widget.cellClicked.connect(self._on_cell_clicked) self.setCentralWidget(self.table_widget) - def _on_cell_clicked(self, row, col): + def _on_cell_clicked(self, row, col) -> None: selection_model = self.table_widget.selectionModel() selection_model.clear() @@ -31,18 +31,18 @@ def _on_cell_clicked(self, row, col): selection_model.select(index, QItemSelectionModel.Select) - def fill(self): + def fill(self) -> None: self.table_widget.clear() - labels = ['ID', 'NAME', 'PRICE'] + labels = ["ID", "NAME", "PRICE"] self.table_widget.setColumnCount(len(labels)) self.table_widget.setHorizontalHeaderLabels(labels) for id_, name, price in [ - ['1', 'name_1', 'price_1'], - ['2', 'name_2', 'price_2'], - ['3', 'name_3', 'price_3'], + ["1", "name_1", "price_1"], + ["2", "name_2", "price_2"], + ["3", "name_3", "price_3"], ]: row = self.table_widget.rowCount() self.table_widget.setRowCount(row + 1) @@ -52,7 +52,7 @@ def fill(self): self.table_widget.setItem(row, 2, QTableWidgetItem(price)) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py index a3d4cbfc5..9c67d101c 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.table = Qt.QTableWidget() @@ -16,7 +16,7 @@ def __init__(self): self.table.setColumnCount(2) self.table.itemDoubleClicked.connect(self.on_cell_item_clicked) - rows = ['Vasya', 'Petya', 'Masha'] + rows = ["Vasya", "Petya", "Masha"] for i, value in enumerate(rows): self.table.setItem(i, 0, Qt.QTableWidgetItem(str(i + 1))) @@ -29,15 +29,17 @@ def __init__(self): self.setCentralWidget(main_widget) - def on_cell_item_clicked(self, item): + def on_cell_item_clicked(self, item) -> None: print(item) - new_text, ok = Qt.QInputDialog.getText(self, 'Change Name', 'Change Name', text=item.text()) + new_text, ok = Qt.QInputDialog.getText( + self, "Change Name", "Change Name", text=item.text() + ) if ok: item.setText(new_text) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py b/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py index b203c538e..4d8d76894 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time from PyQt5.Qt import * class RunFuncThread(QThread): run_finished = pyqtSignal(object) - def __init__(self, func): + def __init__(self, func) -> None: super().__init__() self.func = func - def run(self): + def run(self) -> None: self.run_finished.emit(self.func()) class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.pb_go = QPushButton('Go') + self.pb_go = QPushButton("Go") self.pb_go.clicked.connect(self.go) self.te_log = QPlainTextEdit() @@ -34,14 +35,13 @@ def __init__(self): self.setLayout(layout) - def _on_run_finished(self, value): + def _on_run_finished(self, value) -> None: self.te_log.setPlainText(str(value)) - def go(self): + def go(self) -> None: progress_dialog = QProgressDialog(self) def foo(): - import time items = [] for i in range(10): @@ -57,13 +57,13 @@ def foo(): thread.run_finished.connect(progress_dialog.close) thread.start() - progress_dialog.setWindowTitle('Please wait...') + progress_dialog.setWindowTitle("Please wait...") progress_dialog.setLabelText(progress_dialog.windowTitle()) progress_dialog.setRange(0, 0) progress_dialog.exec() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py b/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py index 5e882a3e2..047c7fe30 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QTreeView, QFileSystemModel, QApplication from PyQt5.QtCore import QDir -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) model = QFileSystemModel() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py index 1c9787b21..8013eed2f 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -10,7 +10,7 @@ class Client(QWebEnginePage): - def __init__(self, urls): + def __init__(self, urls) -> None: self.app = QApplication([]) super().__init__() @@ -22,10 +22,10 @@ def __init__(self, urls): self.load(QUrl(url)) self.app.exec_() - def _on_load_finished(self): + def _on_load_finished(self) -> None: self.toHtml(self.callable) - def callable(self, html_str): + def callable(self, html_str) -> None: self.response_list.append(html_str) self.app.quit() @@ -35,22 +35,23 @@ def go(urls): return client.response_list -if __name__ == '__main__': +if __name__ == "__main__": urls = [ [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://yandex.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://yandex.ru/", ], [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://www.google.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://www.google.ru/", ], [ - 'https://www.google.ru/', - ] + "https://www.google.ru/", + ], ] from multiprocessing import Pool + with Pool() as p: results = p.map(go, urls) print(len(results)) @@ -61,7 +62,7 @@ def go(urls): print(len(result)) for html in result: - with open('result_{}.html'.format(number), 'w', encoding='utf-8') as f: + with open(f"result_{number}.html", "w", encoding="utf-8") as f: f.write(html) number += 1 diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py index 4cc5bbd9d..9a6d08c47 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -10,7 +10,7 @@ class Client(QWebEnginePage): - def __init__(self, urls): + def __init__(self, urls) -> None: self.app = QApplication([]) super().__init__() @@ -22,19 +22,20 @@ def __init__(self, urls): self.load(QUrl(url)) self.app.exec_() - def _on_load_finished(self): + def _on_load_finished(self) -> None: self.toHtml(self.callable) - def callable(self, html_str): + def callable(self, html_str) -> None: self.response_list.append(html_str) self.app.quit() -if __name__ == '__main__': +if __name__ == "__main__": urls = [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://www.google.ru/', - 'https://yandex.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://www.google.ru/", + "https://yandex.ru/", ] client = Client(urls) - print(len(client.response_list), client.response_list) # 3 [' - - - - - - - - - - - - - - - - diff --git a/webserver__handler_key_mouse_click_and_move/static/nipplejs/nipplejs.js b/webserver__handler_key_mouse_click_and_move/static/nipplejs/nipplejs.js deleted file mode 100644 index d404c12e9..000000000 --- a/webserver__handler_key_mouse_click_and_move/static/nipplejs/nipplejs.js +++ /dev/null @@ -1,1474 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.nipplejs = f()}})(function(){var define,module,exports; -'use strict'; - -// Constants -var isTouch = !!('ontouchstart' in window); -var isPointer = window.PointerEvent ? true : false; -var isMSPointer = window.MSPointerEvent ? true : false; -var events = { - touch: { - start: 'touchstart', - move: 'touchmove', - end: 'touchend, touchcancel' - }, - mouse: { - start: 'mousedown', - move: 'mousemove', - end: 'mouseup' - }, - pointer: { - start: 'pointerdown', - move: 'pointermove', - end: 'pointerup, pointercancel' - }, - MSPointer: { - start: 'MSPointerDown', - move: 'MSPointerMove', - end: 'MSPointerUp' - } -}; -var toBind; -var secondBind = {}; -if (isPointer) { - toBind = events.pointer; -} else if (isMSPointer) { - toBind = events.MSPointer; -} else if (isTouch) { - toBind = events.touch; - secondBind = events.mouse; -} else { - toBind = events.mouse; -} - -/////////////////////// -/// UTILS /// -/////////////////////// - -var u = {}; -u.distance = function (p1, p2) { - var dx = p2.x - p1.x; - var dy = p2.y - p1.y; - - return Math.sqrt((dx * dx) + (dy * dy)); -}; - -u.angle = function(p1, p2) { - var dx = p2.x - p1.x; - var dy = p2.y - p1.y; - - return u.degrees(Math.atan2(dy, dx)); -}; - -u.findCoord = function(p, d, a) { - var b = {x: 0, y: 0}; - a = u.radians(a); - b.x = p.x - d * Math.cos(a); - b.y = p.y - d * Math.sin(a); - return b; -}; - -u.radians = function(a) { - return a * (Math.PI / 180); -}; - -u.degrees = function(a) { - return a * (180 / Math.PI); -}; - -u.bindEvt = function (el, arg, handler) { - var types = arg.split(/[ ,]+/g); - var type; - for (var i = 0; i < types.length; i += 1) { - type = types[i]; - if (el.addEventListener) { - el.addEventListener(type, handler, false); - } else if (el.attachEvent) { - el.attachEvent(type, handler); - } - } -}; - -u.unbindEvt = function (el, arg, handler) { - var types = arg.split(/[ ,]+/g); - var type; - for (var i = 0; i < types.length; i += 1) { - type = types[i]; - if (el.removeEventListener) { - el.removeEventListener(type, handler); - } else if (el.detachEvent) { - el.detachEvent(type, handler); - } - } -}; - -u.trigger = function (el, type, data) { - var evt = new CustomEvent(type, data); - el.dispatchEvent(evt); -}; - -u.prepareEvent = function (evt) { - evt.preventDefault(); - return evt.type.match(/^touch/) ? evt.changedTouches : evt; -}; - -u.getScroll = function () { - var x = (window.pageXOffset !== undefined) ? - window.pageXOffset : - (document.documentElement || document.body.parentNode || document.body) - .scrollLeft; - - var y = (window.pageYOffset !== undefined) ? - window.pageYOffset : - (document.documentElement || document.body.parentNode || document.body) - .scrollTop; - return { - x: x, - y: y - }; -}; - -u.applyPosition = function (el, pos) { - if (pos.top || pos.right || pos.bottom || pos.left) { - el.style.top = pos.top; - el.style.right = pos.right; - el.style.bottom = pos.bottom; - el.style.left = pos.left; - } else { - el.style.left = pos.x + 'px'; - el.style.top = pos.y + 'px'; - } -}; - -u.getTransitionStyle = function (property, values, time) { - var obj = u.configStylePropertyObject(property); - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - if (typeof values === 'string') { - obj[i] = values + ' ' + time; - } else { - var st = ''; - for (var j = 0, max = values.length; j < max; j += 1) { - st += values[j] + ' ' + time + ', '; - } - obj[i] = st.slice(0, -2); - } - } - } - return obj; -}; - -u.getVendorStyle = function (property, value) { - var obj = u.configStylePropertyObject(property); - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - obj[i] = value; - } - } - return obj; -}; - -u.configStylePropertyObject = function (prop) { - var obj = {}; - obj[prop] = ''; - var vendors = ['webkit', 'Moz', 'o']; - vendors.forEach(function (vendor) { - obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = ''; - }); - return obj; -}; - -u.extend = function (objA, objB) { - for (var i in objB) { - if (objB.hasOwnProperty(i)) { - objA[i] = objB[i]; - } - } - return objA; -}; - -// Overwrite only what's already present -u.safeExtend = function (objA, objB) { - var obj = {}; - for (var i in objA) { - if (objA.hasOwnProperty(i) && objB.hasOwnProperty(i)) { - obj[i] = objB[i]; - } else if (objA.hasOwnProperty(i)) { - obj[i] = objA[i]; - } - } - return obj; -}; - -// Map for array or unique item. -u.map = function (ar, fn) { - if (ar.length) { - for (var i = 0, max = ar.length; i < max; i += 1) { - fn(ar[i]); - } - } else { - fn(ar); - } -}; - -/////////////////////// -/// SUPER CLASS /// -/////////////////////// - -function Super () {}; - -// Basic event system. -Super.prototype.on = function (arg, cb) { - var self = this; - var types = arg.split(/[ ,]+/g); - var type; - self._handlers_ = self._handlers_ || {}; - - for (var i = 0; i < types.length; i += 1) { - type = types[i]; - self._handlers_[type] = self._handlers_[type] || []; - self._handlers_[type].push(cb); - } - return self; -}; - -Super.prototype.off = function (type, cb) { - var self = this; - self._handlers_ = self._handlers_ || {}; - - if (type === undefined) { - self._handlers_ = {}; - } else if (cb === undefined) { - self._handlers_[type] = null; - } else if (self._handlers_[type] && - self._handlers_[type].indexOf(cb) >= 0) { - self._handlers_[type].splice(self._handlers_[type].indexOf(cb), 1); - } - - return self; -}; - -Super.prototype.trigger = function (arg, data) { - var self = this; - var types = arg.split(/[ ,]+/g); - var type; - self._handlers_ = self._handlers_ || {}; - - for (var i = 0; i < types.length; i += 1) { - type = types[i]; - if (self._handlers_[type] && self._handlers_[type].length) { - self._handlers_[type].forEach(function (handler) { - handler.call(self, { - type: type, - target: self - }, data); - }); - } - } -}; - -// Configuration -Super.prototype.config = function (options) { - var self = this; - self.options = self.defaults || {}; - if (options) { - self.options = u.safeExtend(self.options, options); - } -}; - -// Bind internal events. -Super.prototype.bindEvt = function (el, type) { - var self = this; - self._domHandlers_ = self._domHandlers_ || {}; - - self._domHandlers_[type] = function () { - if (typeof self['on' + type] === 'function') { - self['on' + type].apply(self, arguments); - } else { - console.warn('[WARNING] : Missing "on' + type + '" handler.'); - } - }; - - u.bindEvt(el, toBind[type], self._domHandlers_[type]); - - if (secondBind[type]) { - // Support for both touch and mouse at the same time. - u.bindEvt(el, secondBind[type], self._domHandlers_[type]); - } - - return self; -}; - -// Unbind dom events. -Super.prototype.unbindEvt = function (el, type) { - var self = this; - self._domHandlers_ = self._domHandlers_ || {}; - - u.unbindEvt(el, toBind[type], self._domHandlers_[type]); - - if (secondBind[type]) { - // Support for both touch and mouse at the same time. - u.unbindEvt(el, secondBind[type], self._domHandlers_[type]); - } - - delete self._domHandlers_[type]; - - return this; -}; - -/////////////////////// -/// THE NIPPLE /// -/////////////////////// - -function Nipple (collection, options) { - this.identifier = options.identifier; - this.position = options.position; - this.frontPosition = options.frontPosition; - this.collection = collection; - - // Defaults - this.defaults = { - size: 100, - threshold: 0.1, - color: 'white', - fadeTime: 250, - dataOnly: false, - restJoystick: true, - restOpacity: 0.5, - mode: 'dynamic', - zone: document.body, - lockX: false, - lockY: false - }; - - this.config(options); - - // Overwrites - if (this.options.mode === 'dynamic') { - this.options.restOpacity = 0; - } - - this.id = Nipple.id; - Nipple.id += 1; - this.buildEl() - .stylize(); - - // Nipple's API. - this.instance = { - el: this.ui.el, - on: this.on.bind(this), - off: this.off.bind(this), - show: this.show.bind(this), - hide: this.hide.bind(this), - add: this.addToDom.bind(this), - remove: this.removeFromDom.bind(this), - destroy: this.destroy.bind(this), - resetDirection: this.resetDirection.bind(this), - computeDirection: this.computeDirection.bind(this), - trigger: this.trigger.bind(this), - position: this.position, - frontPosition: this.frontPosition, - ui: this.ui, - identifier: this.identifier, - id: this.id, - options: this.options - }; - - return this.instance; -}; - -Nipple.prototype = new Super(); -Nipple.constructor = Nipple; -Nipple.id = 0; - -// Build the dom element of the Nipple instance. -Nipple.prototype.buildEl = function (options) { - this.ui = {}; - - if (this.options.dataOnly) { - return this; - } - - this.ui.el = document.createElement('div'); - this.ui.back = document.createElement('div'); - this.ui.front = document.createElement('div'); - - this.ui.el.className = 'nipple collection_' + this.collection.id; - this.ui.back.className = 'back'; - this.ui.front.className = 'front'; - - this.ui.el.setAttribute('id', 'nipple_' + this.collection.id + - '_' + this.id); - - this.ui.el.appendChild(this.ui.back); - this.ui.el.appendChild(this.ui.front); - - return this; -}; - -// Apply CSS to the Nipple instance. -Nipple.prototype.stylize = function () { - if (this.options.dataOnly) { - return this; - } - var animTime = this.options.fadeTime + 'ms'; - var borderStyle = u.getVendorStyle('borderRadius', '50%'); - var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime); - var styles = {}; - styles.el = { - position: 'absolute', - opacity: this.options.restOpacity, - display: 'block', - 'zIndex': 999 - }; - - styles.back = { - position: 'absolute', - display: 'block', - width: this.options.size + 'px', - height: this.options.size + 'px', - marginLeft: -this.options.size / 2 + 'px', - marginTop: -this.options.size / 2 + 'px', - background: this.options.color, - 'opacity': '.5' - }; - - styles.front = { - width: this.options.size / 2 + 'px', - height: this.options.size / 2 + 'px', - position: 'absolute', - display: 'block', - marginLeft: -this.options.size / 4 + 'px', - marginTop: -this.options.size / 4 + 'px', - background: this.options.color, - 'opacity': '.5' - }; - - u.extend(styles.el, transitStyle); - u.extend(styles.back, borderStyle); - u.extend(styles.front, borderStyle); - - this.applyStyles(styles); - - return this; -}; - -Nipple.prototype.applyStyles = function (styles) { - // Apply styles - for (var i in this.ui) { - if (this.ui.hasOwnProperty(i)) { - for (var j in styles[i]) { - this.ui[i].style[j] = styles[i][j]; - } - } - } - - return this; -}; - -// Inject the Nipple instance into DOM. -Nipple.prototype.addToDom = function () { - // We're not adding it if we're dataOnly or already in dom. - if (this.options.dataOnly || document.body.contains(this.ui.el)) { - return this; - } - this.options.zone.appendChild(this.ui.el); - return this; -}; - -// Remove the Nipple instance from DOM. -Nipple.prototype.removeFromDom = function () { - if (this.options.dataOnly || !document.body.contains(this.ui.el)) { - return this; - } - this.options.zone.removeChild(this.ui.el); - return this; -}; - -// Entirely destroy this nipple -Nipple.prototype.destroy = function () { - clearTimeout(this.removeTimeout); - clearTimeout(this.showTimeout); - clearTimeout(this.restTimeout); - this.trigger('destroyed', this.instance); - this.removeFromDom(); - this.off(); -}; - -// Fade in the Nipple instance. -Nipple.prototype.show = function (cb) { - var self = this; - - if (self.options.dataOnly) { - return self; - } - - clearTimeout(self.removeTimeout); - clearTimeout(self.showTimeout); - clearTimeout(self.restTimeout); - - self.addToDom(); - - self.restCallback(); - - setTimeout(function () { - self.ui.el.style.opacity = 1; - }, 0); - - self.showTimeout = setTimeout(function () { - self.trigger('shown', self.instance); - if (typeof cb === 'function') { - cb.call(this); - } - }, self.options.fadeTime); - - return self; -}; - -// Fade out the Nipple instance. -Nipple.prototype.hide = function (cb) { - var self = this; - - if (self.options.dataOnly) { - return self; - } - - self.ui.el.style.opacity = self.options.restOpacity; - - clearTimeout(self.removeTimeout); - clearTimeout(self.showTimeout); - clearTimeout(self.restTimeout); - - self.removeTimeout = setTimeout( - function () { - var display = self.options.mode === 'dynamic' ? 'none' : 'block'; - self.ui.el.style.display = display; - if (typeof cb === 'function') { - cb.call(self); - } - - self.trigger('hidden', self.instance); - }, - self.options.fadeTime - ); - if (self.options.restJoystick) { - self.restPosition(); - } - - return self; -}; - -Nipple.prototype.restPosition = function (cb) { - var self = this; - self.frontPosition = { - x: 0, - y: 0 - }; - var animTime = self.options.fadeTime + 'ms'; - - var transitStyle = {}; - transitStyle.front = u.getTransitionStyle('transition', - ['top', 'left'], animTime); - - var styles = {front: {}}; - styles.front = { - left: self.frontPosition.x + 'px', - top: self.frontPosition.y + 'px' - }; - - self.applyStyles(transitStyle); - self.applyStyles(styles); - - self.restTimeout = setTimeout( - function () { - if (typeof cb === 'function') { - cb.call(self); - } - self.restCallback(); - }, - self.options.fadeTime - ); -}; - -Nipple.prototype.restCallback = function () { - var self = this; - var transitStyle = {}; - transitStyle.front = u.getTransitionStyle('transition', 'none', ''); - self.applyStyles(transitStyle); - self.trigger('rested', self.instance); -}; - -Nipple.prototype.resetDirection = function () { - // Fully rebuild the object to let the iteration possible. - this.direction = { - x: false, - y: false, - angle: false - }; -}; - -Nipple.prototype.computeDirection = function (obj) { - var rAngle = obj.angle.radian; - var angle45 = Math.PI / 4; - var angle90 = Math.PI / 2; - var direction, directionX, directionY; - - // Angular direction - // \ UP / - // \ / - // LEFT RIGHT - // / \ - // /DOWN \ - // - if ( - rAngle > angle45 && - rAngle < (angle45 * 3) && - !obj.lockX - ) { - direction = 'up'; - } else if ( - rAngle > -angle45 && - rAngle <= angle45 && - !obj.lockY - ) { - direction = 'left'; - } else if ( - rAngle > (-angle45 * 3) && - rAngle <= -angle45 && - !obj.lockX - ) { - direction = 'down'; - } else if (!obj.lockY) { - direction = 'right'; - } - - // Plain direction - // UP | - // _______ | RIGHT - // LEFT | - // DOWN | - if (!obj.lockY) { - if (rAngle > -angle90 && rAngle < angle90) { - directionX = 'left'; - } else { - directionX = 'right'; - } - } - - if (!obj.lockX) { - if (rAngle > 0) { - directionY = 'up'; - } else { - directionY = 'down'; - } - } - - if (obj.force > this.options.threshold) { - var oldDirection = {}; - for (var i in this.direction) { - if (this.direction.hasOwnProperty(i)) { - oldDirection[i] = this.direction[i]; - } - } - - var same = {}; - - this.direction = { - x: directionX, - y: directionY, - angle: direction - }; - - obj.direction = this.direction; - - for (var i in oldDirection) { - if (oldDirection[i] === this.direction[i]) { - same[i] = true; - } - } - - // If all 3 directions are the same, we don't trigger anything. - if (same.x && same.y && same.angle) { - return obj; - } - - if (!same.x || !same.y) { - this.trigger('plain', obj); - } - - if (!same.x) { - this.trigger('plain:' + directionX, obj); - } - - if (!same.y) { - this.trigger('plain:' + directionY, obj); - } - - if (!same.angle) { - this.trigger('dir dir:' + direction, obj); - } - } - return obj; -}; - -/* global Nipple, Super */ - -/////////////////////////// -/// THE COLLECTION /// -/////////////////////////// - -function Collection (manager, options) { - var self = this; - self.nipples = []; - self.idles = []; - self.actives = []; - self.ids = []; - self.pressureIntervals = {}; - self.manager = manager; - self.id = Collection.id; - Collection.id += 1; - - // Defaults - self.defaults = { - zone: document.body, - multitouch: false, - maxNumberOfNipples: 10, - mode: 'dynamic', - position: {top: 0, left: 0}, - catchDistance: 200, - size: 100, - threshold: 0.1, - color: 'white', - fadeTime: 250, - dataOnly: false, - restJoystick: true, - restOpacity: 0.5, - lockX: false, - lockY: false - }; - - self.config(options); - - // Overwrites - if (self.options.mode === 'static' || self.options.mode === 'semi') { - self.options.multitouch = false; - } - - if (!self.options.multitouch) { - self.options.maxNumberOfNipples = 1; - } - - self.updateBox(); - self.prepareNipples(); - self.bindings(); - self.begin(); - - return self.nipples; -} - -Collection.prototype = new Super(); -Collection.constructor = Collection; -Collection.id = 0; - -Collection.prototype.prepareNipples = function () { - var self = this; - var nips = self.nipples; - - // Public API Preparation. - nips.on = self.on.bind(self); - nips.off = self.off.bind(self); - nips.options = self.options; - nips.destroy = self.destroy.bind(self); - nips.ids = self.ids; - nips.id = self.id; - nips.processOnMove = self.processOnMove.bind(self); - nips.processOnEnd = self.processOnEnd.bind(self); - nips.get = function (id) { - if (id === undefined) { - return nips[0]; - } - for (var i = 0, max = nips.length; i < max; i += 1) { - if (nips[i].identifier === id) { - return nips[i]; - } - } - return false; - }; -}; - -Collection.prototype.bindings = function () { - var self = this; - // Touch start event. - self.bindEvt(self.options.zone, 'start'); - // Avoid native touch actions (scroll, zoom etc...) on the zone. - self.options.zone.style.touchAction = 'none'; - self.options.zone.style.msTouchAction = 'none'; -}; - -Collection.prototype.begin = function () { - var self = this; - var opts = self.options; - - // We place our static nipple - // if needed. - if (opts.mode === 'static') { - var nipple = self.createNipple( - opts.position, - self.manager.getIdentifier() - ); - // Add it to the dom. - nipple.add(); - // Store it in idles. - self.idles.push(nipple); - } -}; - -// Nipple Factory -Collection.prototype.createNipple = function (position, identifier) { - var self = this; - var scroll = u.getScroll(); - var toPutOn = {}; - var opts = self.options; - - if (position.x && position.y) { - toPutOn = { - x: position.x - - (scroll.x + self.box.left), - y: position.y - - (scroll.y + self.box.top) - }; - } else if ( - position.top || - position.right || - position.bottom || - position.left - ) { - - // We need to compute the position X / Y of the joystick. - var dumb = document.createElement('DIV'); - dumb.style.display = 'hidden'; - dumb.style.top = position.top; - dumb.style.right = position.right; - dumb.style.bottom = position.bottom; - dumb.style.left = position.left; - dumb.style.position = 'absolute'; - - opts.zone.appendChild(dumb); - var dumbBox = dumb.getBoundingClientRect(); - opts.zone.removeChild(dumb); - - toPutOn = position; - position = { - x: dumbBox.left + scroll.x, - y: dumbBox.top + scroll.y - }; - } - - var nipple = new Nipple(self, { - color: opts.color, - size: opts.size, - threshold: opts.threshold, - fadeTime: opts.fadeTime, - dataOnly: opts.dataOnly, - restJoystick: opts.restJoystick, - restOpacity: opts.restOpacity, - mode: opts.mode, - identifier: identifier, - position: position, - zone: opts.zone, - frontPosition: { - x: 0, - y: 0 - } - }); - - if (!opts.dataOnly) { - u.applyPosition(nipple.ui.el, toPutOn); - u.applyPosition(nipple.ui.front, nipple.frontPosition); - } - self.nipples.push(nipple); - self.trigger('added ' + nipple.identifier + ':added', nipple); - self.manager.trigger('added ' + nipple.identifier + ':added', nipple); - - self.bindNipple(nipple); - - return nipple; -}; - -Collection.prototype.updateBox = function () { - var self = this; - self.box = self.options.zone.getBoundingClientRect(); -}; - -Collection.prototype.bindNipple = function (nipple) { - var self = this; - var type; - // Bubble up identified events. - var handler = function (evt, data) { - // Identify the event type with the nipple's id. - type = evt.type + ' ' + data.id + ':' + evt.type; - self.trigger(type, data); - }; - - // When it gets destroyed. - nipple.on('destroyed', self.onDestroyed.bind(self)); - - // Other events that will get bubbled up. - nipple.on('shown hidden rested dir plain', handler); - nipple.on('dir:up dir:right dir:down dir:left', handler); - nipple.on('plain:up plain:right plain:down plain:left', handler); -}; - -Collection.prototype.pressureFn = function (touch, nipple, identifier) { - var self = this; - var previousPressure = 0; - clearInterval(self.pressureIntervals[identifier]); - // Create an interval that will read the pressure every 100ms - self.pressureIntervals[identifier] = setInterval(function () { - var pressure = touch.force || touch.pressure || - touch.webkitForce || 0; - if (pressure !== previousPressure) { - nipple.trigger('pressure', pressure); - self.trigger('pressure ' + - nipple.identifier + ':pressure', pressure); - previousPressure = pressure; - } - }.bind(self), 100); -}; - -Collection.prototype.onstart = function (evt) { - var self = this; - var opts = self.options; - evt = u.prepareEvent(evt); - - // Update the box position - self.updateBox(); - - var process = function (touch) { - // If we can create new nipples - // meaning we don't have more active nipples than we should. - if (self.actives.length < opts.maxNumberOfNipples) { - self.processOnStart(touch); - } - }; - - u.map(evt, process); - - // We ask upstream to bind the document - // on 'move' and 'end' - self.manager.bindDocument(); - return false; -}; - -Collection.prototype.processOnStart = function (evt) { - var self = this; - var opts = self.options; - var indexInIdles; - var identifier = self.manager.getIdentifier(evt); - var pressure = evt.force || evt.pressure || evt.webkitForce || 0; - var position = { - x: evt.pageX, - y: evt.pageY - }; - - var nipple = self.getOrCreate(identifier, position); - - // Update its touch identifier - if (nipple.identifier !== identifier) { - self.manager.removeIdentifier(nipple.identifier); - } - nipple.identifier = identifier; - - var process = function (nip) { - // Trigger the start. - nip.trigger('start', nip); - self.trigger('start ' + nip.id + ':start', nip); - - nip.show(); - if (pressure > 0) { - self.pressureFn(evt, nip, nip.identifier); - } - // Trigger the first move event. - self.processOnMove(evt); - }; - - // Transfer it from idles to actives. - if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) { - self.idles.splice(indexInIdles, 1); - } - - // Store the nipple in the actives array - self.actives.push(nipple); - self.ids.push(nipple.identifier); - - if (opts.mode !== 'semi') { - process(nipple); - } else { - // In semi we check the distance of the touch - // to decide if we have to reset the nipple - var distance = u.distance(position, nipple.position); - if (distance <= opts.catchDistance) { - process(nipple); - } else { - nipple.destroy(); - self.processOnStart(evt); - return; - } - } - - return nipple; -}; - -Collection.prototype.getOrCreate = function (identifier, position) { - var self = this; - var opts = self.options; - var nipple; - - // If we're in static or semi, we might already have an active. - if (/(semi|static)/.test(opts.mode)) { - // Get the active one. - // TODO: Multi-touche for semi and static will start here. - // Return the nearest one. - nipple = self.idles[0]; - if (nipple) { - self.idles.splice(0, 1); - return nipple; - } - - if (opts.mode === 'semi') { - // If we're in semi mode, we need to create one. - return self.createNipple(position, identifier); - } - - console.warn('Coudln\'t find the needed nipple.'); - return false; - } - // In dynamic, we create a new one. - nipple = self.createNipple(position, identifier); - return nipple; -}; - -Collection.prototype.processOnMove = function (evt) { - var self = this; - var opts = self.options; - var identifier = self.manager.getIdentifier(evt); - var nipple = self.nipples.get(identifier); - - if (!nipple) { - // This is here just for safety. - // It shouldn't happen. - console.error('Found zombie joystick with ID ' + identifier); - self.manager.removeIdentifier(identifier); - return; - } - - nipple.identifier = identifier; - - var size = nipple.options.size / 2; - var pos = { - x: evt.pageX, - y: evt.pageY - }; - - var dist = u.distance(pos, nipple.position); - var angle = u.angle(pos, nipple.position); - var rAngle = u.radians(angle); - var force = dist / size; - - // If distance is bigger than nipple's size - // we clamp the position. - if (dist > size) { - dist = size; - pos = u.findCoord(nipple.position, dist, angle); - } - - var xPosition = pos.x - nipple.position.x - var yPosition = pos.y - nipple.position.y - - if (opts.lockX){ - yPosition = 0 - } - if (opts.lockY) { - xPosition = 0 - } - - nipple.frontPosition = { - x: xPosition, - y: yPosition - }; - - if (!opts.dataOnly) { - u.applyPosition(nipple.ui.front, nipple.frontPosition); - } - - // Prepare event's datas. - var toSend = { - identifier: nipple.identifier, - position: pos, - force: force, - pressure: evt.force || evt.pressure || evt.webkitForce || 0, - distance: dist, - angle: { - radian: rAngle, - degree: angle - }, - instance: nipple, - lockX: opts.lockX, - lockY: opts.lockY - }; - - // Compute the direction's datas. - toSend = nipple.computeDirection(toSend); - - // Offset angles to follow units circle. - toSend.angle = { - radian: u.radians(180 - angle), - degree: 180 - angle - }; - - // Send everything to everyone. - nipple.trigger('move', toSend); - self.trigger('move ' + nipple.id + ':move', toSend); -}; - -Collection.prototype.processOnEnd = function (evt) { - var self = this; - var opts = self.options; - var identifier = self.manager.getIdentifier(evt); - var nipple = self.nipples.get(identifier); - var removedIdentifier = self.manager.removeIdentifier(nipple.identifier); - - if (!nipple) { - return; - } - - if (!opts.dataOnly) { - nipple.hide(function () { - if (opts.mode === 'dynamic') { - nipple.trigger('removed', nipple); - self.trigger('removed ' + nipple.id + ':removed', nipple); - self.manager - .trigger('removed ' + nipple.id + ':removed', nipple); - nipple.destroy(); - } - }); - } - - // Clear the pressure interval reader - clearInterval(self.pressureIntervals[nipple.identifier]); - - // Reset the direciton of the nipple, to be able to trigger a new direction - // on start. - nipple.resetDirection(); - - nipple.trigger('end', nipple); - self.trigger('end ' + nipple.id + ':end', nipple); - - // Remove identifier from our bank. - if (self.ids.indexOf(nipple.identifier) >= 0) { - self.ids.splice(self.ids.indexOf(nipple.identifier), 1); - } - - // Clean our actives array. - if (self.actives.indexOf(nipple) >= 0) { - self.actives.splice(self.actives.indexOf(nipple), 1); - } - - if (/(semi|static)/.test(opts.mode)) { - // Transfer nipple from actives to idles - // if we're in semi or static mode. - self.idles.push(nipple); - } else if (self.nipples.indexOf(nipple) >= 0) { - // Only if we're not in semi or static mode - // we can remove the instance. - self.nipples.splice(self.nipples.indexOf(nipple), 1); - } - - // We unbind move and end. - self.manager.unbindDocument(); - - // We add back the identifier of the idle nipple; - if (/(semi|static)/.test(opts.mode)) { - self.manager.ids[removedIdentifier.id] = removedIdentifier.identifier; - } -}; - -// Remove destroyed nipple from the lists -Collection.prototype.onDestroyed = function(evt, nipple) { - var self = this; - if (self.nipples.indexOf(nipple) >= 0) { - self.nipples.splice(self.nipples.indexOf(nipple), 1); - } - if (self.actives.indexOf(nipple) >= 0) { - self.actives.splice(self.actives.indexOf(nipple), 1); - } - if (self.idles.indexOf(nipple) >= 0) { - self.idles.splice(self.idles.indexOf(nipple), 1); - } - if (self.ids.indexOf(nipple.identifier) >= 0) { - self.ids.splice(self.ids.indexOf(nipple.identifier), 1); - } - - // Remove the identifier from our bank - self.manager.removeIdentifier(nipple.identifier); - - // We unbind move and end. - self.manager.unbindDocument(); -}; - -// Cleanly destroy the manager -Collection.prototype.destroy = function () { - var self = this; - self.unbindEvt(self.options.zone, 'start'); - - // Destroy nipples. - self.nipples.forEach(function(nipple) { - nipple.destroy(); - }); - - // Clean 3DTouch intervals. - for (var i in self.pressureIntervals) { - if (self.pressureIntervals.hasOwnProperty(i)) { - clearInterval(self.pressureIntervals[i]); - } - } - - // Notify the manager passing the instance - self.trigger('destroyed', self.nipples); - // We unbind move and end. - self.manager.unbindDocument(); - // Unbind everything. - self.off(); -}; - -/* global u, Super, Collection */ - -/////////////////////// -/// MANAGER /// -/////////////////////// - -function Manager (options) { - var self = this; - self.ids = {}; - self.index = 0; - self.collections = []; - - self.config(options); - self.prepareCollections(); - - // Listen for resize, to reposition every joysticks - var resizeTimer; - u.bindEvt(window, 'resize', function (evt) { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(function () { - var pos; - var scroll = u.getScroll(); - self.collections.forEach(function (collection) { - collection.forEach(function (nipple) { - pos = nipple.el.getBoundingClientRect(); - nipple.position = { - x: scroll.x + pos.left, - y: scroll.y + pos.top - }; - }); - }); - }, 100); - }); - - return self.collections; -}; - -Manager.prototype = new Super(); -Manager.constructor = Manager; - -Manager.prototype.prepareCollections = function () { - var self = this; - // Public API Preparation. - self.collections.create = self.create.bind(self); - // Listen to anything - self.collections.on = self.on.bind(self); - // Unbind general events - self.collections.off = self.off.bind(self); - // Destroy everything - self.collections.destroy = self.destroy.bind(self); - // Get any nipple - self.collections.get = function (id) { - var nipple; - self.collections.every(function (collection) { - if (nipple = collection.get(id)) { - return false; - } - return true; - }); - return nipple; - }; -}; - -Manager.prototype.create = function (options) { - return this.createCollection(options); -}; - -// Collection Factory -Manager.prototype.createCollection = function (options) { - var self = this; - var collection = new Collection(self, options); - - self.bindCollection(collection); - self.collections.push(collection); - - return collection; -}; - -Manager.prototype.bindCollection = function (collection) { - var self = this; - var type; - // Bubble up identified events. - var handler = function (evt, data) { - // Identify the event type with the nipple's identifier. - type = evt.type + ' ' + data.id + ':' + evt.type; - self.trigger(type, data); - }; - - // When it gets destroyed we clean. - collection.on('destroyed', self.onDestroyed.bind(self)); - - // Other events that will get bubbled up. - collection.on('shown hidden rested dir plain', handler); - collection.on('dir:up dir:right dir:down dir:left', handler); - collection.on('plain:up plain:right plain:down plain:left', handler); -}; - -Manager.prototype.bindDocument = function () { - var self = this; - // Bind only if not already binded - if (!self.binded) { - self.bindEvt(document, 'move') - .bindEvt(document, 'end'); - self.binded = true; - } -}; - -Manager.prototype.unbindDocument = function (force) { - var self = this; - // If there are no touch left - // unbind the document. - if (!Object.keys(self.ids).length || force === true) { - self.unbindEvt(document, 'move') - .unbindEvt(document, 'end'); - self.binded = false; - } -}; - -Manager.prototype.getIdentifier = function (evt) { - var id; - // If no event, simple increment - if (!evt) { - id = this.index; - } else { - // Extract identifier from event object. - // Unavailable in mouse events so replaced by latest increment. - id = evt.identifier === undefined ? evt.pointerId : evt.identifier; - if (id === undefined) { - id = this.latest || 0; - } - } - - if (this.ids[id] === undefined) { - this.ids[id] = this.index; - this.index += 1; - } - - // Keep the latest id used in case we're using an unidentified mouseEvent - this.latest = id; - return this.ids[id]; -}; - -Manager.prototype.removeIdentifier = function (identifier) { - var removed = {}; - for (var id in this.ids) { - if (this.ids[id] === identifier) { - removed.id = id; - removed.identifier = this.ids[id]; - delete this.ids[id]; - break; - } - } - return removed; -}; - -Manager.prototype.onmove = function (evt) { - var self = this; - self.onAny('move', evt); - return false; -}; - -Manager.prototype.onend = function (evt) { - var self = this; - self.onAny('end', evt); - return false; -}; - -Manager.prototype.oncancel = function (evt) { - var self = this; - self.onAny('end', evt); - return false; -}; - -Manager.prototype.onAny = function (which, evt) { - var self = this; - var id; - var processFn = 'processOn' + which.charAt(0).toUpperCase() + - which.slice(1); - evt = u.prepareEvent(evt); - var processColl = function (e, id, coll) { - if (coll.ids.indexOf(id) >= 0) { - coll[processFn](e); - // Mark the event to avoid cleaning it later. - e._found_ = true; - } - }; - var processEvt = function (e) { - id = self.getIdentifier(e); - u.map(self.collections, processColl.bind(null, e, id)); - // If the event isn't handled by any collection, - // we need to clean its identifier. - if (!e._found_) { - self.removeIdentifier(id); - } - }; - - u.map(evt, processEvt); - - return false; -}; - -// Cleanly destroy the manager -Manager.prototype.destroy = function () { - var self = this; - self.unbindDocument(true); - self.ids = {}; - self.index = 0; - self.collections.forEach(function(collection) { - collection.destroy(); - }); - self.off(); -}; - -// When a collection gets destroyed -// we clean behind. -Manager.prototype.onDestroyed = function (evt, coll) { - var self = this; - if (self.collections.indexOf(coll) < 0) { - return false; - } - self.collections.splice(self.collections.indexOf(coll), 1); -}; - -var factory = new Manager(); -return { - create: function (options) { - return factory.create(options); - }, - factory: factory -}; - -}); diff --git a/webserver__handler_key_mouse_click_and_move/templates/index.html b/webserver__handler_key_mouse_click_and_move/templates/index.html deleted file mode 100644 index 09093fb0b..000000000 --- a/webserver__handler_key_mouse_click_and_move/templates/index.html +++ /dev/null @@ -1,540 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - - - diff --git a/websocket__examples/echo_client__using_websockets.py b/websocket__examples/echo_client__using_websockets.py index 047dc4561..8d6161f3a 100644 --- a/websocket__examples/echo_client__using_websockets.py +++ b/websocket__examples/echo_client__using_websockets.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/aaugustin/websockets @@ -13,7 +13,7 @@ import websockets -async def hello(url: str): +async def hello(url: str) -> None: async with websockets.connect(url) as websocket: await websocket.send("Hello World!") @@ -21,10 +21,8 @@ async def hello(url: str): # Hello World! -if __name__ == '__main__': +if __name__ == "__main__": # From http://websocket.org/echo.html - url = 'wss://echo.websocket.org' + url = "wss://echo.websocket.org" - asyncio.get_event_loop().run_until_complete( - hello(url) - ) + asyncio.get_event_loop().run_until_complete(hello(url)) diff --git a/websocket__examples/using__websocket-client/echo_client__long_lived_connection.py b/websocket__examples/using__websocket-client/echo_client__long_lived_connection.py index 5269c3558..206b75ab3 100644 --- a/websocket__examples/using__websocket-client/echo_client__long_lived_connection.py +++ b/websocket__examples/using__websocket-client/echo_client__long_lived_connection.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/websocket-client/websocket-client#long-lived-connection @@ -11,17 +11,17 @@ import thread except ImportError: import _thread as thread -import time +import time # pip install websocket-client import websocket -def on_open(ws): - print(f'[on_open]') +def on_open(ws: websocket.WebSocketApp) -> None: + print(f"[on_open]") - def run(*args): + def run(*args) -> None: for i in range(3): time.sleep(1) ws.send(f"Hello {i}") @@ -34,21 +34,25 @@ def run(*args): thread.start_new_thread(run, ()) -def on_message(ws, message): - print(f'[on_message] {message}') +def on_message(ws: websocket.WebSocketApp, message: str) -> None: + print(f"[on_message] {message}") -def on_error(ws, error): - print(f'[on_error] {error}') +def on_error(ws: websocket.WebSocketApp, error: Exception) -> None: + print(f"[on_error] {error}") -def on_close(ws): - print(f'[on_close]') +def on_close( + ws: websocket.WebSocketApp, + close_status_code: int | None, + close_msg: str | None, +) -> None: + print(f"[on_close] close_status_code={close_status_code} close_msg={close_msg}") if __name__ == "__main__": # From http://websocket.org/echo.html - url = 'wss://echo.websocket.org' + url = "wss://echo.websocket.org" websocket.enableTrace(True) ws = websocket.WebSocketApp( @@ -56,6 +60,6 @@ def on_close(ws): on_open=on_open, on_message=on_message, on_error=on_error, - on_close=on_close + on_close=on_close, ) ws.run_forever() diff --git a/websocket__examples/using__websocket-client/echo_client__short_lived_connection.py b/websocket__examples/using__websocket-client/echo_client__short_lived_connection.py index baa0ff18e..44bf7d15e 100644 --- a/websocket__examples/using__websocket-client/echo_client__short_lived_connection.py +++ b/websocket__examples/using__websocket-client/echo_client__short_lived_connection.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/websocket-client/websocket-client#short-lived-one-off-send-receive @@ -12,7 +12,7 @@ # From http://websocket.org/echo.html -url = 'wss://echo.websocket.org' +url = "wss://echo.websocket.org" ws = websocket.create_connection(url) diff --git a/websocket__examples/using__websocket-client/echo_enable_trace.py b/websocket__examples/using__websocket-client/echo_enable_trace.py index 09a1ee2ec..3a6be59c2 100644 --- a/websocket__examples/using__websocket-client/echo_enable_trace.py +++ b/websocket__examples/using__websocket-client/echo_enable_trace.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/websocket-client/websocket-client#short-lived-one-off-send-receive @@ -14,7 +14,7 @@ websocket.enableTrace(True) # From http://websocket.org/echo.html -url = 'wss://echo.websocket.org' +url = "wss://echo.websocket.org" ws = websocket.create_connection(url) diff --git a/whois__examples/hello_world.py b/whois__examples/hello_world.py new file mode 100644 index 000000000..6c4f3c443 --- /dev/null +++ b/whois__examples/hello_world.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install python-whois +import whois + + +w = whois.whois("ru.stackoverflow.com") +print(w) +""" +{ + "domain_name": [ + "STACKOVERFLOW.COM", + "stackoverflow.com" + ], + "registrar": "CSC CORPORATE DOMAINS, INC.", + "whois_server": "whois.corporatedomains.com", + "referral_url": null, + "updated_date": [ + "2022-08-17 04:32:10", + "2022-08-17 00:32:11" + ], + "creation_date": [ + "2003-12-26 19:18:07", + "2003-12-26 14:18:07" + ], + "expiration_date": "2024-02-02 11:59:59", + "name_servers": [ + "NS-1033.AWSDNS-01.ORG", + "NS-358.AWSDNS-44.COM", + "NS-CLOUD-E1.GOOGLEDOMAINS.COM", + "NS-CLOUD-E2.GOOGLEDOMAINS.COM", + "ns-1033.awsdns-01.org", + "ns-358.awsdns-44.com", + "ns-cloud-e2.googledomains.com", + "ns-cloud-e1.googledomains.com" + ], + "status": [ + "clientTransferProhibited https://icann.org/epp#clientTransferProhibited", + "clientTransferProhibited http://www.icann.org/epp#clientTransferProhibited" + ], + "emails": [ + "domainabuse@cscglobal.com", + "sysadmin-team@stackoverflow.com" + ], + "dnssec": "unsigned", + "name": "Domain Administrator", + "org": "Stack Exchange, Inc.", + "address": "110 William Street, 28th Floor", + "city": "New York", + "state": "NY", + "registrant_postal_code": "10038", + "country": "US" +} +""" diff --git a/wikipedia__examples/hello_world.py b/wikipedia__examples/hello_world.py index 0a51abe43..3fdc46a40 100644 --- a/wikipedia__examples/hello_world.py +++ b/wikipedia__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://pypi.org/project/wikipedia/ @@ -23,11 +23,13 @@ print() ny = wikipedia.page("New York City") -print(ny) # -print(ny.title) # New York City -print(ny.url) # https://en.wikipedia.org/wiki/New_York_City -print(repr(ny.content)) # 'The City of New York, usually referred to as either New York City (NYC) or simply New York (NY), is the most populous city in the United States. With an estimated 2018 population of 8,398,748 distributed over a land area of about 302.6 square miles (784 km2), New York is also the most densely populated major city in the United States. Located at the southern tip of the state of New York, the city is the center of the New York metropolitan area, the largest metropolitan area in the world by urban landmass and one of the world\'s most populous megacities, with an estimated 19,979,477 people in its 2018 Metropolitan Statistical Area and 22,679,948 residents in its Combined Statistical Area. A global power city, New York City has been described as the cultural, financial, and media capital of the world, and exerts a significant impact upon commerce, entertainment, research, technology, education, politics, tourism, art, fashion, and sports. The city\'s fast pace has inspired the term New York minute. Home to the headquarters of the United Nations, New York is an important center for international diplomacy.Situated on one of the world\'s largest natural harbors, New York City consists of five boroughs, each of which is a separate county of the State of New York. The five boroughs – Brooklyn, Queens, Manhattan, The Bronx, and Staten Island – were consolidated into a single city in 1898. The city and its metropolitan area constitute the premier gateway for legal immigration to the United States. As many as 800 languages are spoken in New York, making it the most linguistically diverse city in the world. New York City is home to more than 3.2 million residents born outside the United States, the largest foreign-born population of any city in the world. As of 2019, the New York metropolitan area is estimated to produce a gross metropolitan product (GMP) of US$1.9 trillion. If greater New York City were a sovereign state, it would have the 12th highest GDP in the world. New York is home to the highest number of billionaires of any city in the world.New York City traces its origins to a trading post founded by colonists from the Dutch Republic in 1624 on Lower Manhattan; the post was named New Amsterdam in 1626. The city and its surroundings came under English control in 1664 and were renamed New York after King Charles II of England granted the lands to his brother, the Duke of York. New York was the capital of the United States from 1785 until 1790, and has been the largest US city since 1790. The Statue of Liberty greeted millions of immigrants as they came to the U.S. by ship in the late 19th and early 20th centuries and is an international symbol of the U.S. and its ideals of liberty and peace. In the 21st century, New York has emerged as a global node of creativity and entrepreneurship, social tolerance, and environmental sustainability, and as a symbol of freedom and cultural diversity. In 2019, New York was voted the greatest city in the world per a survey of over 30,000 people from 48 cities worldwide, citing its cultural diversity.Many districts and landmarks in New York City are well known, including three of the world\'s ten most visited tourist attractions in 2013; a record 62.8 million tourists visited in 2017. Several sources have ranked New York the most photographed city in the world. Times Square, iconic as the world\'s "heart" and "crossroads", is the brightly illuminated hub of the Broadway Theater District, one of the world\'s busiest pedestrian intersections, and a major center of the world\'s entertainment industry. The names of many of the city\'s landmarks, skyscrapers, and parks are known internationally. Manhattan\'s real estate market is among the most expensive in the world. New York is home to the largest ethnic Chinese population outside of Asia, with multiple distinct Chinatowns across the city. Providing continuous 24/7 service, the New York City Subway is the largest single-operator rapid transit system worldwide, with 472 rail stations. The city has over 120 colleges and universities, including Columbia University, New York University, and Rockefeller University, ranked among the top universities in the world. Anchored by Wall Street in the Financial District of Lower Manhattan, New York has been called both the most economically powerful city and world\'s leading financial center, and is home to the world\'s two largest stock exchanges by total market capitalization, the New York Stock Exchange and NASDAQ.\n\n\n== History ==\n\n\n=== Etymology ===\nIn 1664, the city was named in honor of the Duke of York, who would become King James II of England. James\'s older brother, King Charles II, had appointed the Duke proprietor of the former territory of New Netherland, including the city of New Amsterdam, which England had recently seized from the Dutch.\n\n\n=== Early history ===\nDuring the Wisconsin glaciation, 75,000 to 11,000 years ago, the New York City region was situated at the edge of a large ice sheet over 2,000 feet (610 m) in depth. The erosive forward movement of the ice (and its subsequent retreat) contributed to the separation of what is now Long Island and Staten Island. That action also left bedrock at a relatively shallow depth, providing a solid foundation for most of Manhattan\'s skyscrapers.In the precolonial era, the area of present-day New York City was inhabited by Algonquian Native Americans, including the Lenape. Their homeland, known as Lenapehoking, included Staten Island, Manhattan, the Bronx, the western portion of Long Island (including the areas that would later become the boroughs of Brooklyn and Queens), and the Lower Hudson Valley.The first documented visit into New York Harbor by a European was in 1524 by Giovanni da Verrazzano, a Florentine explorer in the service of the French crown. He claimed the area for France and named it Nouvelle Angoulême (New Angoulême). A Spanish expedition, led by the Portuguese captain Estêvão Gomes sailing for Emperor Charles V, arrived in New York Harbor in January 1525 and charted the mouth of the Hudson River, which he named Río de San Antonio (Saint Anthony\'s River). The Padrón Real of 1527, the first scientific map to show the East Coast of North America continuously, was informed by Gomes\' expedition and labeled the northeastern United States as Tierra de Esteban Gómez in his honor.\n\nIn 1609, the English explorer Henry Hudson rediscovered the New York Harbor while searching for the Northwest Passage to the Orient for the Dutch East India Company. He proceeded to sail up what the Dutch would name the North River (now the Hudson River), named first by Hudson as the Mauritius after Maurice, Prince of Orange. Hudson\'s first mate described the harbor as "a very good Harbour for all windes" and the river as "a mile broad" and "full of fish." Hudson sailed roughly 150 miles (240 km) north, past the site of the present-day New York State capital city of Albany, in the belief that it might be an oceanic tributary before the river became too shallow to continue. He made a ten-day exploration of the area and claimed the region for the Dutch East India Company. In 1614, the area between Cape Cod and Delaware Bay was claimed by the Netherlands and called Nieuw-Nederland (New Netherland).\nThe first non-Native American inhabitant of what would eventually become New York City was Juan Rodriguez (transliterated to Dutch as Jan Rodrigues), a merchant from Santo Domingo. Born in Santo Domingo of Portuguese and African descent, he arrived in Manhattan during the winter of 1613–14, trapping for pelts and trading with the local population as a representative of the Dutch. Broadway, from 159th Street to 218th Street in Upper Manhattan, is named Juan Rodriguez Way in his honor.\n\n\n=== Dutch rule ===\n\nA permanent European presence in New Netherland began in 1624 – making New York the 12th oldest continuously occupied European-established settlement in the continental United States – with the founding of a Dutch fur trading settlement on Governors Island. In 1625, construction was started on a citadel and Fort Amsterdam, later called Nieuw Amsterdam (New Amsterdam), on present-day Manhattan Island. The colony of New Amsterdam was centered at the site which would eventually become Lower Manhattan. It extended from the lower tip of Manhattan to modern day Wall Street,where a 12-foot wooden stockade was built in 1653 to protect against Native American and British Raids. In 1626, the Dutch colonial Director-General Peter Minuit, acting as charged by the Dutch West India Company, purchased the island of Manhattan from the Canarsie, a small Lenape band, for "the value of 60 guilders" (about $900 in 2018). A disproved legend claims that Manhattan was purchased for $24 worth of glass beads.Following the purchase, New Amsterdam grew slowly. To attract settlers, the Dutch instituted the patroon system in 1628, whereby wealthy Dutchmen (patroons, or patrons) who brought 50 colonists to New Netherland would be awarded swathes of land, along with local political autonomy and rights to participate in the lucrative fur trade. This program had little success.Since 1621, the Dutch West India Company had operated as a monopoly in New Netherland, on authority granted by the Dutch States General. In 1639–1640, in an effort to bolster economic growth, the Dutch West India Company relinquished its monopoly over the fur trade, leading to growth in the production and trade of food, timber, tobacco, and slaves (particularly with the Dutch West Indies).In 1647, Peter Stuyvesant began his tenure as the last Director-General of New Netherland. During his tenure, the population of New Netherland grew from 2,000 to 8,000. Stuyvesant has been credited with improving law and order in the colony; however, he also earned a reputation as a despotic leader. He instituted regulations on liquor sales, attempted to assert control over the Dutch Reformed Church, and blocked other religious groups (including Quakers, Jews, and Lutherans) from establishing houses of worship. The Dutch West India Company would eventually attempt to ease tensions between Stuyvesant and residents of New Amsterdam.\n\n\n=== English rule ===\n\nIn 1664, unable to summon any significant resistance, Stuyvesant surrendered New Amsterdam to English troops, led by Colonel Richard Nicolls, without bloodshed. The terms of the surrender permitted Dutch residents to remain in the colony and allowed for religious freedom. The English promptly renamed the fledgling city "New York" after the Duke of York (the future King James II of England). The transfer was confirmed in 1667 by the Treaty of Breda, which concluded the Second Anglo-Dutch War.On August 24, 1673, during the Third Anglo-Dutch War, Dutch captain Anthony Colve seized the colony of New York from England at the behest of Cornelis Evertsen the Youngest and rechristened it "New Orange" after William III, the Prince of Orange. The Dutch would soon return the island to England under the Treaty of Westminster of November 1674.Several intertribal wars among the Native Americans and some epidemics brought on by contact with the Europeans caused sizeable population losses for the Lenape between the years 1660 and 1670. By 1700, the Lenape population had diminished to 200. New York experienced several yellow fever epidemics in the 18th century, losing ten percent of its population to the disease in 1702 alone.New York grew in importance as a trading port while under British rule in the early 1700s. It also became a center of slavery, with 42% of households holding slaves by 1730, the highest percentage outside Charleston, South Carolina. Most slaveholders held a few or several domestic slaves, but others hired them out to work at labor. Slavery became integrally tied to New York\'s economy through the labor of slaves throughout the port, and the banks and shipping tied to the American South. Discovery of the African Burying Ground in the 1990s, during construction of a new federal courthouse near Foley Square, revealed that tens of thousands of Africans had been buried in the area in the colonial years.The 1735 trial and acquittal in Manhattan of John Peter Zenger, who had been accused of seditious libel after criticizing colonial governor William Cosby, helped to establish the freedom of the press in North America. In 1754, Columbia University was founded under charter by King George II as King\'s College in Lower Manhattan.\n\n\n=== American Revolution ===\n\nThe Stamp Act Congress met in New York in October 1765, as the Sons of Liberty, organized in the city, skirmished over the next ten years with British troops stationed there. The Battle of Long Island, the largest battle of the American Revolutionary War, was fought in August 1776 within the modern-day borough of Brooklyn. After the battle, in which the Americans were defeated, the British made the city their military and political base of operations in North America. The city was a haven for Loyalist refugees and escaped slaves who joined the British lines for freedom newly promised by the Crown for all fighters. As many as 10,000 escaped slaves crowded into the city during the British occupation. When the British forces evacuated at the close of the war in 1783, they transported 3,000 freedmen for resettlement in Nova Scotia. They resettled other freedmen in England and the Caribbean.\nThe only attempt at a peaceful solution to the war took place at the Conference House on Staten Island between American delegates, including Benjamin Franklin, and British general Lord Howe on September 11, 1776. Shortly after the British occupation began, the Great Fire of New York occurred, a large conflagration on the West Side of Lower Manhattan, which destroyed about a quarter of the buildings in the city, including Trinity Church.In 1785, the assembly of the Congress of the Confederation made New York City the national capital shortly after the war. New York was the last capital of the U.S. under the Articles of Confederation and the first capital under the Constitution of the United States. In 1789, the first President of the United States, George Washington, was inaugurated; the first United States Congress and the Supreme Court of the United States each assembled for the first time, and the United States Bill of Rights was drafted, all at Federal Hall on Wall Street. By 1790, New York had surpassed Philadelphia to become the largest city in the United States, but by the end of that year, pursuant to the Residence Act, the national capital was moved to Philadelphia.\n\n\n=== Nineteenth century ===\n\nUnder New York State\'s gradual abolition act of 1799, children of slave mothers were to be eventually liberated but to be held in indentured servitude until their mid-to-late twenties. Together with slaves freed by their masters after the Revolutionary War and escaped slaves, a significant free-black population gradually developed in Manhattan. Under such influential United States founders as Alexander Hamilton and John Jay, the New York Manumission Society worked for abolition and established the African Free School to educate black children. It was not until 1827 that slavery was completely abolished in the state, and free blacks struggled afterward with discrimination. New York interracial abolitionist activism continued; among its leaders were graduates of the African Free School. The city\'s black population reached more than 16,000 in 1840.In the 19th century, the city was transformed by development relating to its status as a national and international trading center, as well as by European immigration. The city adopted the Commissioners\' Plan of 1811, which expanded the city street grid to encompass almost all of Manhattan. The 1825 completion of the Erie Canal through central New York connected the Atlantic port to the agricultural markets and commodities of the North American interior via the Hudson River and the Great Lakes. Local politics became dominated by Tammany Hall, a political machine supported by Irish and German immigrants.Several prominent American literary figures lived in New York during the 1830s and 1840s, including William Cullen Bryant, Washington Irving, Herman Melville, Rufus Wilmot Griswold, John Keese, Nathaniel Parker Willis, and Edgar Allan Poe. Public-minded members of the contemporaneous business elite lobbied for the establishment of Central Park, which in 1857 became the first landscaped park in an American city.\n\nThe Great Irish Famine brought a large influx of Irish immigrants, of whom over 200,000 were living in New York by 1860, upwards of a quarter of the city\'s population. There was also extensive immigration from the German provinces, where revolutions had disrupted societies, and Germans comprised another 25% of New York\'s population by 1860.Democratic Party candidates were consistently elected to local office, increasing the city\'s ties to the South and its dominant party. In 1861, Mayor Fernando Wood called upon the aldermen to declare independence from Albany and the United States after the South seceded, but his proposal was not acted on. Anger at new military conscription laws during the American Civil War (1861–1865), which spared wealthier men who could afford to pay a $300 (equivalent to $6,105 in 2018) commutation fee to hire a substitute, led to the Draft Riots of 1863, whose most visible participants were ethnic Irish working class.The situation deteriorated into attacks on New York\'s elite, followed by attacks on Black New Yorkers and their property after fierce competition for a decade between Irish immigrants and black people for work. Rioters burned the Colored Orphan Asylum to the ground, with more than 200 children escaping harm due to efforts of the New York Police Department, which was mainly made up of Irish immigrants. At least 120 people were killed. Eleven Black men were lynched over five days, and the riots forced hundreds of Blacks to flee the city for Williamsburg, Brooklyn, and New Jersey. The black population in Manhattan fell below 10,000 by 1865, which it had last been in 1820. The white working class had established dominance. Violence by longshoremen against Black men was especially fierce in the docks area. It was one of the worst incidents of civil unrest in American history.\n\n\n=== Modern history ===\n\nIn 1898, the modern City of New York was formed with the consolidation of Brooklyn (until then a separate city), the County of New York (which then included parts of the Bronx), the County of Richmond, and the western portion of the County of Queens. The opening of the subway in 1904, first built as separate private systems, helped bind the new city together. Throughout the first half of the 20th century, the city became a world center for industry, commerce, and communication.In 1904, the steamship General Slocum caught fire in the East River, killing 1,021 people on board. In 1911, the Triangle Shirtwaist Factory fire, the city\'s worst industrial disaster, took the lives of 146 garment workers and spurred the growth of the International Ladies\' Garment Workers\' Union and major improvements in factory safety standards.\n\nNew York\'s non-white population was 36,620 in 1890. New York City was a prime destination in the early twentieth century for African Americans during the Great Migration from the American South, and by 1916, New York City had become home to the largest urban African diaspora in North America. The Harlem Renaissance of literary and cultural life flourished during the era of Prohibition. The larger economic boom generated construction of skyscrapers competing in height and creating an identifiable skyline.\nNew York became the most populous urbanized area in the world in the early 1920s, overtaking London. The metropolitan area surpassed the 10 million mark in the early 1930s, becoming the first megacity in human history. The difficult years of the Great Depression saw the election of reformer Fiorello La Guardia as mayor and the fall of Tammany Hall after eighty years of political dominance.Returning World War II veterans created a post-war economic boom and the development of large housing tracts in eastern Queens and Nassau County as well as similar suburban areas in New Jersey. New York emerged from the war unscathed as the leading city of the world, with Wall Street leading America\'s place as the world\'s dominant economic power. The United Nations Headquarters was completed in 1952, solidifying New York\'s global geopolitical influence, and the rise of abstract expressionism in the city precipitated New York\'s displacement of Paris as the center of the art world.\n\nThe Stonewall riots were a series of spontaneous, violent demonstrations by members of the gay community against a police raid that took place in the early morning hours of June 28, 1969, at the Stonewall Inn in the Greenwich Village neighborhood of Lower Manhattan. They are widely considered to constitute the single most important event leading to the gay liberation movement and the modern fight for LGBT rights. Wayne R. Dynes, author of the Encyclopedia of Homosexuality, wrote that drag queens were the only "transgender folks around" during the June 1969 Stonewall riots. "None of them in fact made a major contribution to the movement." Others say the transgender community in New York City played a significant role in fighting for LGBT equality during the period of the Stonewall riots and thereafter.In the 1970s, job losses due to industrial restructuring caused New York City to suffer from economic problems and rising crime rates. While a resurgence in the financial industry greatly improved the city\'s economic health in the 1980s, New York\'s crime rate continued to increase through that decade and into the beginning of the 1990s. By the mid 1990s, crime rates started to drop dramatically due to revised police strategies, improving economic opportunities, gentrification, and new residents, both American transplants and new immigrants from Asia and Latin America. Important new sectors, such as Silicon Alley, emerged in the city\'s economy. New York\'s population reached all-time highs in the 2000 Census and then again in the 2010 Census.\n\nNew York suffered the bulk of the economic damage and largest loss of human life in the aftermath of the September 11, 2001 attacks. Two of the four airliners highjacked that day were flown into the twin towers of the World Trade Center, destroying them and killing 2,192 civilians, 343 firefighters, and 71 law enforcement officers. The North Tower became the tallest building ever to be destroyed anywhere then or subsequently.The rebuilding of the area has created a new One World Trade Center, and a 9/11 memorial and museum along with other new buildings and infrastructure. The World Trade Center PATH station, which had opened on July 19, 1909 as the Hudson Terminal, was also destroyed in the attack. A temporary station was built and opened on November 23, 2003. An 800,000-square-foot (74,000 m2) permanent rail station designed by Santiago Calatrava, the World Trade Center Transportation Hub, the city\'s third-largest hub, was completed in 2016. The new One World Trade Center is the tallest skyscraper in the Western Hemisphere and the sixth-tallest building in the world by pinnacle height, with its spire reaching a symbolic 1,776 feet (541.3 m) in reference to the year of U.S. independence.The Occupy Wall Street protests in Zuccotti Park in the Financial District of Lower Manhattan began on September 17, 2011, receiving global attention and popularizing the Occupy movement against social and economic inequality worldwide.\n\n\n== Geography ==\n\nNew York City is situated in the Northeastern United States, in southeastern New York State, approximately halfway between Washington, D.C. and Boston. The location at the mouth of the Hudson River, which feeds into a naturally sheltered harbor and then into the Atlantic Ocean, has helped the city grow in significance as a trading port. Most of New York City is built on the three islands of Long Island, Manhattan, and Staten Island.\nThe Hudson River flows through the Hudson Valley into New York Bay. Between New York City and Troy, New York, the river is an estuary. The Hudson River separates the city from the U.S. state of New Jersey. The East River—a tidal strait—flows from Long Island Sound and separates the Bronx and Manhattan from Long Island. The Harlem River, another tidal strait between the East and Hudson Rivers, separates most of Manhattan from the Bronx. The Bronx River, which flows through the Bronx and Westchester County, is the only entirely fresh water river in the city.The city\'s land has been altered substantially by human intervention, with considerable land reclamation along the waterfronts since Dutch colonial times; reclamation is most prominent in Lower Manhattan, with developments such as Battery Park City in the 1970s and 1980s. Some of the natural relief in topography has been evened out, especially in Manhattan.The city\'s total area is 468.484 square miles (1,213.37 km2), including 302.643 sq mi (783.84 km2) of land and 165.841 sq mi (429.53 km2) of this is water.\nThe highest point in the city is Todt Hill on Staten Island, which, at 409.8 feet (124.9 m) above sea level, is the highest point on the Eastern Seaboard south of Maine. The summit of the ridge is mostly covered in woodlands as part of the Staten Island Greenbelt.\n\n\n=== Boroughs ===\n\n \n\nNew York City is often referred to collectively as the five boroughs, and in turn, there are hundreds of distinct neighborhoods throughout the boroughs, many with a definable history and character to call their own. If the boroughs were each independent cities, four of the boroughs (Brooklyn, Queens, Manhattan, and the Bronx) would be among the ten most populous cities in the United States (Staten Island would be ranked 37th) ; these same boroughs are coterminous with the four most densely populated counties in the United States (New York [Manhattan], Kings [Brooklyn], Bronx, and Queens).\n\nManhattan (New York County) is the geographically smallest and most densely populated borough, is home to Central Park and most of the city\'s skyscrapers, and may be locally known simply as The City. Manhattan\'s (New York County\'s) population density of 72,033 people per square mile (27,812/km²) in 2015 makes it the highest of any county in the United States and higher than the density of any individual American city. Manhattan is the cultural, administrative, and financial center of New York City and contains the headquarters of many major multinational corporations, the United Nations Headquarters, Wall Street, and a number of important universities. Manhattan is often described as the financial and cultural center of the world.Most of the borough is situated on Manhattan Island, at the mouth of the Hudson River. Several small islands also compose part of the borough of Manhattan, including Randall\'s Island, Wards Island, and Roosevelt Island in the East River, and Governors Island and Liberty Island to the south in New York Harbor. Manhattan Island is loosely divided into Lower, Midtown, and Uptown regions. Uptown Manhattan is divided by Central Park into the Upper East Side and the Upper West Side, and above the park is Harlem. The borough also includes a small neighborhood on the United States mainland, called Marble Hill, which is contiguous with The Bronx. New York City\'s remaining four boroughs are collectively referred to as the outer boroughs.\nBrooklyn (Kings County), on the western tip of Long Island, is the city\'s most populous borough. Brooklyn is known for its cultural, social, and ethnic diversity, an independent art scene, distinct neighborhoods, and a distinctive architectural heritage. Downtown Brooklyn is the largest central core neighborhood in the outer boroughs. The borough has a long beachfront shoreline including Coney Island, established in the 1870s as one of the earliest amusement grounds in the country. Marine Park and Prospect Park are the two largest parks in Brooklyn. Since 2010, Brooklyn has evolved into a thriving hub of entrepreneurship and high technology startup firms, and of postmodern art and design.\nQueens (Queens County), on Long Island north and east of Brooklyn, is geographically the largest borough, the most ethnically diverse county in the United States, and the most ethnically diverse urban area in the world. Historically a collection of small towns and villages founded by the Dutch, the borough has since developed both commercial and residential prominence. Downtown Flushing has become one of the busiest central core neighborhoods in the outer boroughs. Queens is the site of Citi Field, the baseball stadium of the New York Mets, and hosts the annual U.S. Open tennis tournament at Flushing Meadows-Corona Park. Additionally, two of the three busiest airports serving the New York metropolitan area, John F. Kennedy International Airport and LaGuardia Airport, are located in Queens. (The third is Newark Liberty International Airport in Newark, New Jersey.)\nThe Bronx (Bronx County) is New York City\'s northernmost borough and the only New York City borough with a majority of it a part of the mainland United States. It is the location of Yankee Stadium, the baseball park of the New York Yankees, and home to the largest cooperatively owned housing complex in the United States, Co-op City. It is also home to the Bronx Zoo, the world\'s largest metropolitan zoo, which spans 265 acres (1.07 km2) and houses over 6,000 animals. The Bronx is also the birthplace of rap and hip hop culture. Pelham Bay Park is the largest park in New York City, at 2,772 acres (1,122 ha).\nStaten Island (Richmond County) is the most suburban in character of the five boroughs. Staten Island is connected to Brooklyn by the Verrazano-Narrows Bridge, and to Manhattan by way of the free Staten Island Ferry, a daily commuter ferry which provides unobstructed views of the Statue of Liberty, Ellis Island, and Lower Manhattan. In central Staten Island, the Staten Island Greenbelt spans approximately 2,500 acres (10 km2), including 28 miles (45 km) of walking trails and one of the last undisturbed forests in the city. Designated in 1984 to protect the island\'s natural lands, the Greenbelt comprises seven city parks.\n\n\n=== Architecture ===\n\nNew York has architecturally noteworthy buildings in a wide range of styles and from distinct time periods, from the saltbox style Pieter Claesen Wyckoff House in Brooklyn, the oldest section of which dates to 1656, to the modern One World Trade Center, the skyscraper at Ground Zero in Lower Manhattan and the most expensive office tower in the world by construction cost.Manhattan\'s skyline, with its many skyscrapers, is universally recognized, and the city has been home to several of the tallest buildings in the world. As of 2019, New York City had 6,455 high-rise buildings, the third most in world after Hong Kong and Seoul. Of these, as of 2011, 550 completed structures were at least 330 feet (100 m) high, the second most in the world after Hong Kong, with over 50 completed skyscrapers taller than 656 feet (200 m). These include the Woolworth Building, an early example of Gothic Revival architecture in skyscraper design, built with massively scaled Gothic detailing; completed in 1913, for 17 years it was the world\'s tallest building.The 1916 Zoning Resolution required setbacks in new buildings and restricted towers to a percentage of the lot size, to allow sunlight to reach the streets below. The Art Deco style of the Chrysler Building (1930) and Empire State Building (1931), with their tapered tops and steel spires, reflected the zoning requirements. The buildings have distinctive ornamentation, such as the eagles at the corners of the 61st floor on the Chrysler Building, and are considered some of the finest examples of the Art Deco style. A highly influential example of the international style in the United States is the Seagram Building (1957), distinctive for its façade using visible bronze-toned I-beams to evoke the building\'s structure. The Condé Nast Building (2000) is a prominent example of green design in American skyscrapers and has received an award from the American Institute of Architects and AIA New York State for its design.\nThe character of New York\'s large residential districts is often defined by the elegant brownstone rowhouses and townhouses and shabby tenements that were built during a period of rapid expansion from 1870 to 1930. In contrast, New York City also has neighborhoods that are less densely populated and feature free-standing dwellings. In neighborhoods such as Riverdale (in the Bronx), Ditmas Park (in Brooklyn), and Douglaston (in Queens), large single-family homes are common in various architectural styles such as Tudor Revival and Victorian.Stone and brick became the city\'s building materials of choice after the construction of wood-frame houses was limited in the aftermath of the Great Fire of 1835. A distinctive feature of many of the city\'s buildings is the roof-mounted wooden water tower. In the 1800s, the city required their installation on buildings higher than six stories to prevent the need for excessively high water pressures at lower elevations, which could break municipal water pipes. Garden apartments became popular during the 1920s in outlying areas, such as Jackson Heights.According to the United States Geological Survey, an updated analysis of seismic hazard in July 2014 revealed a "slightly lower hazard for tall buildings" in New York City than previously assessed. Scientists estimated this lessened risk based upon a lower likelihood than previously thought of slow shaking near the city, which would be more likely to cause damage to taller structures from an earthquake in the vicinity of the city.\n\n\n=== Climate ===\n\nUnder the Köppen climate classification, using the 0 °C (32 °F) isotherm, New York City features a humid subtropical climate (Cfa), and is thus the northernmost major city on the North American continent with this categorization. The suburbs to the immediate north and west lie in the transitional zone between humid subtropical and humid continental climates (Dfa). For the Trewartha classification, it is defined as an oceanic climate (Do). Annually, the city averages 234 days with at least some sunshine. The city lies in the USDA 7b plant hardiness zone.Winters are cold and damp, and prevailing wind patterns that blow sea breezes offshore temper the moderating effects of the Atlantic Ocean; yet the Atlantic and the partial shielding from colder air by the Appalachian Mountains keep the city warmer in the winter than inland North American cities at similar or lesser latitudes such as Pittsburgh, Cincinnati, and Indianapolis. The daily mean temperature in January, the area\'s coldest month, is 32.6 °F (0.3 °C); temperatures usually drop to 10 °F (−12 °C) several times per winter, and reach 60 °F (16 °C) several days in the coldest winter month. Spring and autumn are unpredictable and can range from chilly to warm, although they are usually mild with low humidity. Summers are typically warm to hot and humid, with a daily mean temperature of 76.5 °F (24.7 °C) in July.Nighttime conditions are often exacerbated by the urban heat island phenomenon, while daytime temperatures exceed 90 °F (32 °C) on average of 17 days each summer and in some years exceed 100 °F (38 °C), although the last time this happened was July 23, 2011. Extreme temperatures have ranged from −15 °F (−26 °C), recorded on February 9, 1934, up to 106 °F (41 °C) on July 9, 1936; the coldest recorded wind chill was −37 °F (−38 °C) on the same day as the all-time record low. The record cold daily maximum was 2 °F (−17 °C) on December 30, 1917, while, conversely, the record warm daily minimum was 84 °F (29 °C), last recorded on July 22, 2011. The average water temperature of the nearby Atlantic Ocean ranges from 39.7 °F (4.3 °C) in February to 74.1 °F (23.4 °C) in August.The city receives 49.9 inches (1,270 mm) of precipitation annually, which is relatively evenly spread throughout the year. Average winter snowfall between 1981 and 2010 has been 25.8 inches (66 cm); this varies considerably between years. Hurricanes and tropical storms are rare in the New York area. Hurricane Sandy brought a destructive storm surge to New York City on the evening of October 29, 2012, flooding numerous streets, tunnels, and subway lines in Lower Manhattan and other areas of the city and cutting off electricity in many parts of the city and its suburbs. The storm and its profound impacts have prompted the discussion of constructing seawalls and other coastal barriers around the shorelines of the city and the metropolitan area to minimize the risk of destructive consequences from another such event in the future.The warmest month on record is July 1999, with a mean temperature of 81.4 °F (27.4 °C). The coldest month was February 1934, with a mean temperature of 19.9 °F (−6.7 °C). The warmest year on record is 2012, with a mean temperature of 57.4 °F (14.1 °C). The coldest year was 1888, with a mean temperature of 49.3 °F (9.6 °C). The driest month on record is June 1949, with 0.02 inches (0.51 mm) of rainfall. The wettest month was August 2011, with 18.95 inches (481 mm) of rainfall. The driest year on record is 1965, with 26.09 inches (663 mm) of rainfall. The wettest year was 1983, with 80.56 inches (2,046 mm) of rainfall. The snowiest month on record is February 2010, with 36.9 inches (94 cm) of snowfall. The snowiest season (Jul–Jun) on record is 1995–1996, with 75.6 inches (192 cm) of snowfall. The least snowy season was 1972–1973, with 2.3 inches (5.8 cm) of snowfall.\n\nSee or edit raw graph data.\n\n\n=== Parks ===\n\nThe City of New York has a complex park system, with various lands operated by the National Park Service, the New York State Office of Parks, Recreation and Historic Preservation, and the New York City Department of Parks and Recreation.\nIn its 2018 ParkScore ranking, The Trust for Public Land reported that the park system in New York City was the ninth-best park system among the fifty most populous U.S. cities. ParkScore ranks urban park systems by a formula that analyzes median park size, park acres as percent of city area, the percent of city residents within a half-mile of a park, spending of park services per resident, and the number of playgrounds per 10,000 residents.\n\n\n==== National parks ====\n\nGateway National Recreation Area contains over 26,000 acres (10,521.83 ha) in total, most of it surrounded by New York City, including the Jamaica Bay Wildlife Refuge. In Brooklyn and Queens, the park contains over 9,000 acres (36 km2) of salt marsh, wetlands, islands, and water, including most of Jamaica Bay. Also in Queens, the park includes a significant portion of the western Rockaway Peninsula, most notably Jacob Riis Park and Fort Tilden. In Staten Island, Gateway National Recreation Area includes Fort Wadsworth, with historic pre-Civil War era Battery Weed and Fort Tompkins, and Great Kills Park, with beaches, trails, and a marina.\nThe Statue of Liberty National Monument and Ellis Island Immigration Museum are managed by the National Park Service and are in both the states of New York and New Jersey. They are joined in the harbor by Governors Island National Monument, in New York. Historic sites under federal management on Manhattan Island include Castle Clinton National Monument; Federal Hall National Memorial; Theodore Roosevelt Birthplace National Historic Site; General Grant National Memorial ("Grant\'s Tomb"); African Burial Ground National Monument; and Hamilton Grange National Memorial. Hundreds of private properties are listed on the National Register of Historic Places or as a National Historic Landmark such as, for example, the Stonewall Inn, part of the Stonewall National Monument in Greenwich Village, as the catalyst of the modern gay rights movement.\n\n\n==== State parks ====\n\nThere are seven state parks within the confines of New York City, including Clay Pit Ponds State Park Preserve, a natural area that includes extensive riding trails, and Riverbank State Park, a 28-acre (11 ha) facility that rises 69 feet (21 m) over the Hudson River.\n\n\n==== City parks ====\n\nNew York City has over 28,000 acres (110 km2) of municipal parkland and 14 miles (23 km) of public beaches. The largest municipal park in the city is Pelham Bay Park in the Bronx, with 2,772 acres (1,122 ha).\nCentral Park, an 843-acre (3.41 km2) park in middle-upper Manhattan, is the most visited urban park in the United States and one of the most filmed locations in the world, with 40 million visitors in 2013. The park contains a wide range of attractions; there are several lakes and ponds, two ice-skating rinks, the Central Park Zoo, the Central Park Conservatory Garden, and the 106-acre (0.43 km2) Jackie Onassis Reservoir. Indoor attractions include Belvedere Castle with its nature center, the Swedish Cottage Marionette Theater, and the historic Carousel. On October 23, 2012, hedge fund manager John A. Paulson announced a $100 million gift to the Central Park Conservancy, the largest ever monetary donation to New York City\'s park system.\nWashington Square Park is a prominent landmark in the Greenwich Village neighborhood of Lower Manhattan. The Washington Square Arch at the northern gateway to the park is an iconic symbol of both New York University and Greenwich Village.\nProspect Park in Brooklyn has a 90-acre (36 ha) meadow, a lake, and extensive woodlands. Within the park is the historic Battle Pass, prominent in the Battle of Long Island.\nFlushing Meadows–Corona Park in Queens, with its 897 acres (363 ha) making it the city\'s fourth largest park, was the setting for the 1939 World\'s Fair and the 1964 World\'s Fair and is host to the USTA Billie Jean King National Tennis Center and the annual United States Open Tennis Championships tournament.\nOver a fifth of the Bronx\'s area, 7,000 acres (28 km2), is given over to open space and parks, including Pelham Bay Park, Van Cortlandt Park, the Bronx Zoo, and the New York Botanical Gardens.\nIn Staten Island, the Conference House Park contains the historic Conference House, site of the only attempt of a peaceful resolution to the American Revolution which was conducted in September 1775, attended by Benjamin Franklin representing the Americans and Lord Howe representing the British Crown. The historic Burial Ridge, the largest Native American burial ground within New York City, is within the park.\n\n\n=== Military installations ===\nNew York City is home to Fort Hamilton, the U.S. military\'s only active duty installation within the city. The Brooklyn facility was established in 1825 on the site of a small battery utilized during the American Revolution, and it is one of America\'s longest serving military forts. Today Fort Hamilton serves as the headquarters of the North Atlantic Division of the United States Army Corps of Engineers and for the New York City Recruiting Battalion. It also houses the 1179th Transportation Brigade, the 722nd Aeromedical Staging Squadron, and a military entrance processing station. Other formerly active military reservations still utilized for National Guard and military training or reserve operations in the city include Fort Wadsworth in Staten Island and Fort Totten in Queens.\n\n\n== Demographics ==\n\nNew York City is the most populous city in the United States, with an estimated 8,398,748 residents as of July 2018, incorporating more immigration into the city than outmigration since the 2010 United States Census. More than twice as many people live in New York City as compared to Los Angeles, the second-most populous U.S. city, and within a smaller area. New York City gained more residents between April 2010 and July 2014 (316,000) than any other U.S. city. New York City\'s population is about 43% of New York State\'s population and about 36% of the population of the New York metropolitan area.\n\n\n=== Population density ===\nIn 2017, the city had an estimated population density of 28,491 inhabitants per square mile (11,000/km2), rendering it the most densely populated of all municipalities housing over 100,000 residents in the United States, with several small cities (of fewer than 100,000) in adjacent Hudson County, New Jersey having greater density, as per the 2010 Census. Geographically co-extensive with New York County, the borough of Manhattan\'s 2017 population density of 72,918 inhabitants per square mile (28,154/km2) makes it the highest of any county in the United States and higher than the density of any individual American city.\n\n\n=== Race and ethnicity ===\n\nThe city\'s population in 2010 was 44% white (33.3% non-Hispanic white), 25.5% black (23% non-Hispanic black), 0.7% Native American, and 12.7% Asian. Hispanics of any race represented 28.6% of the population, while Asians constituted the fastest-growing segment of the city\'s population between 2000 and 2010; the non-Hispanic white population declined 3 percent, the smallest recorded decline in decades; and for the first time since the Civil War, the number of blacks declined over a decade.\nThroughout its history, New York has been a major port of entry for immigrants into the United States. More than 12 million European immigrants were received at Ellis Island between 1892 and 1924. The term "melting pot" was first coined to describe densely populated immigrant neighborhoods on the Lower East Side. By 1900, Germans constituted the largest immigrant group, followed by the Irish, Jews, and Italians. In 1940, whites represented 92% of the city\'s population.Approximately 37% of the city\'s population is foreign born, and more than half of all children are born to mothers who are immigrants. In New York, no single country or region of origin dominates. The ten largest sources of foreign-born individuals in the city as of 2011 were the Dominican Republic, China, Mexico, Guyana, Jamaica, Ecuador, Haiti, India, Russia, and Trinidad and Tobago, while the Bangladeshi-born immigrant population has become one of the fastest growing in the city, counting over 74,000 by 2011.\n\nAsian Americans in New York City, according to the 2010 Census, number more than one million, greater than the combined totals of San Francisco and Los Angeles. New York contains the highest total Asian population of any U.S. city proper. The New York City borough of Queens is home to the state\'s largest Asian American population and the largest Andean (Colombian, Ecuadorian, Peruvian, and Bolivian) populations in the United States, and is also the most ethnically diverse urban area in the world.The Chinese population constitutes the fastest-growing nationality in New York State; multiple satellites of the original Manhattan Chinatown, in Brooklyn, and around Flushing, Queens, are thriving as traditionally urban enclaves – while also expanding rapidly eastward into suburban Nassau County on Long Island, as the New York metropolitan region and New York State have become the top destinations for new Chinese immigrants, respectively, and large-scale Chinese immigration continues into New York City and surrounding areas, with the largest metropolitan Chinese diaspora outside Asia, including an estimated 812,410 individuals in 2015.In 2012, 6.3% of New York City was of Chinese ethnicity, with nearly three-fourths living in either Queens or Brooklyn, geographically on Long Island. A community numbering 20,000 Korean-Chinese (Chaoxianzu or Joseonjok) is centered in Flushing, Queens, while New York City is also home to the largest Tibetan population outside China, India, and Nepal, also centered in Queens. Koreans made up 1.2% of the city\'s population, and Japanese 0.3%. Filipinos were the largest Southeast Asian ethnic group at 0.8%, followed by Vietnamese, who made up 0.2% of New York City\'s population in 2010. Indians are the largest South Asian group, comprising 2.4% of the city\'s population, with Bangladeshis and Pakistanis at 0.7% and 0.5%, respectively. Queens is the preferred borough of settlement for Asian Indians, Koreans, Filipinos, and Malaysians and other Southeast Asians; while Brooklyn is receiving large numbers of both West Indian and Asian Indian immigrants.\n\nNew York City has the largest European and non-Hispanic white population of any American city. At 2.7 million in 2012, New York\'s non-Hispanic white population is larger than the non-Hispanic white populations of Los Angeles (1.1 million), Chicago (865,000), and Houston (550,000) combined. The non-Hispanic white population was 6.6 million in 1940. The non-Hispanic white population has begun to increase since 2010.The European diaspora residing in the city is very diverse. According to 2012 Census estimates, there were roughly 560,000 Italian Americans, 385,000 Irish Americans, 253,000 German Americans, 223,000 Russian Americans, 201,000 Polish Americans, and 137,000 English Americans. Additionally, Greek and French Americans numbered 65,000 each, with those of Hungarian descent estimated at 60,000 people. Ukrainian and Scottish Americans numbered 55,000 and 35,000, respectively. People identifying ancestry from Spain numbered 30,838 total in 2010.People of Norwegian and Swedish descent both stood at about 20,000 each, while people of Czech, Lithuanian, Portuguese, Scotch-Irish, and Welsh descent all numbered between 12,000–14,000 people. Arab Americans number over 160,000 in New York City, with the highest concentration in Brooklyn. Central Asians, primarily Uzbek Americans, are a rapidly growing segment of the city\'s non-Hispanic white population, enumerating over 30,000, and including over half of all Central Asian immigrants to the United States, most settling in Queens or Brooklyn. Albanian Americans are most highly concentrated in the Bronx.The wider New York City metropolitan statistical area, with over 20 million people, about 50% greater than the second-place Los Angeles metropolitan area in the United States, is also ethnically diverse, with the largest foreign-born population of any metropolitan region in the world. The New York region continues to be by far the leading metropolitan gateway for legal immigrants admitted into the United States, substantially exceeding the combined totals of Los Angeles and Miami. It is home to the largest Jewish and Israeli communities outside Israel, with the Jewish population in the region numbering over 1.5 million in 2012 and including many diverse Jewish sects, predominantly from around the Middle East and Eastern Europe, and including a rapidly growing Orthodox Jewish population, the largest outside Israel.The metropolitan area is also home to 20% of the USA\'s Indian Americans and at least 20 Little India enclaves, and 15% of all Korean Americans and four Koreatowns; the largest Asian Indian population in the Western Hemisphere; the largest Russian American, Italian American, and African American populations; the largest Dominican American, Puerto Rican American, and South American and second-largest overall Hispanic population in the United States, numbering 4.8 million; and includes multiple established Chinatowns within New York City alone.Ecuador, Colombia, Guyana, Peru, and Brazil were the top source countries from South America for legal immigrants to the New York City region in 2013; the Dominican Republic, Jamaica, Haiti, and Trinidad and Tobago in the Caribbean; Egypt, Ghana, and Nigeria from Africa; and El Salvador, Honduras, and Guatemala in Central America. Amidst a resurgence of Puerto Rican migration to New York City, this population had increased to approximately 1.3 million in the metropolitan area as of 2013.\nSince 2010, a Little Australia has emerged and is growing rapidly representing the Australasian presence in Nolita, Manhattan. In 2011, there were an estimated 20,000 Australian residents of New York City, nearly quadruple the 5,537 in 2005. Qantas Airways of Australia and Air New Zealand have been exploring the possibilities of long-haul flights from New York to Sydney and Auckland, respectively, which would both rank among the longest non-stop flights in the world. A Little Sri Lanka has developed in the Tompkinsville neighborhood of Staten Island.\n\n\n=== Sexual orientation and gender identity ===\n\nThe New York metropolitan area is home to a prominent self-identifying gay and bisexual community estimated at nearly 570,000 individuals, the largest in the United States and one of the world\'s largest. Same-sex marriages in New York were legalized on June 24, 2011 and were authorized to take place beginning 30 days thereafter. Charles Kaiser, author of The Gay Metropolis: The Landmark History of Gay Life in America, wrote that in the era after World War II, "New York City became the literal gay metropolis for hundreds of thousands of immigrants from within and without the United States: the place they chose to learn how to live openly, honestly and without shame."The annual New York City Pride March (or gay pride parade) traverses southward down Fifth Avenue and ends at Greenwich Village in Lower Manhattan; the parade rivals the Sao Paulo Gay Pride Parade as the largest pride parade in the world, attracting tens of thousands of participants and millions of sidewalk spectators each June. The annual Queens Pride Parade is held in Jackson Heights and is accompanied by the ensuing Multicultural Parade. Stonewall 50 – WorldPride NYC 2019 was the largest international Pride celebration in history, produced by Heritage of Pride and enhanced through a partnership with the I ❤ NY program\'s LGBT division, commemorating the 50th anniversary of the Stonewall uprising, with 150,000 participants and five million spectators attending in Manhattan alone. New York City is also home to the largest transgender population in the world, estimated at more than 50,000 in 2018, concentrated in Manhattan and Queens; however, until the June 1969 Stonewall riots, this community had felt marginalized and neglected by the gay community.\n\n\n=== Religion ===\nChristianity (59%) — made up of Roman Catholicism (33%), Protestantism (23%), and other Christians (3%) — is the most prevalent religion in New York, as of 2014. It is followed by Judaism, with approximately 1.1 million adherents, over half of whom live in Brooklyn. The Jewish population makes up 18.4% of the city. Islam ranks third in New York City, with estimates ranging between 600,000 and 1,000,000 observers, including 10% of the city\'s public school children. These three largest groups are followed by Hinduism, Buddhism, and a variety of other religions, as well as atheism. In 2014, 24% of New Yorkers self-identified with no organized religious affiliation.\n\n\n=== Wealth and income disparity ===\nNew York City has a high degree of income disparity as indicated by its Gini Coefficient of 0.5 for the city overall and 0.6 for Manhattan, as of 2006. (This is not unusual, as all large cities have greater income disparities than the nation overall.) In the first quarter of 2014, the average weekly wage in New York County (Manhattan) was $2,749, representing the highest total among large counties in the United States. As of 2017, New York City was home to the highest number of billionaires of any city in the world at 103, including former Mayor Michael Bloomberg. New York also had the highest density of millionaires per capita among major U.S. cities in 2014, at 4.6% of residents. New York City is one of the relatively few American cities levying an income tax (currently about 3%) on its residents. As of 2018, there were 78,676 homeless people in New York City.\n\n\n== Economy ==\n\n\n=== City economic overview ===\nNew York City is a global hub of business and commerce, as a center for banking and finance, retailing, world trade, transportation, tourism, real estate, new media, traditional media, advertising, legal services, accountancy, insurance, theater, fashion, and the arts in the United States; while Silicon Alley, metonymous for New York\'s broad-spectrum high technology sphere, continues to expand. The Port of New York and New Jersey is also a major economic engine, handling record cargo volume in 2017, over 6.7 million TEUs. New York City\'s unemployment rate fell to its record low of 4.0% in September 2018.Many Fortune 500 corporations are headquartered in New York City, as are a large number of multinational corporations. One out of ten private sector jobs in the city is with a foreign company. New York City has been ranked first among cities across the globe in attracting capital, business, and tourists. New York City\'s role as the top global center for the advertising industry is metonymously reflected as "Madison Avenue". The city\'s fashion industry provides approximately 180,000 employees with $11 billion in annual wages.Other important sectors include medical research and technology, non-profit institutions, and universities. Manufacturing accounts for a significant but declining share of employment, although the city\'s garment industry is showing a resurgence in Brooklyn. Food processing is a US$5 billion industry that employs more than 19,000 residents.\nChocolate is New York City\'s leading specialty-food export, with up to US$234 million worth of exports each year. Entrepreneurs were forming a "Chocolate District" in Brooklyn as of 2014, while Godiva, one of the world\'s largest chocolatiers, continues to be headquartered in Manhattan.\n\n\n=== Wall Street ===\n\nNew York City\'s most important economic sector lies in its role as the headquarters for the U.S. financial industry, metonymously known as Wall Street. The city\'s securities industry, enumerating 163,400 jobs in August 2013, continues to form the largest segment of the city\'s financial sector and an important economic engine, accounting in 2012 for 5 percent of the city\'s private sector jobs, 8.5 percent (US$3.8 billion) of its tax revenue, and 22 percent of the city\'s total wages, including an average salary of US$360,700. Many large financial companies are headquartered in New York City, and the city is also home to a burgeoning number of financial startup companies.\nLower Manhattan is home to the New York Stock Exchange, on Wall Street, and the NASDAQ, at 165 Broadway, representing the world\'s largest and second largest stock exchanges, respectively, when measured both by overall average daily trading volume and by total market capitalization of their listed companies in 2013. Investment banking fees on Wall Street totaled approximately $40 billion in 2012, while in 2013, senior New York City bank officers who manage risk and compliance functions earned as much as $324,000 annually. In fiscal year 2013–14, Wall Street\'s securities industry generated 19% of New York State\'s tax revenue.New York City remains the largest global center for trading in public equity and debt capital markets, driven in part by the size and financial development of the U.S. economy. New York also leads in hedge fund management; private equity; and the monetary volume of mergers and acquisitions. Several investment banks and investment managers headquartered in Manhattan are important participants in other global financial centers. New York is also the principal commercial banking center of the United States.Many of the world\'s largest media conglomerates are also based in the city. Manhattan contained over 500 million square feet (46.5 million m2) of office space in 2018, making it the largest office market in the United States, while Midtown Manhattan, with 400 million square feet (37.2 million m2) in 2018, is the largest central business district in the world.\n\n\n=== Tech and biotech ===\n\nSilicon Alley, centered in Manhattan, has evolved into a metonym for the sphere encompassing the New York City metropolitan region\'s high technology industries involving the Internet, new media, telecommunications, digital media, software development, game design, financial technology ("FinTech"), and other fields within information technology that are supported by its entrepreneurship ecosystem and venture capital investments. In 2015, Silicon Alley generated over US$7.3 billion in venture capital investment across a broad spectrum of high technology enterprises, most based in Manhattan, with others in Brooklyn, Queens, and elsewhere in the region.\nHigh technology startup companies and employment are growing in New York City and the region, bolstered by the city\'s position in North America as the leading Internet hub and telecommunications center, including its vicinity to several transatlantic fiber optic trunk lines, New York\'s intellectual capital, and its extensive outdoor wireless connectivity. Verizon Communications, headquartered at 140 West Street in Lower Manhattan, was at the final stages in 2014 of completing a US$3 billion fiberoptic telecommunications upgrade throughout New York City. As of 2014, New York City hosted 300,000 employees in the tech sector. The technology sector has been claiming a greater share of New York City\'s economy since 2010. Tech:NYC, founded in 2016, is a non-profit organization which represents New York City\'s technology industry with government, civic institutions, in business, and in the media, and whose primary goals are to further augment New York\'s substantial tech talent base and to advocate for policies that will nurture tech companies to grow in the city.The biotechnology sector is also growing in New York City, based upon the city\'s strength in academic scientific research and public and commercial financial support. On December 19, 2011, then Mayor Michael R. Bloomberg announced his choice of Cornell University and Technion-Israel Institute of Technology to build a US$2 billion graduate school of applied sciences called Cornell Tech on Roosevelt Island with the goal of transforming New York City into the world\'s premier technology capital. By mid-2014, Accelerator, a biotech investment firm, had raised more than US$30 million from investors, including Eli Lilly and Company, Pfizer, and Johnson & Johnson, for initial funding to create biotechnology startups at the Alexandria Center for Life Science, which encompasses more than 700,000 square feet (65,000 m2) on East 29th Street and promotes collaboration among scientists and entrepreneurs at the center and with nearby academic, medical, and research institutions. The New York City Economic Development Corporation\'s Early Stage Life Sciences Funding Initiative and venture capital partners, including Celgene, General Electric Ventures, and Eli Lilly, committed a minimum of US$100 million to help launch 15 to 20 ventures in life sciences and biotechnology.\n\n\n=== Real estate ===\n\nReal estate is a major force in the city\'s economy, as the total value of all New York City property was assessed at US$1.072 trillion for the 2017 fiscal year, an increase of 10.6% from the previous year, with 89% of the increase coming from market effects. The Time Warner Center is the property with the highest-listed market value in the city, at US$1.1 billion in 2006. New York City is home to some of the nation\'s—and the world\'s—most valuable real estate. 450 Park Avenue was sold on July 2, 2007 for US$510 million, about $1,589 per square foot ($17,104/m²), breaking the barely month-old record for an American office building of $1,476 per square foot ($15,887/m²) set in the June 2007 sale of 660 Madison Avenue.In 2014 Manhattan was home to six of the top ten ZIP Codes in the United States by median housing price. Fifth Avenue in Midtown Manhattan commands the highest retail rents in the world, at US$3,000 per square foot ($32,000/m2) in 2017. In 2019, the most expensive home sale ever in the United States achieved completion in Manhattan, at a selling price of US$238 million, for a 24,000 square feet (2,200 m2) penthouse apartment overlooking Central Park.\n\n\n=== Tourism ===\n\nTourism is a vital industry for New York City, which has witnessed a growing combined volume of international and domestic tourists, receiving an eighth consecutive annual record of approximately 62.8 million visitors in 2017. Tourism had generated an all-time high US$61.3 billion in overall economic impact for New York City in 2014, pending 2015 statistics. Approximately 12 million visitors to New York City were from outside the United States, with the highest numbers from the United Kingdom, Canada, Brazil, and China.\nI Love New York (stylized I ❤ NY) is both a logo and a song that are the basis of an advertising campaign and have been used since 1977 to promote tourism in New York City, and later to promote New York State as well. The trademarked logo, owned by New York State Empire State Development, appears in souvenir shops and brochures throughout the city and state, some licensed, many not. The song is the state song of New York.\nMajor tourist destinations include Times Square; Broadway theater productions; the Empire State Building; the Statue of Liberty; Ellis Island; the United Nations Headquarters; museums such as the Metropolitan Museum of Art; greenspaces such as Central Park and Washington Square Park; Rockefeller Center; the Manhattan Chinatown; luxury shopping along Fifth and Madison Avenues; and events such as the Halloween Parade in Greenwich Village; the Macy\'s Thanksgiving Day Parade; the lighting of the Rockefeller Center Christmas Tree; the St. Patrick\'s Day parade; seasonal activities such as ice skating in Central Park in the wintertime; the Tribeca Film Festival; and free performances in Central Park at Summerstage. Major attractions in the boroughs outside Manhattan include Flushing Meadows-Corona Park and the Unisphere in Queens; the Bronx Zoo; Coney Island, Brooklyn; and the New York Botanical Garden in the Bronx. The New York Wheel, a 630-foot ferris wheel, was under construction at the northern shore of Staten Island in 2015, overlooking the Statue of Liberty, New York Harbor, and the Lower Manhattan skyline.Manhattan was on track to have an estimated 90,000 hotel rooms at the end of 2014, a 10% increase from 2013. In October 2014, the Anbang Insurance Group, based in China, purchased the Waldorf Astoria New York for US$1.95 billion, making it the world\'s most expensive hotel ever sold.\n\n\n=== Media and entertainment ===\n\nNew York is a prominent location for the American entertainment industry, with many films, television series, books, and other media being set there. As of 2012, New York City was the second largest center for filmmaking and television production in the United States, producing about 200 feature films annually, employing 130,000 individuals. The filmed entertainment industry has been growing in New York, contributing nearly US$9 billion to the New York City economy alone as of 2015. By volume, New York is the world leader in independent film production – one-third of all American independent films are produced in New York City. The Association of Independent Commercial Producers is also based in New York. In the first five months of 2014 alone, location filming for television pilots in New York City exceeded the record production levels for all of 2013, with New York surpassing Los Angeles as the top North American city for the same distinction during the 2013–2014 cycle.New York City is also a center for the advertising, music, newspaper, digital media, and publishing industries and is also the largest media market in North America. Some of the city\'s media conglomerates and institutions include Time Warner, the Thomson Reuters Corporation, the Associated Press, Bloomberg L.P., the News Corporation, The New York Times Company, NBCUniversal, the Hearst Corporation, AOL, and Viacom. Seven of the world\'s top eight global advertising agency networks have their headquarters in New York. Two of the top three record labels\' headquarters are in New York: Sony Music Entertainment and Warner Music Group. Universal Music Group also has offices in New York. New media enterprises are contributing an increasingly important component to the city\'s central role in the media sphere.\nMore than 200 newspapers and 350 consumer magazines have an office in the city, and the publishing industry employs about 25,000 people. Two of the three national daily newspapers with the largest circulations in the United States are published in New York: The Wall Street Journal and The New York Times, which has won the most Pulitzer Prizes for journalism. Major tabloid newspapers in the city include The New York Daily News, which was founded in 1919 by Joseph Medill Patterson, and The New York Post, founded in 1801 by Alexander Hamilton. The city also has a comprehensive ethnic press, with 270 newspapers and magazines published in more than 40 languages. El Diario La Prensa is New York\'s largest Spanish-language daily and the oldest in the nation. The New York Amsterdam News, published in Harlem, is a prominent African American newspaper. The Village Voice, historically the largest alternative newspaper in the United States, announced in 2017 that it would cease publication of its print edition and convert to a fully digital venture.The television and radio industry developed in New York and is a significant employer in the city\'s economy. The three major American broadcast networks are all headquartered in New York: ABC, CBS, and NBC. Many cable networks are based in the city as well, including MTV, Fox News, HBO, Showtime, Bravo, Food Network, AMC, and Comedy Central. The City of New York operates a public broadcast service, NYC Media, that has produced several original Emmy Award-winning shows covering music and culture in city neighborhoods and city government. WBAI, with news and information programming, is one of the few socialist radio stations operating in the United States.\nNew York is also a major center for non-commercial educational media. The oldest public-access television channel in the United States is the Manhattan Neighborhood Network, founded in 1971. WNET is the city\'s major public television station and a primary source of national Public Broadcasting Service (PBS) television programming. WNYC, a public radio station owned by the city until 1997, has the largest public radio audience in the United States.\n\n\n== Education and scholarly activity ==\n\n\n=== Primary and secondary education ===\nThe New York City Public Schools system, managed by the New York City Department of Education, is the largest public school system in the United States, serving about 1.1 million students in more than 1,700 separate primary and secondary schools. The city\'s public school system includes nine specialized high schools to serve academically and artistically gifted students. The city government pays the Pelham Public Schools to educate a very small, detached section of the Bronx.\n\nThe New York City Charter School Center assists the setup of new charter schools. There are approximately 900 additional privately run secular and religious schools in the city.\n\n\n=== Higher education and research ===\nOver 600,000 students are enrolled in New York City\'s over 120 higher education institutions, the highest number of any city in the United States and higher than other major global cities like London and Tokyo, including over half million in the City University of New York (CUNY) system alone in 2014. New York City\'s higher education institutions had also higher average scores than those two cities in 2019, according to the Academic Ranking of World Universities. New York City is home to such notable private universities as Barnard College, Columbia University, Cooper Union, Fordham University, New York University, New York Institute of Technology, Rockefeller University, and Yeshiva University; several of these universities are ranked among the top universities in the world.The public CUNY system is one of the largest universities in the nation, comprising 24 institutions across all five boroughs: senior colleges, community colleges, and other graduate/professional schools. The public State University of New York (SUNY) system includes campuses in New York City, including: Downstate Health Sciences University, Fashion Institute of Technology, Maritime College, and the College of Optometry. The city also hosts other smaller private colleges and universities, including many religious and special-purpose institutions, such as: St. John\'s University, The Juilliard School, Manhattan College, The College of Mount Saint Vincent, Parsons School of Design, The New School, Pratt Institute, New York Film Academy, The School of Visual Arts, The King\'s College, and Wagner College.\nMuch of the scientific research in the city is done in medicine and the life sciences. New York City has the most postgraduate life sciences degrees awarded annually in the United States, with 127 Nobel laureates having roots in local institutions as of 2005; while in 2012, 43,523 licensed physicians were practicing in New York City. Major biomedical research institutions include Memorial Sloan–Kettering Cancer Center, Rockefeller University, SUNY Downstate Medical Center, Albert Einstein College of Medicine, Mount Sinai School of Medicine, and Weill Cornell Medical College, being joined by the Cornell University/Technion-Israel Institute of Technology venture on Roosevelt Island. The graduates of SUNY Maritime College in the Bronx earned the highest average annual salary of any university graduates in the United States, US$144,000 as of 2017.\n\n\n== Human resources ==\n\n\n=== Public health ===\n\nThe New York City Health and Hospitals Corporation (HHC) operates the public hospitals and clinics in New York City. A public benefit corporation with $6.7 billion in annual revenues, HHC is the largest municipal healthcare system in the United States serving 1.4 million patients, including more than 475,000 uninsured city residents. HHC was created in 1969 by the New York State Legislature as a public benefit corporation (Chapter 1016 of the Laws 1969). HHC operates 11 acute care hospitals, five nursing homes, six diagnostic and treatment centers, and more than 70 community-based primary care sites, serving primarily the poor and working class. HHC\'s MetroPlus Health Plan is one of the New York area\'s largest providers of government-sponsored health insurance and is the plan of choice for nearly half million New Yorkers.HHC\'s facilities annually provide millions of New Yorkers services interpreted in more than 190 languages. The most well-known hospital in the HHC system is Bellevue Hospital, the oldest public hospital in the United States. Bellevue is the designated hospital for treatment of the President of the United States and other world leaders if they become sick or injured while in New York City. The president of HHC is Ramanathan Raju, MD, a surgeon and former CEO of the Cook County health system in Illinois. In August 2017, Mayor Bill de Blasio signed legislation outlawing pharmacies from selling cigarettes once their existing licenses to do so expired, beginning in 2018.\n\n\n=== Public safety ===\n\n\n==== Police and law enforcement ====\n\nThe New York Police Department (NYPD) has been the largest police force in the United States by a significant margin, with over 35,000 sworn officers. Members of the NYPD are frequently referred to by politicians, the media, and their own police cars by the nickname, New York\'s Finest.\nCrime has continued an overall downward trend in New York City since the 1990s. In 2012, the NYPD came under scrutiny for its use of a stop-and-frisk program, which has undergone several policy revisions since then. In 2014, New York City had the third lowest murder rate among the largest U.S. cities, having become significantly safer after a spike in crime in the 1970s through 1990s. Violent crime in New York City decreased more than 75% from 1993 to 2005, and continued decreasing during periods when the nation as a whole saw increases. By 2002, New York City\'s crime rate was similar to that of Provo, Utah, and was ranked 197th in crime among the 216 U.S. cities with populations greater than 100,000. In 1992, the city recorded 2,245 murders. In 2005, the homicide rate was at its lowest level since 1966, and in 2009, the city recorded fewer than 461 homicides for the first time ever since crime statistics were first published in 1963. In 2017, 60.1% of violent crime suspects were Black, 29.6% Hispanic, 6.5% White, 3.6% Asian and 0.2% American Indian. New York City experienced 292 homicides in 2017,Sociologists and criminologists have not reached consensus on the explanation for the dramatic decrease in the city\'s crime rate. Some attribute the phenomenon to new tactics used by the NYPD, including its use of CompStat and the broken windows theory. Others cite the end of the crack epidemic and demographic changes, including from immigration. Another theory is that widespread exposure to lead pollution from automobile exhaust, which can lower intelligence and increase aggression levels, incited the initial crime wave in the mid-20th century, most acutely affecting heavily trafficked cities like New York. A strong correlation was found demonstrating that violent crime rates in New York and other big cities began to fall after lead was removed from American gasoline in the 1970s. Another theory cited to explain New York City\'s falling homicide rate is the inverse correlation between the number of murders and the increasingly wetter climate in the city.Organized crime has long been associated with New York City, beginning with the Forty Thieves and the Roach Guards in the Five Points in the 1820s. The 20th century saw a rise in the Mafia, dominated by the Five Families, as well as in gangs, including the Black Spades. The Mafia and gang presence has declined in the city in the 21st century.\n\n\n==== Firefighting ====\n\nThe Fire Department of New York (FDNY), provides fire protection, technical rescue, primary response to biological, chemical, and radioactive hazards, and emergency medical services for the five boroughs of New York City. The FDNY is the largest municipal fire department in the United States and the second largest in the world after the Tokyo Fire Department. The FDNY employs approximately 11,080 uniformed firefighters and over 3,300 uniformed EMTs and paramedics. The FDNY\'s motto is New York\'s Bravest.\nThe fire department faces multifaceted firefighting challenges in many ways unique to New York. In addition to responding to building types that range from wood-frame single family homes to high-rise structures, there are many secluded bridges and tunnels, as well as large parks and wooded areas that can give rise to brush fires. New York is also home to one of the largest subway systems in the world, consisting of hundreds of miles of tunnel with electrified track.\nThe FDNY headquarters is located at 9 MetroTech Center in Downtown Brooklyn, and the FDNY Fire Academy is located on Randalls Island. There are three Bureau of Fire Communications alarm offices which receive and dispatch alarms to appropriate units. One office, at 11 Metrotech Center in Brooklyn, houses Manhattan/Citywide, Brooklyn, and Staten Island Fire Communications; the Bronx and Queens offices are in separate buildings.\n\n\n=== Public library system ===\n\nThe New York Public Library, which has the largest collection of any public library system in the United States, serves Manhattan, the Bronx, and Staten Island. Queens is served by the Queens Borough Public Library, the nation\'s second largest public library system, while the Brooklyn Public Library serves Brooklyn.\n\n\n== Culture and contemporary life ==\n\nNew York City has been described as the cultural capital of the world by the diplomatic consulates of Iceland and Latvia and by New York\'s Baruch College. A book containing a series of essays titled New York, Culture Capital of the World, 1940–1965 has also been published as showcased by the National Library of Australia. In describing New York, author Tom Wolfe said, "Culture just seems to be in the air, like part of the weather."Numerous major American cultural movements began in the city, such as the Harlem Renaissance, which established the African-American literary canon in the United States. The city was a center of jazz in the 1940s, abstract expressionism in the 1950s, and the birthplace of hip hop in the 1970s. The city\'s punk and hardcore scenes were influential in the 1970s and 1980s. New York has long had a flourishing scene for Jewish American literature.\nThe city is the birthplace of many cultural movements, including the Harlem Renaissance in literature and visual art; abstract expressionism (also known as the New York School) in painting; and hip hop, punk, salsa, freestyle, Tin Pan Alley, certain forms of jazz, and (along with Philadelphia) disco in music. New York City has been considered the dance capital of the world. The city is also frequently the setting for novels, movies (see List of films set in New York City), and television programs. New York Fashion Week is one of the world\'s preeminent fashion events and is afforded extensive coverage by the media.\nNew York has also frequently been ranked the top fashion capital of the world on the annual list compiled by the Global Language Monitor.\n\n\n=== Pace ===\nOne of the most common traits attributed to New York City is its fast pace, which spawned the term New York minute. Journalist Walt Whitman characterized New York\'s streets as being traversed by "hurrying, feverish, electric crowds".\n\n\n=== Arts ===\nNew York City has more than 2,000 arts and cultural organizations and more than 500 art galleries of all sizes. The city government funds the arts with a larger annual budget than the National Endowment for the Arts. Wealthy business magnates in the 19th century built a network of major cultural institutions, such as the famed Carnegie Hall and the Metropolitan Museum of Art, that would become internationally established. The advent of electric lighting led to elaborate theater productions, and in the 1880s, New York City theaters on Broadway and along 42nd Street began featuring a new stage form that became known as the Broadway musical. Strongly influenced by the city\'s immigrants, productions such as those of Harrigan and Hart, George M. Cohan, and others used song in narratives that often reflected themes of hope and ambition. New York City itself is the subject or background of many plays and musicals.\n\n\n==== Performing arts ====\n\nBroadway theatre is one of the premier forms of English-language theatre in the world, named after Broadway, the major thoroughfare that crosses Times Square, also sometimes referred to as "The Great White Way". Forty-one venues in Midtown Manhattan\'s Theatre District, each with at least 500 seats, are classified as Broadway theatres. According to The Broadway League, Broadway shows sold approximately US$1.27 billion worth of tickets in the 2013–2014 season, an 11.4% increase from US$1.139 billion in the 2012–2013 season. Attendance in 2013–2014 stood at 12.21 million, representing a 5.5% increase from the 2012–2013 season\'s 11.57 million. Performance artists displaying diverse skills are ubiquitous on the streets of Manhattan.\nLincoln Center for the Performing Arts, anchoring Lincoln Square on the Upper West Side of Manhattan, is home to numerous influential arts organizations, including the Metropolitan Opera, New York City Opera, New York Philharmonic, and New York City Ballet, as well as the Vivian Beaumont Theater, the Juilliard School, Jazz at Lincoln Center, and Alice Tully Hall. The Lee Strasberg Theatre and Film Institute is in Union Square, and Tisch School of the Arts is based at New York University, while Central Park SummerStage presents free music concerts in Central Park.\n\n\n==== Visual arts ====\n\nNew York City is home to hundreds of cultural institutions and historic sites, many of which are internationally known. Museum Mile is the name for a section of Fifth Avenue running from 82nd to 105th streets on the Upper East Side of Manhattan, in an area sometimes called Upper Carnegie Hill. The Mile, which contains one of the densest displays of culture in the world, is actually three blocks longer than one mile (1.6 km). Ten museums occupy the length of this section of Fifth Avenue. The tenth museum, the Museum for African Art, joined the ensemble in 2009, although its museum at 110th Street, the first new museum constructed on the Mile since the Guggenheim in 1959, opened in late 2012. In addition to other programming, the museums collaborate for the annual Museum Mile Festival, held each year in June, to promote the museums and increase visitation. Many of the world\'s most lucrative art auctions are held in New York City.\n\n\n=== Cuisine ===\n\nNew York City\'s food culture includes an array of international cuisines influenced by the city\'s immigrant history. Central and Eastern European immigrants, especially Jewish immigrants from those regions, brought bagels, cheesecake, hot dogs, knishes, and delicatessens (or delis) to the city. Italian immigrants brought New York-style pizza and Italian cuisine into the city, while Jewish immigrants and Irish immigrants brought pastrami and corned beef, respectively. Chinese and other Asian restaurants, sandwich joints, trattorias, diners, and coffeehouses are ubiquitous throughout the city. Some 4,000 mobile food vendors licensed by the city, many immigrant-owned, have made Middle Eastern foods such as falafel and kebabs examples of modern New York street food. The city is home to "nearly one thousand of the finest and most diverse haute cuisine restaurants in the world", according to Michelin. The New York City Department of Health and Mental Hygiene assigns letter grades to the city\'s restaurants based upon their inspection results. As of 2019, there were 27,043 restaurants in the city, up from 24,865 in 2017. The Queens Night Market in Flushing Meadows–Corona Park attracts over 10,000 people nightly to sample food from over 85 countries.\n\n\n=== Parades ===\n\nNew York City is well known for its street parades, which celebrate a broad array of themes, including holidays, nationalities, human rights, and major league sports team championship victories. The majority of parades are held in Manhattan. The primary orientation of the annual street parades is typically from north to south, marching along major avenues. The annual Macy\'s Thanksgiving Day Parade is the world\'s largest parade, beginning alongside Central Park and processing southward to the flagship Macy\'s Herald Square store; the parade is viewed on telecasts worldwide and draws millions of spectators in person. Other notable parades including the annual St. Patrick\'s Day Parade in March, the LGBT Pride March in June, the Greenwich Village Halloween Parade in October, and numerous parades commemorating the independence days of many nations. Ticker-tape parades celebrating championships won by sports teams as well as other heroic accomplishments march northward along the Canyon of Heroes on Broadway from Bowling Green to City Hall Park in Lower Manhattan.\n\n\n=== Accent and dialect ===\n\nThe New York area is home to a distinctive regional speech pattern called the New York dialect, alternatively known as Brooklynese or New Yorkese. It has generally been considered one of the most recognizable accents within American English.The traditional New York area accent is characterized as non-rhotic, so that the sound [ɹ] does not appear at the end of a syllable or immediately before a consonant; therefore the pronunciation of the city name as "New Yawk." There is no [ɹ] in words like park [pɑək] or [pɒək] (with vowel backed and diphthongized due to the low-back chain shift), butter [bʌɾə], or here [hiə]. In another feature called the low back chain shift, the [ɔ] vowel sound of words like talk, law, cross, chocolate, and coffee and the often homophonous [ɔr] in core and more are tensed and usually raised more than in General American English. In the most old-fashioned and extreme versions of the New York dialect, the vowel sounds of words like "girl" and of words like "oil" became a diphthong [ɜɪ]. This is often misperceived by speakers of other accents as a reversal of the er and oy sounds, so that girl is pronounced "goil" and oil is pronounced "erl"; this leads to the caricature of New Yorkers saying things like "Joizey" (Jersey), "Toidy-Toid Street" (33rd St.) and "terlet" (toilet). The character Archie Bunker from the 1970s television sitcom All in the Family was an example of having used this pattern of speech.\nThe classic version of the New York City dialect is generally centered on middle and working-class New Yorkers. The influx of non-European immigrants in recent decades has led to changes in this distinctive dialect, and the traditional form of this speech pattern is no longer as prevalent among general New Yorkers as it has been in the past.\n\n\n=== Sports ===\n\nNew York City is home to the headquarters of the National Football League, Major League Baseball, the National Basketball Association, the National Hockey League, and Major League Soccer. The New York metropolitan area hosts the most sports teams in the four major North American professional sports leagues with nine, one more than Los Angeles, and has 11 top-level professional sports teams if Major League Soccer is included, also one more than Los Angeles. Participation in professional sports in the city predates all professional leagues, and the city has been continuously hosting professional sports since the birth of the Brooklyn Dodgers in 1882. The city has played host to over forty major professional teams in the five sports and their respective competing leagues, both current and historic. Four of the ten most expensive stadiums ever built worldwide (MetLife Stadium, the new Yankee Stadium, Madison Square Garden, and Citi Field) are located in the New York metropolitan area. Madison Square Garden, its predecessor, the original Yankee Stadium and Ebbets Field, are sporting venues located in New York City, the latter two having been commemorated on U.S. postage stamps.\nNew York has been described as the "Capital of Baseball". There have been 35 Major League Baseball World Series and 73 pennants won by New York teams. It is one of only five metro areas (Los Angeles, Chicago, Baltimore–Washington, and the San Francisco Bay Area being the others) to have two baseball teams. Additionally, there have been 14 World Series in which two New York City teams played each other, known as a Subway Series and occurring most recently in 2000. No other metropolitan area has had this happen more than once (Chicago in 1906, St. Louis in 1944, and the San Francisco Bay Area in 1989). The city\'s two current Major League Baseball teams are the New York Mets, who play at Citi Field in Queens, and the New York Yankees, who play at Yankee Stadium in the Bronx. These teams compete in six games of interleague play every regular season that has also come to be called the Subway Series. The Yankees have won a record 27 championships, while the Mets have won the World Series twice. The city also was once home to the Brooklyn Dodgers (now the Los Angeles Dodgers), who won the World Series once, and the New York Giants (now the San Francisco Giants), who won the World Series five times. Both teams moved to California in 1958. There are also two Minor League Baseball teams in the city, the Brooklyn Cyclones and Staten Island Yankees.The city is represented in the National Football League by the New York Giants and the New York Jets, although both teams play their home games at MetLife Stadium in nearby East Rutherford, New Jersey, which hosted Super Bowl XLVIII in 2014.The metropolitan area is home to three National Hockey League teams. The New York Rangers, the traditional representative of the city itself and one of the league\'s Original Six, play at Madison Square Garden in Manhattan. The New York Islanders, traditionally representing Nassau and Suffolk Counties of Long Island, currently play at Barclays Center in Brooklyn and are planning a return to Nassau County by way of a new arena just outside the border with Queens at Belmont Park. The New Jersey Devils play at Prudential Center in nearby Newark, New Jersey and traditionally represent the counties of neighboring New Jersey which are coextensive with the boundaries of the New York metropolitan area and media market.\nThe city\'s National Basketball Association teams are the Brooklyn Nets, which played in and were named for New Jersey until 2012, and the New York Knicks, while the New York Liberty is the city\'s Women\'s National Basketball Association team. The first national college-level basketball championship, the National Invitation Tournament, was held in New York in 1938 and remains in the city. The city is well known for its links to basketball, which is played in nearly every park in the city by local youth, many of whom have gone on to play for major college programs and in the NBA.\nIn soccer, New York City is represented by New York City FC of Major League Soccer, who play their home games at Yankee Stadium and the New York Red Bulls, who play their home games at Red Bull Arena in nearby Harrison, New Jersey. Historically, the city is known for the New York Cosmos, the highly successful former professional soccer team which was the American home of Pelé. A new version of the New York Cosmos was formed in 2010, and began play in the second division North American Soccer League in 2013. The Cosmos play their home games at James M. Shuart Stadium on the campus of Hofstra University, just outside the New York City limits in Hempstead, New York.\nThe annual United States Open Tennis Championships is one of the world\'s four Grand Slam tennis tournaments and is held at the National Tennis Center in Flushing Meadows-Corona Park, Queens. The New York City Marathon, which courses through all five boroughs, is the world\'s largest running marathon, with 51,394 finishers in 2016 and 98,247 applicants for the 2017 race. The Millrose Games is an annual track and field meet whose featured event is the Wanamaker Mile. Boxing is also a prominent part of the city\'s sporting scene, with events like the Amateur Boxing Golden Gloves being held at Madison Square Garden each year. The city is also considered the host of the Belmont Stakes, the last, longest and oldest of horse racing\'s Triple Crown races, held just over the city\'s border at Belmont Park on the first or second Sunday of June. The city also hosted the 1932 U.S. Open golf tournament and the 1930 and 1939 PGA Championships, and has been host city for both events several times, most notably for nearby Winged Foot Golf Club. The Gaelic games are played in Riverdale, Bronx at Gaelic Park, home to the New York GAA, the only North American team to compete at the senior inter-county level.\n\n\n== Transportation ==\n\nNew York City\'s comprehensive transportation system is both complex and extensive.\n\n\n=== Rapid transit ===\nMass transit in New York City, most of which runs 24 hours a day, accounts for one in every three users of mass transit in the United States, and two-thirds of the nation\'s rail riders live in the New York City Metropolitan Area.\n\n\n==== Rail ====\nThe iconic New York City Subway system is the largest rapid transit system in the world when measured by stations in operation, with 472, and by length of routes. Nearly all of New York\'s subway system is open 24 hours a day, in contrast to the overnight shutdown common to systems in most cities, including Hong Kong, London, Paris, Seoul, and Tokyo. The New York City Subway is also the busiest metropolitan rail transit system in the Western Hemisphere, with 1.76 billion passenger rides in 2015, while Grand Central Terminal, also referred to as "Grand Central Station", is the world\'s largest railway station by number of train platforms.\n\nPublic transport is essential in New York City. 54.6% of New Yorkers commuted to work in 2005 using mass transit. This is in contrast to the rest of the United States, where 91% of commuters travel in automobiles to their workplace. According to the New York City Comptroller, workers in the New York City area spend an average of 6 hours and 18 minutes getting to work each week, the longest commute time in the nation among large cities. New York is the only US city in which a majority (52%) of households do not have a car; only 22% of Manhattanites own a car. Due to their high usage of mass transit, New Yorkers spend less of their household income on transportation than the national average, saving $19 billion annually on transportation compared to other urban Americans.New York City\'s commuter rail network is the largest in North America. The rail network, connecting New York City to its suburbs, consists of the Long Island Rail Road, Metro-North Railroad, and New Jersey Transit. The combined systems converge at Grand Central Terminal and Pennsylvania Station and contain more than 250 stations and 20 rail lines. In Queens, the elevated AirTrain people mover system connects JFK International Airport to the New York City Subway and the Long Island Rail Road; a separate AirTrain system is planned alongside the Grand Central Parkway to connect LaGuardia Airport to these transit systems. For intercity rail, New York City is served by Amtrak, whose busiest station by a significant margin is Pennsylvania Station on the West Side of Manhattan, from which Amtrak provides connections to Boston, Philadelphia, and Washington, D.C. along the Northeast Corridor, and long-distance train service to other North American cities.The Staten Island Railway rapid transit system solely serves Staten Island, operating 24 hours a day. The Port Authority Trans-Hudson (PATH train) links Midtown and Lower Manhattan to northeastern New Jersey, primarily Hoboken, Jersey City, and Newark. Like the New York City Subway, the PATH operates 24 hours a day; meaning three of the six rapid transit systems in the world which operate on 24-hour schedules are wholly or partly in New York (the others are a portion of the Chicago \'L\', the PATCO Speedline serving Philadelphia, and the Copenhagen Metro).\nMultibillion-dollar heavy rail transit projects under construction in New York City include the Second Avenue Subway, and the East Side Access project.\n\n\n==== Buses ====\n\nNew York City\'s public bus fleet runs 24/7 and is the largest in North America. The Port Authority Bus Terminal, the main intercity bus terminal of the city, serves 7,000 buses and 200,000 commuters daily, making it the busiest bus station in the world.\n\n\n=== Air ===\nNew York\'s airspace is the busiest in the United States and one of the world\'s busiest air transportation corridors. The three busiest airports in the New York metropolitan area include John F. Kennedy International Airport, Newark Liberty International Airport, and LaGuardia Airport; 130.5 million travelers used these three airports in 2016, and the city\'s airspace is the busiest in the nation. JFK and Newark Liberty were the busiest and fourth busiest U.S. gateways for international air passengers, respectively, in 2012; as of 2011, JFK was the busiest airport for international passengers in North America. Plans have advanced to expand passenger volume at a fourth airport, Stewart International Airport near Newburgh, New York, by the Port Authority of New York and New Jersey. Plans were announced in July 2015 to entirely rebuild LaGuardia Airport in a multibillion-dollar project to replace its aging facilities. Other commercial airports in or serving the New York metropolitan area include Long Island MacArthur Airport, Trenton–Mercer Airport and Westchester County Airport. The primary general aviation airport serving the area is Teterboro Airport.\n\n\n=== Ferries ===\n\nThe Staten Island Ferry is the world\'s busiest ferry route, carrying over 23 million passengers from July 2015 through June 2016 on the 5.2-mile (8.4 km) route between Staten Island and Lower Manhattan and running 24 hours a day. Other ferry systems shuttle commuters between Manhattan and other locales within the city and the metropolitan area.\nNYC Ferry, a NYCEDC initiative with routes planned to travel to all five boroughs, was launched in 2017, with second graders choosing the names of the ferries. Meanwhile, Seastreak ferry announced construction of a 600-passenger high-speed luxury ferry in September 2016, to shuttle riders between the Jersey Shore and Manhattan, anticipated to start service in 2017; this would be the largest vessel in its class.\n\n\n=== Taxis, transport startups, and trams ===\n\nOther features of the city\'s transportation infrastructure encompass more than 12,000 yellow taxicabs; various competing startup transportation network companies; and an aerial tramway that transports commuters between Roosevelt Island and Manhattan Island. Ride-sharing services have become significant competition for cab drivers in New York.\n\n\n=== Streets and highways ===\n\nDespite New York\'s heavy reliance on its vast public transit system, streets are a defining feature of the city. Manhattan\'s street grid plan greatly influenced the city\'s physical development. Several of the city\'s streets and avenues, like Broadway, Wall Street, Madison Avenue, and Seventh Avenue are also used as metonyms for national industries there: the theater, finance, advertising, and fashion organizations, respectively.\nNew York City also has an extensive web of expressways and parkways, which link the city\'s boroughs to each other and to northern New Jersey, Westchester County, Long Island, and southwestern Connecticut through various bridges and tunnels. Because these highways serve millions of outer borough and suburban residents who commute into Manhattan, it is quite common for motorists to be stranded for hours in traffic jams that are a daily occurrence, particularly during rush hour.New York City is also known for its rules regarding turning at red lights. Unlike the rest of the United States, New York State prohibits right or left turns on red in cities with a population greater than one million, to reduce traffic collisions and increase pedestrian safety. In New York City, therefore, all turns at red lights are illegal unless a sign permitting such maneuvers is present.\n\n\n==== River crossings ====\n\nNew York City is located on one of the world\'s largest natural harbors, and the boroughs of Manhattan and Staten Island are (primarily) coterminous with islands of the same names, while Queens and Brooklyn are located at the west end of the larger Long Island, and The Bronx is located at the southern tip of New York State\'s mainland. This situation of boroughs separated by water led to the development of an extensive infrastructure of well-known bridges and tunnels.\nThe George Washington Bridge is the world\'s busiest motor vehicle bridge, connecting Manhattan to Bergen County, New Jersey. The Verrazano-Narrows Bridge is the longest suspension bridge in the Americas and one of the world\'s longest. The Brooklyn Bridge is an icon of the city itself. The towers of the Brooklyn Bridge are built of limestone, granite, and Rosendale cement, and their architectural style is neo-Gothic, with characteristic pointed arches above the passageways through the stone towers. This bridge was also the longest suspension bridge in the world from its opening until 1903, and is the first steel-wire suspension bridge. The Queensboro Bridge is an important piece of cantilever architecture. The Manhattan Bridge, opened in 1909, is considered to be the forerunner of modern suspension bridges, and its design served as the model for many of the long-span suspension bridges around the world; the Manhattan Bridge, Throgs Neck Bridge, Triborough Bridge, and Verrazano-Narrows Bridge are all examples of Structural Expressionism.Manhattan Island is linked to New York City\'s outer boroughs and New Jersey by several tunnels as well. The Lincoln Tunnel, which carries 120,000 vehicles a day under the Hudson River between New Jersey and Midtown Manhattan, is the busiest vehicular tunnel in the world. The tunnel was built instead of a bridge to allow unfettered passage of large passenger and cargo ships that sailed through New York Harbor and up the Hudson River to Manhattan\'s piers. The Holland Tunnel, connecting Lower Manhattan to Jersey City, New Jersey, was the world\'s first mechanically ventilated vehicular tunnel when it opened in 1927. The Queens-Midtown Tunnel, built to relieve congestion on the bridges connecting Manhattan with Queens and Brooklyn, was the largest non-federal project in its time when it was completed in 1940. President Franklin D. Roosevelt was the first person to drive through it. The Brooklyn-Battery Tunnel (officially known as the Hugh L. Carey Tunnel) runs underneath Battery Park and connects the Financial District at the southern tip of Manhattan to Red Hook in Brooklyn.\n\n\n=== Cycling network ===\n\nCycling in New York City is associated with mixed cycling conditions that include dense urban proximities, relatively flat terrain, congested roadways with "stop-and-go" traffic, and streets with heavy pedestrian activity. The city\'s large cycling population includes utility cyclists, such as delivery and messenger services; cycling clubs for recreational cyclists; and increasingly commuters. Cycling is increasingly popular in New York City; in 2017 there were approximately 450,000 daily bike trips, compared with 170,000 daily bike trips in 2005. As of 2017, New York City had 1,333 miles of bike lanes, compared to 513 miles of bike lanes in 2006. As of 2019, there are 126 miles (203 km) of segregated or "protected" bike lanes citywide.\n\n\n== Environment ==\n\n\n=== Environmental impact reduction ===\nNew York City has focused on reducing its environmental impact and carbon footprint. Mass transit use in New York City is the highest in the United States. Also, by 2010, the city had 3,715 hybrid taxis and other clean diesel vehicles, representing around 28% of New York\'s taxi fleet in service, the most of any city in North America.New York\'s high rate of public transit use, over 200,000 daily cyclists as of 2014, and many pedestrian commuters make it the most energy-efficient major city in the United States. Walk and bicycle modes of travel account for 21% of all modes for trips in the city; nationally the rate for metro regions is about 8%. In both its 2011 and 2015 rankings, Walk Score named New York City the most walkable large city in the United States, and in 2018, Stacker ranked New York the most walkable U.S. city. Citibank sponsored the introduction of 10,000 public bicycles for the city\'s bike-share project in the summer of 2013. New York City\'s numerical "in-season cycling indicator" of bicycling in the city had hit an all-time high of 437 when measured in 2014.The city government was a petitioner in the landmark Massachusetts v. Environmental Protection Agency Supreme Court case forcing the EPA to regulate greenhouse gases as pollutants. The city is a leader in the construction of energy-efficient green office buildings, including the Hearst Tower among others. Mayor Bill de Blasio has committed to an 80% reduction in greenhouse gas emissions between 2014 and 2050 to reduce the city\'s contributions to climate change, beginning with a comprehensive "Green Buildings" plan.\n\n\n=== Water purity and availability ===\n\nNew York City is supplied with drinking water by the protected Catskill Mountains watershed. As a result of the watershed\'s integrity and undisturbed natural water filtration system, New York is one of only four major cities in the United States the majority of whose drinking water is pure enough not to require purification by water treatment plants. The city\'s municipal water system is the largest in the United States, moving over one billion gallons of water per day. The Croton Watershed north of the city is undergoing construction of a US$3.2 billion water purification plant to augment New York City\'s water supply by an estimated 290 million gallons daily, representing a greater than 20% addition to the city\'s current availability of water. The ongoing expansion of New York City Water Tunnel No. 3, an integral part of the New York City water supply system, is the largest capital construction project in the city\'s history, with segments serving Manhattan and The Bronx completed, and with segments serving Brooklyn and Queens planned for construction in 2020. In 2018, New York City announced a US$1 billion investment to protect the integrity of its water system and to maintain the purity of its unfiltered water supply.\n\n\n=== Air quality ===\nAccording to the 2016 World Health Organization Global Urban Ambient Air Pollution Database, the annual average concentration in New York City\'s air of particulate matter measuring 2.5 microns or less (PM2.5) was 7 micrograms per cubic meter, or 3 micrograms below the recommended limit of the WHO Air Quality Guidelines for the annual mean PM2.5. The New York City Department of Health and Mental Hygiene, in partnership with Queens College, conducts the New York Community Air Survey to measure pollutants at about 150 locations.\n\n\n=== Environmental revitalization ===\nNewtown Creek, a 3.5-mile (6-kilometer) a long estuary that forms part of the border between the boroughs of Brooklyn and Queens, has been designated a Superfund site for environmental clean-up and remediation of the waterway\'s recreational and economic resources for many communities. One of the most heavily used bodies of water in the Port of New York and New Jersey, it had been one of the most contaminated industrial sites in the country, containing years of discarded toxins, an estimated 30 million US gallons (110,000 m3) of spilled oil, including the Greenpoint oil spill, raw sewage from New York City\'s sewer system, and other accumulation.\n\n\n== Government and politics ==\n\n\n=== Government ===\n\nNew York City has been a metropolitan municipality with a mayor–council form of government since its consolidation in 1898. In New York City, the city government is responsible for public education, correctional institutions, public safety, recreational facilities, sanitation, water supply, and welfare services.\nThe Mayor and council members are elected to four-year terms. The City Council is a unicameral body consisting of 51 council members whose districts are defined by geographic population boundaries. Each term for the mayor and council members lasts four years and has a three consecutive-term limit, which is reset after a four-year break. The New York City Administrative Code, the New York City Rules, and the City Record are the code of local laws, compilation of regulations, and official journal, respectively.\n\nEach borough is coextensive with a judicial district of the state Unified Court System, of which the Criminal Court and the Civil Court are the local courts, while the New York Supreme Court conducts major trials and appeals. Manhattan hosts the First Department of the Supreme Court, Appellate Division while Brooklyn hosts the Second Department. There are also several extrajudicial administrative courts, which are executive agencies and not part of the state Unified Court System.\nUniquely among major American cities, New York is divided between, and is host to the main branches of, two different US district courts: the District Court for the Southern District of New York, whose main courthouse is on Foley Square near City Hall in Manhattan and whose jurisdiction includes Manhattan and the Bronx; and the District Court for the Eastern District of New York, whose main courthouse is in Brooklyn and whose jurisdiction includes Brooklyn, Queens, and Staten Island. The US Court of Appeals for the Second Circuit and US Court of International Trade are also based in New York, also on Foley Square in Manhattan.\n\n\n=== Politics ===\n\nThe present mayor is Bill de Blasio, the first Democrat since 1993. He was elected in 2013 with over 73% of the vote, and assumed office on January 1, 2014.\nThe Democratic Party holds the majority of public offices. As of April 2016, 69% of registered voters in the city are Democrats and 10% are Republicans. New York City has not been carried by a Republican in a statewide or presidential election since President Calvin Coolidge won the five boroughs in 1924. In 2012, Democrat Barack Obama became the first presidential candidate of any party to receive more than 80% of the overall vote in New York City, sweeping all five boroughs. Party platforms center on affordable housing, education, and economic development, and labor politics are of importance in the city.\nNew York is the most important source of political fundraising in the United States, as four of the top five ZIP Codes in the nation for political contributions are in Manhattan. The top ZIP Code, 10021 on the Upper East Side, generated the most money for the 2004 presidential campaigns of George W. Bush and John Kerry. The city has a strong imbalance of payments with the national and state governments. It receives 83 cents in services for every $1 it sends to the federal government in taxes (or annually sends $11.4 billion more than it receives back). City residents and businesses also sent an additional $4.1 billion in the 2009–2010 fiscal year to the state of New York than the city received in return.\n\n\n== Notable people ==\n\n\n== Global outreach ==\nIn 2006, the Sister City Program of the City of New York, Inc. was restructured and renamed New York City Global Partners. Through this program, New York City has expanded its international outreach to a network of cities worldwide, promoting the exchange of ideas and innovation between their citizenry and policymakers. New York\'s historic sister cities are denoted below by the year they joined New York City\'s partnership network.\n\n\n== See also ==\nOutline of New York City\n\n\n== Notes ==\n\n\n== References ==\n\n\n== Further reading ==\nBelden, E. Porter (1849). New York, Past, Present, and Future: Comprising a History of the City of New York, a Description of its Present Condition, and an Estimate of its Future Increase. New York: G.P. Putnam. From Google Books.\nBurgess, Anthony (1976). New York. New York: Little, Brown & Co. ISBN 978-90-6182-266-0.\nBurrows, Edwin G. & Wallace, Mike (1999), Gotham: A History of New York City to 1898, New York: Oxford University Press, ISBN 0-195-11634-8\nFederal Writers\' Project (1939). The WPA Guide to New York City (1995 reissue ed.). New York: The New Press. ISBN 978-1-56584-321-9.\nJackson, Kenneth T., ed. (1995), The Encyclopedia of New York City, New Haven: Yale University Press, ISBN 0300055366\nJackson, Kenneth T.; Dunbar, David S., eds. (2005). Empire City: New York Through the Centuries. Columbia University Press. ISBN 978-0-231-10909-3.\nLankevich, George L. (1998). American Metropolis: A History of New York City. NYU Press. ISBN 978-0-8147-5186-2.\nWhite, E.B. (1949). Here is New York (2000 reissue ed.). Little Bookroom.\nWhite, Norval & Willensky, Elliot (2000), AIA Guide to New York City (4th ed.), New York: Three Rivers Press, ISBN 978-0-8129-3107-5\nWhitehead, Colson (2003). The Colossus of New York: A City in 13 Parts. New York: Doubleday. ISBN 978-0-385-50794-3.\n\n\n== External links ==\nOfficial website\nNYC Go, official tourism website of New York City\nNew York City at Curlie\n Geographic data related to New York City at OpenStreetMap.\nCollections, 145,000 NYC photographs at the Museum of the City of New York\n"The New New York Skyline". National Geographic. November 2015. (Interactive.)' -print(ny.links[0]) # 10 Hudson Yards +print(ny) # +print(ny.title) # New York City +print(ny.url) # https://en.wikipedia.org/wiki/New_York_City +print( + repr(ny.content) +) # 'The City of New York, usually referred to as either New York City (NYC) or simply New York (NY), is the most populous city in the United States. With an estimated 2018 population of 8,398,748 distributed over a land area of about 302.6 square miles (784 km2), New York is also the most densely populated major city in the United States. Located at the southern tip of the state of New York, the city is the center of the New York metropolitan area, the largest metropolitan area in the world by urban landmass and one of the world\'s most populous megacities, with an estimated 19,979,477 people in its 2018 Metropolitan Statistical Area and 22,679,948 residents in its Combined Statistical Area. A global power city, New York City has been described as the cultural, financial, and media capital of the world, and exerts a significant impact upon commerce, entertainment, research, technology, education, politics, tourism, art, fashion, and sports. The city\'s fast pace has inspired the term New York minute. Home to the headquarters of the United Nations, New York is an important center for international diplomacy.Situated on one of the world\'s largest natural harbors, New York City consists of five boroughs, each of which is a separate county of the State of New York. The five boroughs – Brooklyn, Queens, Manhattan, The Bronx, and Staten Island – were consolidated into a single city in 1898. The city and its metropolitan area constitute the premier gateway for legal immigration to the United States. As many as 800 languages are spoken in New York, making it the most linguistically diverse city in the world. New York City is home to more than 3.2 million residents born outside the United States, the largest foreign-born population of any city in the world. As of 2019, the New York metropolitan area is estimated to produce a gross metropolitan product (GMP) of US$1.9 trillion. If greater New York City were a sovereign state, it would have the 12th highest GDP in the world. New York is home to the highest number of billionaires of any city in the world.New York City traces its origins to a trading post founded by colonists from the Dutch Republic in 1624 on Lower Manhattan; the post was named New Amsterdam in 1626. The city and its surroundings came under English control in 1664 and were renamed New York after King Charles II of England granted the lands to his brother, the Duke of York. New York was the capital of the United States from 1785 until 1790, and has been the largest US city since 1790. The Statue of Liberty greeted millions of immigrants as they came to the U.S. by ship in the late 19th and early 20th centuries and is an international symbol of the U.S. and its ideals of liberty and peace. In the 21st century, New York has emerged as a global node of creativity and entrepreneurship, social tolerance, and environmental sustainability, and as a symbol of freedom and cultural diversity. In 2019, New York was voted the greatest city in the world per a survey of over 30,000 people from 48 cities worldwide, citing its cultural diversity.Many districts and landmarks in New York City are well known, including three of the world\'s ten most visited tourist attractions in 2013; a record 62.8 million tourists visited in 2017. Several sources have ranked New York the most photographed city in the world. Times Square, iconic as the world\'s "heart" and "crossroads", is the brightly illuminated hub of the Broadway Theater District, one of the world\'s busiest pedestrian intersections, and a major center of the world\'s entertainment industry. The names of many of the city\'s landmarks, skyscrapers, and parks are known internationally. Manhattan\'s real estate market is among the most expensive in the world. New York is home to the largest ethnic Chinese population outside of Asia, with multiple distinct Chinatowns across the city. Providing continuous 24/7 service, the New York City Subway is the largest single-operator rapid transit system worldwide, with 472 rail stations. The city has over 120 colleges and universities, including Columbia University, New York University, and Rockefeller University, ranked among the top universities in the world. Anchored by Wall Street in the Financial District of Lower Manhattan, New York has been called both the most economically powerful city and world\'s leading financial center, and is home to the world\'s two largest stock exchanges by total market capitalization, the New York Stock Exchange and NASDAQ.\n\n\n== History ==\n\n\n=== Etymology ===\nIn 1664, the city was named in honor of the Duke of York, who would become King James II of England. James\'s older brother, King Charles II, had appointed the Duke proprietor of the former territory of New Netherland, including the city of New Amsterdam, which England had recently seized from the Dutch.\n\n\n=== Early history ===\nDuring the Wisconsin glaciation, 75,000 to 11,000 years ago, the New York City region was situated at the edge of a large ice sheet over 2,000 feet (610 m) in depth. The erosive forward movement of the ice (and its subsequent retreat) contributed to the separation of what is now Long Island and Staten Island. That action also left bedrock at a relatively shallow depth, providing a solid foundation for most of Manhattan\'s skyscrapers.In the precolonial era, the area of present-day New York City was inhabited by Algonquian Native Americans, including the Lenape. Their homeland, known as Lenapehoking, included Staten Island, Manhattan, the Bronx, the western portion of Long Island (including the areas that would later become the boroughs of Brooklyn and Queens), and the Lower Hudson Valley.The first documented visit into New York Harbor by a European was in 1524 by Giovanni da Verrazzano, a Florentine explorer in the service of the French crown. He claimed the area for France and named it Nouvelle Angoulême (New Angoulême). A Spanish expedition, led by the Portuguese captain Estêvão Gomes sailing for Emperor Charles V, arrived in New York Harbor in January 1525 and charted the mouth of the Hudson River, which he named Río de San Antonio (Saint Anthony\'s River). The Padrón Real of 1527, the first scientific map to show the East Coast of North America continuously, was informed by Gomes\' expedition and labeled the northeastern United States as Tierra de Esteban Gómez in his honor.\n\nIn 1609, the English explorer Henry Hudson rediscovered the New York Harbor while searching for the Northwest Passage to the Orient for the Dutch East India Company. He proceeded to sail up what the Dutch would name the North River (now the Hudson River), named first by Hudson as the Mauritius after Maurice, Prince of Orange. Hudson\'s first mate described the harbor as "a very good Harbour for all windes" and the river as "a mile broad" and "full of fish." Hudson sailed roughly 150 miles (240 km) north, past the site of the present-day New York State capital city of Albany, in the belief that it might be an oceanic tributary before the river became too shallow to continue. He made a ten-day exploration of the area and claimed the region for the Dutch East India Company. In 1614, the area between Cape Cod and Delaware Bay was claimed by the Netherlands and called Nieuw-Nederland (New Netherland).\nThe first non-Native American inhabitant of what would eventually become New York City was Juan Rodriguez (transliterated to Dutch as Jan Rodrigues), a merchant from Santo Domingo. Born in Santo Domingo of Portuguese and African descent, he arrived in Manhattan during the winter of 1613–14, trapping for pelts and trading with the local population as a representative of the Dutch. Broadway, from 159th Street to 218th Street in Upper Manhattan, is named Juan Rodriguez Way in his honor.\n\n\n=== Dutch rule ===\n\nA permanent European presence in New Netherland began in 1624 – making New York the 12th oldest continuously occupied European-established settlement in the continental United States – with the founding of a Dutch fur trading settlement on Governors Island. In 1625, construction was started on a citadel and Fort Amsterdam, later called Nieuw Amsterdam (New Amsterdam), on present-day Manhattan Island. The colony of New Amsterdam was centered at the site which would eventually become Lower Manhattan. It extended from the lower tip of Manhattan to modern day Wall Street,where a 12-foot wooden stockade was built in 1653 to protect against Native American and British Raids. In 1626, the Dutch colonial Director-General Peter Minuit, acting as charged by the Dutch West India Company, purchased the island of Manhattan from the Canarsie, a small Lenape band, for "the value of 60 guilders" (about $900 in 2018). A disproved legend claims that Manhattan was purchased for $24 worth of glass beads.Following the purchase, New Amsterdam grew slowly. To attract settlers, the Dutch instituted the patroon system in 1628, whereby wealthy Dutchmen (patroons, or patrons) who brought 50 colonists to New Netherland would be awarded swathes of land, along with local political autonomy and rights to participate in the lucrative fur trade. This program had little success.Since 1621, the Dutch West India Company had operated as a monopoly in New Netherland, on authority granted by the Dutch States General. In 1639–1640, in an effort to bolster economic growth, the Dutch West India Company relinquished its monopoly over the fur trade, leading to growth in the production and trade of food, timber, tobacco, and slaves (particularly with the Dutch West Indies).In 1647, Peter Stuyvesant began his tenure as the last Director-General of New Netherland. During his tenure, the population of New Netherland grew from 2,000 to 8,000. Stuyvesant has been credited with improving law and order in the colony; however, he also earned a reputation as a despotic leader. He instituted regulations on liquor sales, attempted to assert control over the Dutch Reformed Church, and blocked other religious groups (including Quakers, Jews, and Lutherans) from establishing houses of worship. The Dutch West India Company would eventually attempt to ease tensions between Stuyvesant and residents of New Amsterdam.\n\n\n=== English rule ===\n\nIn 1664, unable to summon any significant resistance, Stuyvesant surrendered New Amsterdam to English troops, led by Colonel Richard Nicolls, without bloodshed. The terms of the surrender permitted Dutch residents to remain in the colony and allowed for religious freedom. The English promptly renamed the fledgling city "New York" after the Duke of York (the future King James II of England). The transfer was confirmed in 1667 by the Treaty of Breda, which concluded the Second Anglo-Dutch War.On August 24, 1673, during the Third Anglo-Dutch War, Dutch captain Anthony Colve seized the colony of New York from England at the behest of Cornelis Evertsen the Youngest and rechristened it "New Orange" after William III, the Prince of Orange. The Dutch would soon return the island to England under the Treaty of Westminster of November 1674.Several intertribal wars among the Native Americans and some epidemics brought on by contact with the Europeans caused sizeable population losses for the Lenape between the years 1660 and 1670. By 1700, the Lenape population had diminished to 200. New York experienced several yellow fever epidemics in the 18th century, losing ten percent of its population to the disease in 1702 alone.New York grew in importance as a trading port while under British rule in the early 1700s. It also became a center of slavery, with 42% of households holding slaves by 1730, the highest percentage outside Charleston, South Carolina. Most slaveholders held a few or several domestic slaves, but others hired them out to work at labor. Slavery became integrally tied to New York\'s economy through the labor of slaves throughout the port, and the banks and shipping tied to the American South. Discovery of the African Burying Ground in the 1990s, during construction of a new federal courthouse near Foley Square, revealed that tens of thousands of Africans had been buried in the area in the colonial years.The 1735 trial and acquittal in Manhattan of John Peter Zenger, who had been accused of seditious libel after criticizing colonial governor William Cosby, helped to establish the freedom of the press in North America. In 1754, Columbia University was founded under charter by King George II as King\'s College in Lower Manhattan.\n\n\n=== American Revolution ===\n\nThe Stamp Act Congress met in New York in October 1765, as the Sons of Liberty, organized in the city, skirmished over the next ten years with British troops stationed there. The Battle of Long Island, the largest battle of the American Revolutionary War, was fought in August 1776 within the modern-day borough of Brooklyn. After the battle, in which the Americans were defeated, the British made the city their military and political base of operations in North America. The city was a haven for Loyalist refugees and escaped slaves who joined the British lines for freedom newly promised by the Crown for all fighters. As many as 10,000 escaped slaves crowded into the city during the British occupation. When the British forces evacuated at the close of the war in 1783, they transported 3,000 freedmen for resettlement in Nova Scotia. They resettled other freedmen in England and the Caribbean.\nThe only attempt at a peaceful solution to the war took place at the Conference House on Staten Island between American delegates, including Benjamin Franklin, and British general Lord Howe on September 11, 1776. Shortly after the British occupation began, the Great Fire of New York occurred, a large conflagration on the West Side of Lower Manhattan, which destroyed about a quarter of the buildings in the city, including Trinity Church.In 1785, the assembly of the Congress of the Confederation made New York City the national capital shortly after the war. New York was the last capital of the U.S. under the Articles of Confederation and the first capital under the Constitution of the United States. In 1789, the first President of the United States, George Washington, was inaugurated; the first United States Congress and the Supreme Court of the United States each assembled for the first time, and the United States Bill of Rights was drafted, all at Federal Hall on Wall Street. By 1790, New York had surpassed Philadelphia to become the largest city in the United States, but by the end of that year, pursuant to the Residence Act, the national capital was moved to Philadelphia.\n\n\n=== Nineteenth century ===\n\nUnder New York State\'s gradual abolition act of 1799, children of slave mothers were to be eventually liberated but to be held in indentured servitude until their mid-to-late twenties. Together with slaves freed by their masters after the Revolutionary War and escaped slaves, a significant free-black population gradually developed in Manhattan. Under such influential United States founders as Alexander Hamilton and John Jay, the New York Manumission Society worked for abolition and established the African Free School to educate black children. It was not until 1827 that slavery was completely abolished in the state, and free blacks struggled afterward with discrimination. New York interracial abolitionist activism continued; among its leaders were graduates of the African Free School. The city\'s black population reached more than 16,000 in 1840.In the 19th century, the city was transformed by development relating to its status as a national and international trading center, as well as by European immigration. The city adopted the Commissioners\' Plan of 1811, which expanded the city street grid to encompass almost all of Manhattan. The 1825 completion of the Erie Canal through central New York connected the Atlantic port to the agricultural markets and commodities of the North American interior via the Hudson River and the Great Lakes. Local politics became dominated by Tammany Hall, a political machine supported by Irish and German immigrants.Several prominent American literary figures lived in New York during the 1830s and 1840s, including William Cullen Bryant, Washington Irving, Herman Melville, Rufus Wilmot Griswold, John Keese, Nathaniel Parker Willis, and Edgar Allan Poe. Public-minded members of the contemporaneous business elite lobbied for the establishment of Central Park, which in 1857 became the first landscaped park in an American city.\n\nThe Great Irish Famine brought a large influx of Irish immigrants, of whom over 200,000 were living in New York by 1860, upwards of a quarter of the city\'s population. There was also extensive immigration from the German provinces, where revolutions had disrupted societies, and Germans comprised another 25% of New York\'s population by 1860.Democratic Party candidates were consistently elected to local office, increasing the city\'s ties to the South and its dominant party. In 1861, Mayor Fernando Wood called upon the aldermen to declare independence from Albany and the United States after the South seceded, but his proposal was not acted on. Anger at new military conscription laws during the American Civil War (1861–1865), which spared wealthier men who could afford to pay a $300 (equivalent to $6,105 in 2018) commutation fee to hire a substitute, led to the Draft Riots of 1863, whose most visible participants were ethnic Irish working class.The situation deteriorated into attacks on New York\'s elite, followed by attacks on Black New Yorkers and their property after fierce competition for a decade between Irish immigrants and black people for work. Rioters burned the Colored Orphan Asylum to the ground, with more than 200 children escaping harm due to efforts of the New York Police Department, which was mainly made up of Irish immigrants. At least 120 people were killed. Eleven Black men were lynched over five days, and the riots forced hundreds of Blacks to flee the city for Williamsburg, Brooklyn, and New Jersey. The black population in Manhattan fell below 10,000 by 1865, which it had last been in 1820. The white working class had established dominance. Violence by longshoremen against Black men was especially fierce in the docks area. It was one of the worst incidents of civil unrest in American history.\n\n\n=== Modern history ===\n\nIn 1898, the modern City of New York was formed with the consolidation of Brooklyn (until then a separate city), the County of New York (which then included parts of the Bronx), the County of Richmond, and the western portion of the County of Queens. The opening of the subway in 1904, first built as separate private systems, helped bind the new city together. Throughout the first half of the 20th century, the city became a world center for industry, commerce, and communication.In 1904, the steamship General Slocum caught fire in the East River, killing 1,021 people on board. In 1911, the Triangle Shirtwaist Factory fire, the city\'s worst industrial disaster, took the lives of 146 garment workers and spurred the growth of the International Ladies\' Garment Workers\' Union and major improvements in factory safety standards.\n\nNew York\'s non-white population was 36,620 in 1890. New York City was a prime destination in the early twentieth century for African Americans during the Great Migration from the American South, and by 1916, New York City had become home to the largest urban African diaspora in North America. The Harlem Renaissance of literary and cultural life flourished during the era of Prohibition. The larger economic boom generated construction of skyscrapers competing in height and creating an identifiable skyline.\nNew York became the most populous urbanized area in the world in the early 1920s, overtaking London. The metropolitan area surpassed the 10 million mark in the early 1930s, becoming the first megacity in human history. The difficult years of the Great Depression saw the election of reformer Fiorello La Guardia as mayor and the fall of Tammany Hall after eighty years of political dominance.Returning World War II veterans created a post-war economic boom and the development of large housing tracts in eastern Queens and Nassau County as well as similar suburban areas in New Jersey. New York emerged from the war unscathed as the leading city of the world, with Wall Street leading America\'s place as the world\'s dominant economic power. The United Nations Headquarters was completed in 1952, solidifying New York\'s global geopolitical influence, and the rise of abstract expressionism in the city precipitated New York\'s displacement of Paris as the center of the art world.\n\nThe Stonewall riots were a series of spontaneous, violent demonstrations by members of the gay community against a police raid that took place in the early morning hours of June 28, 1969, at the Stonewall Inn in the Greenwich Village neighborhood of Lower Manhattan. They are widely considered to constitute the single most important event leading to the gay liberation movement and the modern fight for LGBT rights. Wayne R. Dynes, author of the Encyclopedia of Homosexuality, wrote that drag queens were the only "transgender folks around" during the June 1969 Stonewall riots. "None of them in fact made a major contribution to the movement." Others say the transgender community in New York City played a significant role in fighting for LGBT equality during the period of the Stonewall riots and thereafter.In the 1970s, job losses due to industrial restructuring caused New York City to suffer from economic problems and rising crime rates. While a resurgence in the financial industry greatly improved the city\'s economic health in the 1980s, New York\'s crime rate continued to increase through that decade and into the beginning of the 1990s. By the mid 1990s, crime rates started to drop dramatically due to revised police strategies, improving economic opportunities, gentrification, and new residents, both American transplants and new immigrants from Asia and Latin America. Important new sectors, such as Silicon Alley, emerged in the city\'s economy. New York\'s population reached all-time highs in the 2000 Census and then again in the 2010 Census.\n\nNew York suffered the bulk of the economic damage and largest loss of human life in the aftermath of the September 11, 2001 attacks. Two of the four airliners highjacked that day were flown into the twin towers of the World Trade Center, destroying them and killing 2,192 civilians, 343 firefighters, and 71 law enforcement officers. The North Tower became the tallest building ever to be destroyed anywhere then or subsequently.The rebuilding of the area has created a new One World Trade Center, and a 9/11 memorial and museum along with other new buildings and infrastructure. The World Trade Center PATH station, which had opened on July 19, 1909 as the Hudson Terminal, was also destroyed in the attack. A temporary station was built and opened on November 23, 2003. An 800,000-square-foot (74,000 m2) permanent rail station designed by Santiago Calatrava, the World Trade Center Transportation Hub, the city\'s third-largest hub, was completed in 2016. The new One World Trade Center is the tallest skyscraper in the Western Hemisphere and the sixth-tallest building in the world by pinnacle height, with its spire reaching a symbolic 1,776 feet (541.3 m) in reference to the year of U.S. independence.The Occupy Wall Street protests in Zuccotti Park in the Financial District of Lower Manhattan began on September 17, 2011, receiving global attention and popularizing the Occupy movement against social and economic inequality worldwide.\n\n\n== Geography ==\n\nNew York City is situated in the Northeastern United States, in southeastern New York State, approximately halfway between Washington, D.C. and Boston. The location at the mouth of the Hudson River, which feeds into a naturally sheltered harbor and then into the Atlantic Ocean, has helped the city grow in significance as a trading port. Most of New York City is built on the three islands of Long Island, Manhattan, and Staten Island.\nThe Hudson River flows through the Hudson Valley into New York Bay. Between New York City and Troy, New York, the river is an estuary. The Hudson River separates the city from the U.S. state of New Jersey. The East River—a tidal strait—flows from Long Island Sound and separates the Bronx and Manhattan from Long Island. The Harlem River, another tidal strait between the East and Hudson Rivers, separates most of Manhattan from the Bronx. The Bronx River, which flows through the Bronx and Westchester County, is the only entirely fresh water river in the city.The city\'s land has been altered substantially by human intervention, with considerable land reclamation along the waterfronts since Dutch colonial times; reclamation is most prominent in Lower Manhattan, with developments such as Battery Park City in the 1970s and 1980s. Some of the natural relief in topography has been evened out, especially in Manhattan.The city\'s total area is 468.484 square miles (1,213.37 km2), including 302.643 sq mi (783.84 km2) of land and 165.841 sq mi (429.53 km2) of this is water.\nThe highest point in the city is Todt Hill on Staten Island, which, at 409.8 feet (124.9 m) above sea level, is the highest point on the Eastern Seaboard south of Maine. The summit of the ridge is mostly covered in woodlands as part of the Staten Island Greenbelt.\n\n\n=== Boroughs ===\n\n \n\nNew York City is often referred to collectively as the five boroughs, and in turn, there are hundreds of distinct neighborhoods throughout the boroughs, many with a definable history and character to call their own. If the boroughs were each independent cities, four of the boroughs (Brooklyn, Queens, Manhattan, and the Bronx) would be among the ten most populous cities in the United States (Staten Island would be ranked 37th) ; these same boroughs are coterminous with the four most densely populated counties in the United States (New York [Manhattan], Kings [Brooklyn], Bronx, and Queens).\n\nManhattan (New York County) is the geographically smallest and most densely populated borough, is home to Central Park and most of the city\'s skyscrapers, and may be locally known simply as The City. Manhattan\'s (New York County\'s) population density of 72,033 people per square mile (27,812/km²) in 2015 makes it the highest of any county in the United States and higher than the density of any individual American city. Manhattan is the cultural, administrative, and financial center of New York City and contains the headquarters of many major multinational corporations, the United Nations Headquarters, Wall Street, and a number of important universities. Manhattan is often described as the financial and cultural center of the world.Most of the borough is situated on Manhattan Island, at the mouth of the Hudson River. Several small islands also compose part of the borough of Manhattan, including Randall\'s Island, Wards Island, and Roosevelt Island in the East River, and Governors Island and Liberty Island to the south in New York Harbor. Manhattan Island is loosely divided into Lower, Midtown, and Uptown regions. Uptown Manhattan is divided by Central Park into the Upper East Side and the Upper West Side, and above the park is Harlem. The borough also includes a small neighborhood on the United States mainland, called Marble Hill, which is contiguous with The Bronx. New York City\'s remaining four boroughs are collectively referred to as the outer boroughs.\nBrooklyn (Kings County), on the western tip of Long Island, is the city\'s most populous borough. Brooklyn is known for its cultural, social, and ethnic diversity, an independent art scene, distinct neighborhoods, and a distinctive architectural heritage. Downtown Brooklyn is the largest central core neighborhood in the outer boroughs. The borough has a long beachfront shoreline including Coney Island, established in the 1870s as one of the earliest amusement grounds in the country. Marine Park and Prospect Park are the two largest parks in Brooklyn. Since 2010, Brooklyn has evolved into a thriving hub of entrepreneurship and high technology startup firms, and of postmodern art and design.\nQueens (Queens County), on Long Island north and east of Brooklyn, is geographically the largest borough, the most ethnically diverse county in the United States, and the most ethnically diverse urban area in the world. Historically a collection of small towns and villages founded by the Dutch, the borough has since developed both commercial and residential prominence. Downtown Flushing has become one of the busiest central core neighborhoods in the outer boroughs. Queens is the site of Citi Field, the baseball stadium of the New York Mets, and hosts the annual U.S. Open tennis tournament at Flushing Meadows-Corona Park. Additionally, two of the three busiest airports serving the New York metropolitan area, John F. Kennedy International Airport and LaGuardia Airport, are located in Queens. (The third is Newark Liberty International Airport in Newark, New Jersey.)\nThe Bronx (Bronx County) is New York City\'s northernmost borough and the only New York City borough with a majority of it a part of the mainland United States. It is the location of Yankee Stadium, the baseball park of the New York Yankees, and home to the largest cooperatively owned housing complex in the United States, Co-op City. It is also home to the Bronx Zoo, the world\'s largest metropolitan zoo, which spans 265 acres (1.07 km2) and houses over 6,000 animals. The Bronx is also the birthplace of rap and hip hop culture. Pelham Bay Park is the largest park in New York City, at 2,772 acres (1,122 ha).\nStaten Island (Richmond County) is the most suburban in character of the five boroughs. Staten Island is connected to Brooklyn by the Verrazano-Narrows Bridge, and to Manhattan by way of the free Staten Island Ferry, a daily commuter ferry which provides unobstructed views of the Statue of Liberty, Ellis Island, and Lower Manhattan. In central Staten Island, the Staten Island Greenbelt spans approximately 2,500 acres (10 km2), including 28 miles (45 km) of walking trails and one of the last undisturbed forests in the city. Designated in 1984 to protect the island\'s natural lands, the Greenbelt comprises seven city parks.\n\n\n=== Architecture ===\n\nNew York has architecturally noteworthy buildings in a wide range of styles and from distinct time periods, from the saltbox style Pieter Claesen Wyckoff House in Brooklyn, the oldest section of which dates to 1656, to the modern One World Trade Center, the skyscraper at Ground Zero in Lower Manhattan and the most expensive office tower in the world by construction cost.Manhattan\'s skyline, with its many skyscrapers, is universally recognized, and the city has been home to several of the tallest buildings in the world. As of 2019, New York City had 6,455 high-rise buildings, the third most in world after Hong Kong and Seoul. Of these, as of 2011, 550 completed structures were at least 330 feet (100 m) high, the second most in the world after Hong Kong, with over 50 completed skyscrapers taller than 656 feet (200 m). These include the Woolworth Building, an early example of Gothic Revival architecture in skyscraper design, built with massively scaled Gothic detailing; completed in 1913, for 17 years it was the world\'s tallest building.The 1916 Zoning Resolution required setbacks in new buildings and restricted towers to a percentage of the lot size, to allow sunlight to reach the streets below. The Art Deco style of the Chrysler Building (1930) and Empire State Building (1931), with their tapered tops and steel spires, reflected the zoning requirements. The buildings have distinctive ornamentation, such as the eagles at the corners of the 61st floor on the Chrysler Building, and are considered some of the finest examples of the Art Deco style. A highly influential example of the international style in the United States is the Seagram Building (1957), distinctive for its façade using visible bronze-toned I-beams to evoke the building\'s structure. The Condé Nast Building (2000) is a prominent example of green design in American skyscrapers and has received an award from the American Institute of Architects and AIA New York State for its design.\nThe character of New York\'s large residential districts is often defined by the elegant brownstone rowhouses and townhouses and shabby tenements that were built during a period of rapid expansion from 1870 to 1930. In contrast, New York City also has neighborhoods that are less densely populated and feature free-standing dwellings. In neighborhoods such as Riverdale (in the Bronx), Ditmas Park (in Brooklyn), and Douglaston (in Queens), large single-family homes are common in various architectural styles such as Tudor Revival and Victorian.Stone and brick became the city\'s building materials of choice after the construction of wood-frame houses was limited in the aftermath of the Great Fire of 1835. A distinctive feature of many of the city\'s buildings is the roof-mounted wooden water tower. In the 1800s, the city required their installation on buildings higher than six stories to prevent the need for excessively high water pressures at lower elevations, which could break municipal water pipes. Garden apartments became popular during the 1920s in outlying areas, such as Jackson Heights.According to the United States Geological Survey, an updated analysis of seismic hazard in July 2014 revealed a "slightly lower hazard for tall buildings" in New York City than previously assessed. Scientists estimated this lessened risk based upon a lower likelihood than previously thought of slow shaking near the city, which would be more likely to cause damage to taller structures from an earthquake in the vicinity of the city.\n\n\n=== Climate ===\n\nUnder the Köppen climate classification, using the 0 °C (32 °F) isotherm, New York City features a humid subtropical climate (Cfa), and is thus the northernmost major city on the North American continent with this categorization. The suburbs to the immediate north and west lie in the transitional zone between humid subtropical and humid continental climates (Dfa). For the Trewartha classification, it is defined as an oceanic climate (Do). Annually, the city averages 234 days with at least some sunshine. The city lies in the USDA 7b plant hardiness zone.Winters are cold and damp, and prevailing wind patterns that blow sea breezes offshore temper the moderating effects of the Atlantic Ocean; yet the Atlantic and the partial shielding from colder air by the Appalachian Mountains keep the city warmer in the winter than inland North American cities at similar or lesser latitudes such as Pittsburgh, Cincinnati, and Indianapolis. The daily mean temperature in January, the area\'s coldest month, is 32.6 °F (0.3 °C); temperatures usually drop to 10 °F (−12 °C) several times per winter, and reach 60 °F (16 °C) several days in the coldest winter month. Spring and autumn are unpredictable and can range from chilly to warm, although they are usually mild with low humidity. Summers are typically warm to hot and humid, with a daily mean temperature of 76.5 °F (24.7 °C) in July.Nighttime conditions are often exacerbated by the urban heat island phenomenon, while daytime temperatures exceed 90 °F (32 °C) on average of 17 days each summer and in some years exceed 100 °F (38 °C), although the last time this happened was July 23, 2011. Extreme temperatures have ranged from −15 °F (−26 °C), recorded on February 9, 1934, up to 106 °F (41 °C) on July 9, 1936; the coldest recorded wind chill was −37 °F (−38 °C) on the same day as the all-time record low. The record cold daily maximum was 2 °F (−17 °C) on December 30, 1917, while, conversely, the record warm daily minimum was 84 °F (29 °C), last recorded on July 22, 2011. The average water temperature of the nearby Atlantic Ocean ranges from 39.7 °F (4.3 °C) in February to 74.1 °F (23.4 °C) in August.The city receives 49.9 inches (1,270 mm) of precipitation annually, which is relatively evenly spread throughout the year. Average winter snowfall between 1981 and 2010 has been 25.8 inches (66 cm); this varies considerably between years. Hurricanes and tropical storms are rare in the New York area. Hurricane Sandy brought a destructive storm surge to New York City on the evening of October 29, 2012, flooding numerous streets, tunnels, and subway lines in Lower Manhattan and other areas of the city and cutting off electricity in many parts of the city and its suburbs. The storm and its profound impacts have prompted the discussion of constructing seawalls and other coastal barriers around the shorelines of the city and the metropolitan area to minimize the risk of destructive consequences from another such event in the future.The warmest month on record is July 1999, with a mean temperature of 81.4 °F (27.4 °C). The coldest month was February 1934, with a mean temperature of 19.9 °F (−6.7 °C). The warmest year on record is 2012, with a mean temperature of 57.4 °F (14.1 °C). The coldest year was 1888, with a mean temperature of 49.3 °F (9.6 °C). The driest month on record is June 1949, with 0.02 inches (0.51 mm) of rainfall. The wettest month was August 2011, with 18.95 inches (481 mm) of rainfall. The driest year on record is 1965, with 26.09 inches (663 mm) of rainfall. The wettest year was 1983, with 80.56 inches (2,046 mm) of rainfall. The snowiest month on record is February 2010, with 36.9 inches (94 cm) of snowfall. The snowiest season (Jul–Jun) on record is 1995–1996, with 75.6 inches (192 cm) of snowfall. The least snowy season was 1972–1973, with 2.3 inches (5.8 cm) of snowfall.\n\nSee or edit raw graph data.\n\n\n=== Parks ===\n\nThe City of New York has a complex park system, with various lands operated by the National Park Service, the New York State Office of Parks, Recreation and Historic Preservation, and the New York City Department of Parks and Recreation.\nIn its 2018 ParkScore ranking, The Trust for Public Land reported that the park system in New York City was the ninth-best park system among the fifty most populous U.S. cities. ParkScore ranks urban park systems by a formula that analyzes median park size, park acres as percent of city area, the percent of city residents within a half-mile of a park, spending of park services per resident, and the number of playgrounds per 10,000 residents.\n\n\n==== National parks ====\n\nGateway National Recreation Area contains over 26,000 acres (10,521.83 ha) in total, most of it surrounded by New York City, including the Jamaica Bay Wildlife Refuge. In Brooklyn and Queens, the park contains over 9,000 acres (36 km2) of salt marsh, wetlands, islands, and water, including most of Jamaica Bay. Also in Queens, the park includes a significant portion of the western Rockaway Peninsula, most notably Jacob Riis Park and Fort Tilden. In Staten Island, Gateway National Recreation Area includes Fort Wadsworth, with historic pre-Civil War era Battery Weed and Fort Tompkins, and Great Kills Park, with beaches, trails, and a marina.\nThe Statue of Liberty National Monument and Ellis Island Immigration Museum are managed by the National Park Service and are in both the states of New York and New Jersey. They are joined in the harbor by Governors Island National Monument, in New York. Historic sites under federal management on Manhattan Island include Castle Clinton National Monument; Federal Hall National Memorial; Theodore Roosevelt Birthplace National Historic Site; General Grant National Memorial ("Grant\'s Tomb"); African Burial Ground National Monument; and Hamilton Grange National Memorial. Hundreds of private properties are listed on the National Register of Historic Places or as a National Historic Landmark such as, for example, the Stonewall Inn, part of the Stonewall National Monument in Greenwich Village, as the catalyst of the modern gay rights movement.\n\n\n==== State parks ====\n\nThere are seven state parks within the confines of New York City, including Clay Pit Ponds State Park Preserve, a natural area that includes extensive riding trails, and Riverbank State Park, a 28-acre (11 ha) facility that rises 69 feet (21 m) over the Hudson River.\n\n\n==== City parks ====\n\nNew York City has over 28,000 acres (110 km2) of municipal parkland and 14 miles (23 km) of public beaches. The largest municipal park in the city is Pelham Bay Park in the Bronx, with 2,772 acres (1,122 ha).\nCentral Park, an 843-acre (3.41 km2) park in middle-upper Manhattan, is the most visited urban park in the United States and one of the most filmed locations in the world, with 40 million visitors in 2013. The park contains a wide range of attractions; there are several lakes and ponds, two ice-skating rinks, the Central Park Zoo, the Central Park Conservatory Garden, and the 106-acre (0.43 km2) Jackie Onassis Reservoir. Indoor attractions include Belvedere Castle with its nature center, the Swedish Cottage Marionette Theater, and the historic Carousel. On October 23, 2012, hedge fund manager John A. Paulson announced a $100 million gift to the Central Park Conservancy, the largest ever monetary donation to New York City\'s park system.\nWashington Square Park is a prominent landmark in the Greenwich Village neighborhood of Lower Manhattan. The Washington Square Arch at the northern gateway to the park is an iconic symbol of both New York University and Greenwich Village.\nProspect Park in Brooklyn has a 90-acre (36 ha) meadow, a lake, and extensive woodlands. Within the park is the historic Battle Pass, prominent in the Battle of Long Island.\nFlushing Meadows–Corona Park in Queens, with its 897 acres (363 ha) making it the city\'s fourth largest park, was the setting for the 1939 World\'s Fair and the 1964 World\'s Fair and is host to the USTA Billie Jean King National Tennis Center and the annual United States Open Tennis Championships tournament.\nOver a fifth of the Bronx\'s area, 7,000 acres (28 km2), is given over to open space and parks, including Pelham Bay Park, Van Cortlandt Park, the Bronx Zoo, and the New York Botanical Gardens.\nIn Staten Island, the Conference House Park contains the historic Conference House, site of the only attempt of a peaceful resolution to the American Revolution which was conducted in September 1775, attended by Benjamin Franklin representing the Americans and Lord Howe representing the British Crown. The historic Burial Ridge, the largest Native American burial ground within New York City, is within the park.\n\n\n=== Military installations ===\nNew York City is home to Fort Hamilton, the U.S. military\'s only active duty installation within the city. The Brooklyn facility was established in 1825 on the site of a small battery utilized during the American Revolution, and it is one of America\'s longest serving military forts. Today Fort Hamilton serves as the headquarters of the North Atlantic Division of the United States Army Corps of Engineers and for the New York City Recruiting Battalion. It also houses the 1179th Transportation Brigade, the 722nd Aeromedical Staging Squadron, and a military entrance processing station. Other formerly active military reservations still utilized for National Guard and military training or reserve operations in the city include Fort Wadsworth in Staten Island and Fort Totten in Queens.\n\n\n== Demographics ==\n\nNew York City is the most populous city in the United States, with an estimated 8,398,748 residents as of July 2018, incorporating more immigration into the city than outmigration since the 2010 United States Census. More than twice as many people live in New York City as compared to Los Angeles, the second-most populous U.S. city, and within a smaller area. New York City gained more residents between April 2010 and July 2014 (316,000) than any other U.S. city. New York City\'s population is about 43% of New York State\'s population and about 36% of the population of the New York metropolitan area.\n\n\n=== Population density ===\nIn 2017, the city had an estimated population density of 28,491 inhabitants per square mile (11,000/km2), rendering it the most densely populated of all municipalities housing over 100,000 residents in the United States, with several small cities (of fewer than 100,000) in adjacent Hudson County, New Jersey having greater density, as per the 2010 Census. Geographically co-extensive with New York County, the borough of Manhattan\'s 2017 population density of 72,918 inhabitants per square mile (28,154/km2) makes it the highest of any county in the United States and higher than the density of any individual American city.\n\n\n=== Race and ethnicity ===\n\nThe city\'s population in 2010 was 44% white (33.3% non-Hispanic white), 25.5% black (23% non-Hispanic black), 0.7% Native American, and 12.7% Asian. Hispanics of any race represented 28.6% of the population, while Asians constituted the fastest-growing segment of the city\'s population between 2000 and 2010; the non-Hispanic white population declined 3 percent, the smallest recorded decline in decades; and for the first time since the Civil War, the number of blacks declined over a decade.\nThroughout its history, New York has been a major port of entry for immigrants into the United States. More than 12 million European immigrants were received at Ellis Island between 1892 and 1924. The term "melting pot" was first coined to describe densely populated immigrant neighborhoods on the Lower East Side. By 1900, Germans constituted the largest immigrant group, followed by the Irish, Jews, and Italians. In 1940, whites represented 92% of the city\'s population.Approximately 37% of the city\'s population is foreign born, and more than half of all children are born to mothers who are immigrants. In New York, no single country or region of origin dominates. The ten largest sources of foreign-born individuals in the city as of 2011 were the Dominican Republic, China, Mexico, Guyana, Jamaica, Ecuador, Haiti, India, Russia, and Trinidad and Tobago, while the Bangladeshi-born immigrant population has become one of the fastest growing in the city, counting over 74,000 by 2011.\n\nAsian Americans in New York City, according to the 2010 Census, number more than one million, greater than the combined totals of San Francisco and Los Angeles. New York contains the highest total Asian population of any U.S. city proper. The New York City borough of Queens is home to the state\'s largest Asian American population and the largest Andean (Colombian, Ecuadorian, Peruvian, and Bolivian) populations in the United States, and is also the most ethnically diverse urban area in the world.The Chinese population constitutes the fastest-growing nationality in New York State; multiple satellites of the original Manhattan Chinatown, in Brooklyn, and around Flushing, Queens, are thriving as traditionally urban enclaves – while also expanding rapidly eastward into suburban Nassau County on Long Island, as the New York metropolitan region and New York State have become the top destinations for new Chinese immigrants, respectively, and large-scale Chinese immigration continues into New York City and surrounding areas, with the largest metropolitan Chinese diaspora outside Asia, including an estimated 812,410 individuals in 2015.In 2012, 6.3% of New York City was of Chinese ethnicity, with nearly three-fourths living in either Queens or Brooklyn, geographically on Long Island. A community numbering 20,000 Korean-Chinese (Chaoxianzu or Joseonjok) is centered in Flushing, Queens, while New York City is also home to the largest Tibetan population outside China, India, and Nepal, also centered in Queens. Koreans made up 1.2% of the city\'s population, and Japanese 0.3%. Filipinos were the largest Southeast Asian ethnic group at 0.8%, followed by Vietnamese, who made up 0.2% of New York City\'s population in 2010. Indians are the largest South Asian group, comprising 2.4% of the city\'s population, with Bangladeshis and Pakistanis at 0.7% and 0.5%, respectively. Queens is the preferred borough of settlement for Asian Indians, Koreans, Filipinos, and Malaysians and other Southeast Asians; while Brooklyn is receiving large numbers of both West Indian and Asian Indian immigrants.\n\nNew York City has the largest European and non-Hispanic white population of any American city. At 2.7 million in 2012, New York\'s non-Hispanic white population is larger than the non-Hispanic white populations of Los Angeles (1.1 million), Chicago (865,000), and Houston (550,000) combined. The non-Hispanic white population was 6.6 million in 1940. The non-Hispanic white population has begun to increase since 2010.The European diaspora residing in the city is very diverse. According to 2012 Census estimates, there were roughly 560,000 Italian Americans, 385,000 Irish Americans, 253,000 German Americans, 223,000 Russian Americans, 201,000 Polish Americans, and 137,000 English Americans. Additionally, Greek and French Americans numbered 65,000 each, with those of Hungarian descent estimated at 60,000 people. Ukrainian and Scottish Americans numbered 55,000 and 35,000, respectively. People identifying ancestry from Spain numbered 30,838 total in 2010.People of Norwegian and Swedish descent both stood at about 20,000 each, while people of Czech, Lithuanian, Portuguese, Scotch-Irish, and Welsh descent all numbered between 12,000–14,000 people. Arab Americans number over 160,000 in New York City, with the highest concentration in Brooklyn. Central Asians, primarily Uzbek Americans, are a rapidly growing segment of the city\'s non-Hispanic white population, enumerating over 30,000, and including over half of all Central Asian immigrants to the United States, most settling in Queens or Brooklyn. Albanian Americans are most highly concentrated in the Bronx.The wider New York City metropolitan statistical area, with over 20 million people, about 50% greater than the second-place Los Angeles metropolitan area in the United States, is also ethnically diverse, with the largest foreign-born population of any metropolitan region in the world. The New York region continues to be by far the leading metropolitan gateway for legal immigrants admitted into the United States, substantially exceeding the combined totals of Los Angeles and Miami. It is home to the largest Jewish and Israeli communities outside Israel, with the Jewish population in the region numbering over 1.5 million in 2012 and including many diverse Jewish sects, predominantly from around the Middle East and Eastern Europe, and including a rapidly growing Orthodox Jewish population, the largest outside Israel.The metropolitan area is also home to 20% of the USA\'s Indian Americans and at least 20 Little India enclaves, and 15% of all Korean Americans and four Koreatowns; the largest Asian Indian population in the Western Hemisphere; the largest Russian American, Italian American, and African American populations; the largest Dominican American, Puerto Rican American, and South American and second-largest overall Hispanic population in the United States, numbering 4.8 million; and includes multiple established Chinatowns within New York City alone.Ecuador, Colombia, Guyana, Peru, and Brazil were the top source countries from South America for legal immigrants to the New York City region in 2013; the Dominican Republic, Jamaica, Haiti, and Trinidad and Tobago in the Caribbean; Egypt, Ghana, and Nigeria from Africa; and El Salvador, Honduras, and Guatemala in Central America. Amidst a resurgence of Puerto Rican migration to New York City, this population had increased to approximately 1.3 million in the metropolitan area as of 2013.\nSince 2010, a Little Australia has emerged and is growing rapidly representing the Australasian presence in Nolita, Manhattan. In 2011, there were an estimated 20,000 Australian residents of New York City, nearly quadruple the 5,537 in 2005. Qantas Airways of Australia and Air New Zealand have been exploring the possibilities of long-haul flights from New York to Sydney and Auckland, respectively, which would both rank among the longest non-stop flights in the world. A Little Sri Lanka has developed in the Tompkinsville neighborhood of Staten Island.\n\n\n=== Sexual orientation and gender identity ===\n\nThe New York metropolitan area is home to a prominent self-identifying gay and bisexual community estimated at nearly 570,000 individuals, the largest in the United States and one of the world\'s largest. Same-sex marriages in New York were legalized on June 24, 2011 and were authorized to take place beginning 30 days thereafter. Charles Kaiser, author of The Gay Metropolis: The Landmark History of Gay Life in America, wrote that in the era after World War II, "New York City became the literal gay metropolis for hundreds of thousands of immigrants from within and without the United States: the place they chose to learn how to live openly, honestly and without shame."The annual New York City Pride March (or gay pride parade) traverses southward down Fifth Avenue and ends at Greenwich Village in Lower Manhattan; the parade rivals the Sao Paulo Gay Pride Parade as the largest pride parade in the world, attracting tens of thousands of participants and millions of sidewalk spectators each June. The annual Queens Pride Parade is held in Jackson Heights and is accompanied by the ensuing Multicultural Parade. Stonewall 50 – WorldPride NYC 2019 was the largest international Pride celebration in history, produced by Heritage of Pride and enhanced through a partnership with the I ❤ NY program\'s LGBT division, commemorating the 50th anniversary of the Stonewall uprising, with 150,000 participants and five million spectators attending in Manhattan alone. New York City is also home to the largest transgender population in the world, estimated at more than 50,000 in 2018, concentrated in Manhattan and Queens; however, until the June 1969 Stonewall riots, this community had felt marginalized and neglected by the gay community.\n\n\n=== Religion ===\nChristianity (59%) — made up of Roman Catholicism (33%), Protestantism (23%), and other Christians (3%) — is the most prevalent religion in New York, as of 2014. It is followed by Judaism, with approximately 1.1 million adherents, over half of whom live in Brooklyn. The Jewish population makes up 18.4% of the city. Islam ranks third in New York City, with estimates ranging between 600,000 and 1,000,000 observers, including 10% of the city\'s public school children. These three largest groups are followed by Hinduism, Buddhism, and a variety of other religions, as well as atheism. In 2014, 24% of New Yorkers self-identified with no organized religious affiliation.\n\n\n=== Wealth and income disparity ===\nNew York City has a high degree of income disparity as indicated by its Gini Coefficient of 0.5 for the city overall and 0.6 for Manhattan, as of 2006. (This is not unusual, as all large cities have greater income disparities than the nation overall.) In the first quarter of 2014, the average weekly wage in New York County (Manhattan) was $2,749, representing the highest total among large counties in the United States. As of 2017, New York City was home to the highest number of billionaires of any city in the world at 103, including former Mayor Michael Bloomberg. New York also had the highest density of millionaires per capita among major U.S. cities in 2014, at 4.6% of residents. New York City is one of the relatively few American cities levying an income tax (currently about 3%) on its residents. As of 2018, there were 78,676 homeless people in New York City.\n\n\n== Economy ==\n\n\n=== City economic overview ===\nNew York City is a global hub of business and commerce, as a center for banking and finance, retailing, world trade, transportation, tourism, real estate, new media, traditional media, advertising, legal services, accountancy, insurance, theater, fashion, and the arts in the United States; while Silicon Alley, metonymous for New York\'s broad-spectrum high technology sphere, continues to expand. The Port of New York and New Jersey is also a major economic engine, handling record cargo volume in 2017, over 6.7 million TEUs. New York City\'s unemployment rate fell to its record low of 4.0% in September 2018.Many Fortune 500 corporations are headquartered in New York City, as are a large number of multinational corporations. One out of ten private sector jobs in the city is with a foreign company. New York City has been ranked first among cities across the globe in attracting capital, business, and tourists. New York City\'s role as the top global center for the advertising industry is metonymously reflected as "Madison Avenue". The city\'s fashion industry provides approximately 180,000 employees with $11 billion in annual wages.Other important sectors include medical research and technology, non-profit institutions, and universities. Manufacturing accounts for a significant but declining share of employment, although the city\'s garment industry is showing a resurgence in Brooklyn. Food processing is a US$5 billion industry that employs more than 19,000 residents.\nChocolate is New York City\'s leading specialty-food export, with up to US$234 million worth of exports each year. Entrepreneurs were forming a "Chocolate District" in Brooklyn as of 2014, while Godiva, one of the world\'s largest chocolatiers, continues to be headquartered in Manhattan.\n\n\n=== Wall Street ===\n\nNew York City\'s most important economic sector lies in its role as the headquarters for the U.S. financial industry, metonymously known as Wall Street. The city\'s securities industry, enumerating 163,400 jobs in August 2013, continues to form the largest segment of the city\'s financial sector and an important economic engine, accounting in 2012 for 5 percent of the city\'s private sector jobs, 8.5 percent (US$3.8 billion) of its tax revenue, and 22 percent of the city\'s total wages, including an average salary of US$360,700. Many large financial companies are headquartered in New York City, and the city is also home to a burgeoning number of financial startup companies.\nLower Manhattan is home to the New York Stock Exchange, on Wall Street, and the NASDAQ, at 165 Broadway, representing the world\'s largest and second largest stock exchanges, respectively, when measured both by overall average daily trading volume and by total market capitalization of their listed companies in 2013. Investment banking fees on Wall Street totaled approximately $40 billion in 2012, while in 2013, senior New York City bank officers who manage risk and compliance functions earned as much as $324,000 annually. In fiscal year 2013–14, Wall Street\'s securities industry generated 19% of New York State\'s tax revenue.New York City remains the largest global center for trading in public equity and debt capital markets, driven in part by the size and financial development of the U.S. economy. New York also leads in hedge fund management; private equity; and the monetary volume of mergers and acquisitions. Several investment banks and investment managers headquartered in Manhattan are important participants in other global financial centers. New York is also the principal commercial banking center of the United States.Many of the world\'s largest media conglomerates are also based in the city. Manhattan contained over 500 million square feet (46.5 million m2) of office space in 2018, making it the largest office market in the United States, while Midtown Manhattan, with 400 million square feet (37.2 million m2) in 2018, is the largest central business district in the world.\n\n\n=== Tech and biotech ===\n\nSilicon Alley, centered in Manhattan, has evolved into a metonym for the sphere encompassing the New York City metropolitan region\'s high technology industries involving the Internet, new media, telecommunications, digital media, software development, game design, financial technology ("FinTech"), and other fields within information technology that are supported by its entrepreneurship ecosystem and venture capital investments. In 2015, Silicon Alley generated over US$7.3 billion in venture capital investment across a broad spectrum of high technology enterprises, most based in Manhattan, with others in Brooklyn, Queens, and elsewhere in the region.\nHigh technology startup companies and employment are growing in New York City and the region, bolstered by the city\'s position in North America as the leading Internet hub and telecommunications center, including its vicinity to several transatlantic fiber optic trunk lines, New York\'s intellectual capital, and its extensive outdoor wireless connectivity. Verizon Communications, headquartered at 140 West Street in Lower Manhattan, was at the final stages in 2014 of completing a US$3 billion fiberoptic telecommunications upgrade throughout New York City. As of 2014, New York City hosted 300,000 employees in the tech sector. The technology sector has been claiming a greater share of New York City\'s economy since 2010. Tech:NYC, founded in 2016, is a non-profit organization which represents New York City\'s technology industry with government, civic institutions, in business, and in the media, and whose primary goals are to further augment New York\'s substantial tech talent base and to advocate for policies that will nurture tech companies to grow in the city.The biotechnology sector is also growing in New York City, based upon the city\'s strength in academic scientific research and public and commercial financial support. On December 19, 2011, then Mayor Michael R. Bloomberg announced his choice of Cornell University and Technion-Israel Institute of Technology to build a US$2 billion graduate school of applied sciences called Cornell Tech on Roosevelt Island with the goal of transforming New York City into the world\'s premier technology capital. By mid-2014, Accelerator, a biotech investment firm, had raised more than US$30 million from investors, including Eli Lilly and Company, Pfizer, and Johnson & Johnson, for initial funding to create biotechnology startups at the Alexandria Center for Life Science, which encompasses more than 700,000 square feet (65,000 m2) on East 29th Street and promotes collaboration among scientists and entrepreneurs at the center and with nearby academic, medical, and research institutions. The New York City Economic Development Corporation\'s Early Stage Life Sciences Funding Initiative and venture capital partners, including Celgene, General Electric Ventures, and Eli Lilly, committed a minimum of US$100 million to help launch 15 to 20 ventures in life sciences and biotechnology.\n\n\n=== Real estate ===\n\nReal estate is a major force in the city\'s economy, as the total value of all New York City property was assessed at US$1.072 trillion for the 2017 fiscal year, an increase of 10.6% from the previous year, with 89% of the increase coming from market effects. The Time Warner Center is the property with the highest-listed market value in the city, at US$1.1 billion in 2006. New York City is home to some of the nation\'s—and the world\'s—most valuable real estate. 450 Park Avenue was sold on July 2, 2007 for US$510 million, about $1,589 per square foot ($17,104/m²), breaking the barely month-old record for an American office building of $1,476 per square foot ($15,887/m²) set in the June 2007 sale of 660 Madison Avenue.In 2014 Manhattan was home to six of the top ten ZIP Codes in the United States by median housing price. Fifth Avenue in Midtown Manhattan commands the highest retail rents in the world, at US$3,000 per square foot ($32,000/m2) in 2017. In 2019, the most expensive home sale ever in the United States achieved completion in Manhattan, at a selling price of US$238 million, for a 24,000 square feet (2,200 m2) penthouse apartment overlooking Central Park.\n\n\n=== Tourism ===\n\nTourism is a vital industry for New York City, which has witnessed a growing combined volume of international and domestic tourists, receiving an eighth consecutive annual record of approximately 62.8 million visitors in 2017. Tourism had generated an all-time high US$61.3 billion in overall economic impact for New York City in 2014, pending 2015 statistics. Approximately 12 million visitors to New York City were from outside the United States, with the highest numbers from the United Kingdom, Canada, Brazil, and China.\nI Love New York (stylized I ❤ NY) is both a logo and a song that are the basis of an advertising campaign and have been used since 1977 to promote tourism in New York City, and later to promote New York State as well. The trademarked logo, owned by New York State Empire State Development, appears in souvenir shops and brochures throughout the city and state, some licensed, many not. The song is the state song of New York.\nMajor tourist destinations include Times Square; Broadway theater productions; the Empire State Building; the Statue of Liberty; Ellis Island; the United Nations Headquarters; museums such as the Metropolitan Museum of Art; greenspaces such as Central Park and Washington Square Park; Rockefeller Center; the Manhattan Chinatown; luxury shopping along Fifth and Madison Avenues; and events such as the Halloween Parade in Greenwich Village; the Macy\'s Thanksgiving Day Parade; the lighting of the Rockefeller Center Christmas Tree; the St. Patrick\'s Day parade; seasonal activities such as ice skating in Central Park in the wintertime; the Tribeca Film Festival; and free performances in Central Park at Summerstage. Major attractions in the boroughs outside Manhattan include Flushing Meadows-Corona Park and the Unisphere in Queens; the Bronx Zoo; Coney Island, Brooklyn; and the New York Botanical Garden in the Bronx. The New York Wheel, a 630-foot ferris wheel, was under construction at the northern shore of Staten Island in 2015, overlooking the Statue of Liberty, New York Harbor, and the Lower Manhattan skyline.Manhattan was on track to have an estimated 90,000 hotel rooms at the end of 2014, a 10% increase from 2013. In October 2014, the Anbang Insurance Group, based in China, purchased the Waldorf Astoria New York for US$1.95 billion, making it the world\'s most expensive hotel ever sold.\n\n\n=== Media and entertainment ===\n\nNew York is a prominent location for the American entertainment industry, with many films, television series, books, and other media being set there. As of 2012, New York City was the second largest center for filmmaking and television production in the United States, producing about 200 feature films annually, employing 130,000 individuals. The filmed entertainment industry has been growing in New York, contributing nearly US$9 billion to the New York City economy alone as of 2015. By volume, New York is the world leader in independent film production – one-third of all American independent films are produced in New York City. The Association of Independent Commercial Producers is also based in New York. In the first five months of 2014 alone, location filming for television pilots in New York City exceeded the record production levels for all of 2013, with New York surpassing Los Angeles as the top North American city for the same distinction during the 2013–2014 cycle.New York City is also a center for the advertising, music, newspaper, digital media, and publishing industries and is also the largest media market in North America. Some of the city\'s media conglomerates and institutions include Time Warner, the Thomson Reuters Corporation, the Associated Press, Bloomberg L.P., the News Corporation, The New York Times Company, NBCUniversal, the Hearst Corporation, AOL, and Viacom. Seven of the world\'s top eight global advertising agency networks have their headquarters in New York. Two of the top three record labels\' headquarters are in New York: Sony Music Entertainment and Warner Music Group. Universal Music Group also has offices in New York. New media enterprises are contributing an increasingly important component to the city\'s central role in the media sphere.\nMore than 200 newspapers and 350 consumer magazines have an office in the city, and the publishing industry employs about 25,000 people. Two of the three national daily newspapers with the largest circulations in the United States are published in New York: The Wall Street Journal and The New York Times, which has won the most Pulitzer Prizes for journalism. Major tabloid newspapers in the city include The New York Daily News, which was founded in 1919 by Joseph Medill Patterson, and The New York Post, founded in 1801 by Alexander Hamilton. The city also has a comprehensive ethnic press, with 270 newspapers and magazines published in more than 40 languages. El Diario La Prensa is New York\'s largest Spanish-language daily and the oldest in the nation. The New York Amsterdam News, published in Harlem, is a prominent African American newspaper. The Village Voice, historically the largest alternative newspaper in the United States, announced in 2017 that it would cease publication of its print edition and convert to a fully digital venture.The television and radio industry developed in New York and is a significant employer in the city\'s economy. The three major American broadcast networks are all headquartered in New York: ABC, CBS, and NBC. Many cable networks are based in the city as well, including MTV, Fox News, HBO, Showtime, Bravo, Food Network, AMC, and Comedy Central. The City of New York operates a public broadcast service, NYC Media, that has produced several original Emmy Award-winning shows covering music and culture in city neighborhoods and city government. WBAI, with news and information programming, is one of the few socialist radio stations operating in the United States.\nNew York is also a major center for non-commercial educational media. The oldest public-access television channel in the United States is the Manhattan Neighborhood Network, founded in 1971. WNET is the city\'s major public television station and a primary source of national Public Broadcasting Service (PBS) television programming. WNYC, a public radio station owned by the city until 1997, has the largest public radio audience in the United States.\n\n\n== Education and scholarly activity ==\n\n\n=== Primary and secondary education ===\nThe New York City Public Schools system, managed by the New York City Department of Education, is the largest public school system in the United States, serving about 1.1 million students in more than 1,700 separate primary and secondary schools. The city\'s public school system includes nine specialized high schools to serve academically and artistically gifted students. The city government pays the Pelham Public Schools to educate a very small, detached section of the Bronx.\n\nThe New York City Charter School Center assists the setup of new charter schools. There are approximately 900 additional privately run secular and religious schools in the city.\n\n\n=== Higher education and research ===\nOver 600,000 students are enrolled in New York City\'s over 120 higher education institutions, the highest number of any city in the United States and higher than other major global cities like London and Tokyo, including over half million in the City University of New York (CUNY) system alone in 2014. New York City\'s higher education institutions had also higher average scores than those two cities in 2019, according to the Academic Ranking of World Universities. New York City is home to such notable private universities as Barnard College, Columbia University, Cooper Union, Fordham University, New York University, New York Institute of Technology, Rockefeller University, and Yeshiva University; several of these universities are ranked among the top universities in the world.The public CUNY system is one of the largest universities in the nation, comprising 24 institutions across all five boroughs: senior colleges, community colleges, and other graduate/professional schools. The public State University of New York (SUNY) system includes campuses in New York City, including: Downstate Health Sciences University, Fashion Institute of Technology, Maritime College, and the College of Optometry. The city also hosts other smaller private colleges and universities, including many religious and special-purpose institutions, such as: St. John\'s University, The Juilliard School, Manhattan College, The College of Mount Saint Vincent, Parsons School of Design, The New School, Pratt Institute, New York Film Academy, The School of Visual Arts, The King\'s College, and Wagner College.\nMuch of the scientific research in the city is done in medicine and the life sciences. New York City has the most postgraduate life sciences degrees awarded annually in the United States, with 127 Nobel laureates having roots in local institutions as of 2005; while in 2012, 43,523 licensed physicians were practicing in New York City. Major biomedical research institutions include Memorial Sloan–Kettering Cancer Center, Rockefeller University, SUNY Downstate Medical Center, Albert Einstein College of Medicine, Mount Sinai School of Medicine, and Weill Cornell Medical College, being joined by the Cornell University/Technion-Israel Institute of Technology venture on Roosevelt Island. The graduates of SUNY Maritime College in the Bronx earned the highest average annual salary of any university graduates in the United States, US$144,000 as of 2017.\n\n\n== Human resources ==\n\n\n=== Public health ===\n\nThe New York City Health and Hospitals Corporation (HHC) operates the public hospitals and clinics in New York City. A public benefit corporation with $6.7 billion in annual revenues, HHC is the largest municipal healthcare system in the United States serving 1.4 million patients, including more than 475,000 uninsured city residents. HHC was created in 1969 by the New York State Legislature as a public benefit corporation (Chapter 1016 of the Laws 1969). HHC operates 11 acute care hospitals, five nursing homes, six diagnostic and treatment centers, and more than 70 community-based primary care sites, serving primarily the poor and working class. HHC\'s MetroPlus Health Plan is one of the New York area\'s largest providers of government-sponsored health insurance and is the plan of choice for nearly half million New Yorkers.HHC\'s facilities annually provide millions of New Yorkers services interpreted in more than 190 languages. The most well-known hospital in the HHC system is Bellevue Hospital, the oldest public hospital in the United States. Bellevue is the designated hospital for treatment of the President of the United States and other world leaders if they become sick or injured while in New York City. The president of HHC is Ramanathan Raju, MD, a surgeon and former CEO of the Cook County health system in Illinois. In August 2017, Mayor Bill de Blasio signed legislation outlawing pharmacies from selling cigarettes once their existing licenses to do so expired, beginning in 2018.\n\n\n=== Public safety ===\n\n\n==== Police and law enforcement ====\n\nThe New York Police Department (NYPD) has been the largest police force in the United States by a significant margin, with over 35,000 sworn officers. Members of the NYPD are frequently referred to by politicians, the media, and their own police cars by the nickname, New York\'s Finest.\nCrime has continued an overall downward trend in New York City since the 1990s. In 2012, the NYPD came under scrutiny for its use of a stop-and-frisk program, which has undergone several policy revisions since then. In 2014, New York City had the third lowest murder rate among the largest U.S. cities, having become significantly safer after a spike in crime in the 1970s through 1990s. Violent crime in New York City decreased more than 75% from 1993 to 2005, and continued decreasing during periods when the nation as a whole saw increases. By 2002, New York City\'s crime rate was similar to that of Provo, Utah, and was ranked 197th in crime among the 216 U.S. cities with populations greater than 100,000. In 1992, the city recorded 2,245 murders. In 2005, the homicide rate was at its lowest level since 1966, and in 2009, the city recorded fewer than 461 homicides for the first time ever since crime statistics were first published in 1963. In 2017, 60.1% of violent crime suspects were Black, 29.6% Hispanic, 6.5% White, 3.6% Asian and 0.2% American Indian. New York City experienced 292 homicides in 2017,Sociologists and criminologists have not reached consensus on the explanation for the dramatic decrease in the city\'s crime rate. Some attribute the phenomenon to new tactics used by the NYPD, including its use of CompStat and the broken windows theory. Others cite the end of the crack epidemic and demographic changes, including from immigration. Another theory is that widespread exposure to lead pollution from automobile exhaust, which can lower intelligence and increase aggression levels, incited the initial crime wave in the mid-20th century, most acutely affecting heavily trafficked cities like New York. A strong correlation was found demonstrating that violent crime rates in New York and other big cities began to fall after lead was removed from American gasoline in the 1970s. Another theory cited to explain New York City\'s falling homicide rate is the inverse correlation between the number of murders and the increasingly wetter climate in the city.Organized crime has long been associated with New York City, beginning with the Forty Thieves and the Roach Guards in the Five Points in the 1820s. The 20th century saw a rise in the Mafia, dominated by the Five Families, as well as in gangs, including the Black Spades. The Mafia and gang presence has declined in the city in the 21st century.\n\n\n==== Firefighting ====\n\nThe Fire Department of New York (FDNY), provides fire protection, technical rescue, primary response to biological, chemical, and radioactive hazards, and emergency medical services for the five boroughs of New York City. The FDNY is the largest municipal fire department in the United States and the second largest in the world after the Tokyo Fire Department. The FDNY employs approximately 11,080 uniformed firefighters and over 3,300 uniformed EMTs and paramedics. The FDNY\'s motto is New York\'s Bravest.\nThe fire department faces multifaceted firefighting challenges in many ways unique to New York. In addition to responding to building types that range from wood-frame single family homes to high-rise structures, there are many secluded bridges and tunnels, as well as large parks and wooded areas that can give rise to brush fires. New York is also home to one of the largest subway systems in the world, consisting of hundreds of miles of tunnel with electrified track.\nThe FDNY headquarters is located at 9 MetroTech Center in Downtown Brooklyn, and the FDNY Fire Academy is located on Randalls Island. There are three Bureau of Fire Communications alarm offices which receive and dispatch alarms to appropriate units. One office, at 11 Metrotech Center in Brooklyn, houses Manhattan/Citywide, Brooklyn, and Staten Island Fire Communications; the Bronx and Queens offices are in separate buildings.\n\n\n=== Public library system ===\n\nThe New York Public Library, which has the largest collection of any public library system in the United States, serves Manhattan, the Bronx, and Staten Island. Queens is served by the Queens Borough Public Library, the nation\'s second largest public library system, while the Brooklyn Public Library serves Brooklyn.\n\n\n== Culture and contemporary life ==\n\nNew York City has been described as the cultural capital of the world by the diplomatic consulates of Iceland and Latvia and by New York\'s Baruch College. A book containing a series of essays titled New York, Culture Capital of the World, 1940–1965 has also been published as showcased by the National Library of Australia. In describing New York, author Tom Wolfe said, "Culture just seems to be in the air, like part of the weather."Numerous major American cultural movements began in the city, such as the Harlem Renaissance, which established the African-American literary canon in the United States. The city was a center of jazz in the 1940s, abstract expressionism in the 1950s, and the birthplace of hip hop in the 1970s. The city\'s punk and hardcore scenes were influential in the 1970s and 1980s. New York has long had a flourishing scene for Jewish American literature.\nThe city is the birthplace of many cultural movements, including the Harlem Renaissance in literature and visual art; abstract expressionism (also known as the New York School) in painting; and hip hop, punk, salsa, freestyle, Tin Pan Alley, certain forms of jazz, and (along with Philadelphia) disco in music. New York City has been considered the dance capital of the world. The city is also frequently the setting for novels, movies (see List of films set in New York City), and television programs. New York Fashion Week is one of the world\'s preeminent fashion events and is afforded extensive coverage by the media.\nNew York has also frequently been ranked the top fashion capital of the world on the annual list compiled by the Global Language Monitor.\n\n\n=== Pace ===\nOne of the most common traits attributed to New York City is its fast pace, which spawned the term New York minute. Journalist Walt Whitman characterized New York\'s streets as being traversed by "hurrying, feverish, electric crowds".\n\n\n=== Arts ===\nNew York City has more than 2,000 arts and cultural organizations and more than 500 art galleries of all sizes. The city government funds the arts with a larger annual budget than the National Endowment for the Arts. Wealthy business magnates in the 19th century built a network of major cultural institutions, such as the famed Carnegie Hall and the Metropolitan Museum of Art, that would become internationally established. The advent of electric lighting led to elaborate theater productions, and in the 1880s, New York City theaters on Broadway and along 42nd Street began featuring a new stage form that became known as the Broadway musical. Strongly influenced by the city\'s immigrants, productions such as those of Harrigan and Hart, George M. Cohan, and others used song in narratives that often reflected themes of hope and ambition. New York City itself is the subject or background of many plays and musicals.\n\n\n==== Performing arts ====\n\nBroadway theatre is one of the premier forms of English-language theatre in the world, named after Broadway, the major thoroughfare that crosses Times Square, also sometimes referred to as "The Great White Way". Forty-one venues in Midtown Manhattan\'s Theatre District, each with at least 500 seats, are classified as Broadway theatres. According to The Broadway League, Broadway shows sold approximately US$1.27 billion worth of tickets in the 2013–2014 season, an 11.4% increase from US$1.139 billion in the 2012–2013 season. Attendance in 2013–2014 stood at 12.21 million, representing a 5.5% increase from the 2012–2013 season\'s 11.57 million. Performance artists displaying diverse skills are ubiquitous on the streets of Manhattan.\nLincoln Center for the Performing Arts, anchoring Lincoln Square on the Upper West Side of Manhattan, is home to numerous influential arts organizations, including the Metropolitan Opera, New York City Opera, New York Philharmonic, and New York City Ballet, as well as the Vivian Beaumont Theater, the Juilliard School, Jazz at Lincoln Center, and Alice Tully Hall. The Lee Strasberg Theatre and Film Institute is in Union Square, and Tisch School of the Arts is based at New York University, while Central Park SummerStage presents free music concerts in Central Park.\n\n\n==== Visual arts ====\n\nNew York City is home to hundreds of cultural institutions and historic sites, many of which are internationally known. Museum Mile is the name for a section of Fifth Avenue running from 82nd to 105th streets on the Upper East Side of Manhattan, in an area sometimes called Upper Carnegie Hill. The Mile, which contains one of the densest displays of culture in the world, is actually three blocks longer than one mile (1.6 km). Ten museums occupy the length of this section of Fifth Avenue. The tenth museum, the Museum for African Art, joined the ensemble in 2009, although its museum at 110th Street, the first new museum constructed on the Mile since the Guggenheim in 1959, opened in late 2012. In addition to other programming, the museums collaborate for the annual Museum Mile Festival, held each year in June, to promote the museums and increase visitation. Many of the world\'s most lucrative art auctions are held in New York City.\n\n\n=== Cuisine ===\n\nNew York City\'s food culture includes an array of international cuisines influenced by the city\'s immigrant history. Central and Eastern European immigrants, especially Jewish immigrants from those regions, brought bagels, cheesecake, hot dogs, knishes, and delicatessens (or delis) to the city. Italian immigrants brought New York-style pizza and Italian cuisine into the city, while Jewish immigrants and Irish immigrants brought pastrami and corned beef, respectively. Chinese and other Asian restaurants, sandwich joints, trattorias, diners, and coffeehouses are ubiquitous throughout the city. Some 4,000 mobile food vendors licensed by the city, many immigrant-owned, have made Middle Eastern foods such as falafel and kebabs examples of modern New York street food. The city is home to "nearly one thousand of the finest and most diverse haute cuisine restaurants in the world", according to Michelin. The New York City Department of Health and Mental Hygiene assigns letter grades to the city\'s restaurants based upon their inspection results. As of 2019, there were 27,043 restaurants in the city, up from 24,865 in 2017. The Queens Night Market in Flushing Meadows–Corona Park attracts over 10,000 people nightly to sample food from over 85 countries.\n\n\n=== Parades ===\n\nNew York City is well known for its street parades, which celebrate a broad array of themes, including holidays, nationalities, human rights, and major league sports team championship victories. The majority of parades are held in Manhattan. The primary orientation of the annual street parades is typically from north to south, marching along major avenues. The annual Macy\'s Thanksgiving Day Parade is the world\'s largest parade, beginning alongside Central Park and processing southward to the flagship Macy\'s Herald Square store; the parade is viewed on telecasts worldwide and draws millions of spectators in person. Other notable parades including the annual St. Patrick\'s Day Parade in March, the LGBT Pride March in June, the Greenwich Village Halloween Parade in October, and numerous parades commemorating the independence days of many nations. Ticker-tape parades celebrating championships won by sports teams as well as other heroic accomplishments march northward along the Canyon of Heroes on Broadway from Bowling Green to City Hall Park in Lower Manhattan.\n\n\n=== Accent and dialect ===\n\nThe New York area is home to a distinctive regional speech pattern called the New York dialect, alternatively known as Brooklynese or New Yorkese. It has generally been considered one of the most recognizable accents within American English.The traditional New York area accent is characterized as non-rhotic, so that the sound [ɹ] does not appear at the end of a syllable or immediately before a consonant; therefore the pronunciation of the city name as "New Yawk." There is no [ɹ] in words like park [pɑək] or [pɒək] (with vowel backed and diphthongized due to the low-back chain shift), butter [bʌɾə], or here [hiə]. In another feature called the low back chain shift, the [ɔ] vowel sound of words like talk, law, cross, chocolate, and coffee and the often homophonous [ɔr] in core and more are tensed and usually raised more than in General American English. In the most old-fashioned and extreme versions of the New York dialect, the vowel sounds of words like "girl" and of words like "oil" became a diphthong [ɜɪ]. This is often misperceived by speakers of other accents as a reversal of the er and oy sounds, so that girl is pronounced "goil" and oil is pronounced "erl"; this leads to the caricature of New Yorkers saying things like "Joizey" (Jersey), "Toidy-Toid Street" (33rd St.) and "terlet" (toilet). The character Archie Bunker from the 1970s television sitcom All in the Family was an example of having used this pattern of speech.\nThe classic version of the New York City dialect is generally centered on middle and working-class New Yorkers. The influx of non-European immigrants in recent decades has led to changes in this distinctive dialect, and the traditional form of this speech pattern is no longer as prevalent among general New Yorkers as it has been in the past.\n\n\n=== Sports ===\n\nNew York City is home to the headquarters of the National Football League, Major League Baseball, the National Basketball Association, the National Hockey League, and Major League Soccer. The New York metropolitan area hosts the most sports teams in the four major North American professional sports leagues with nine, one more than Los Angeles, and has 11 top-level professional sports teams if Major League Soccer is included, also one more than Los Angeles. Participation in professional sports in the city predates all professional leagues, and the city has been continuously hosting professional sports since the birth of the Brooklyn Dodgers in 1882. The city has played host to over forty major professional teams in the five sports and their respective competing leagues, both current and historic. Four of the ten most expensive stadiums ever built worldwide (MetLife Stadium, the new Yankee Stadium, Madison Square Garden, and Citi Field) are located in the New York metropolitan area. Madison Square Garden, its predecessor, the original Yankee Stadium and Ebbets Field, are sporting venues located in New York City, the latter two having been commemorated on U.S. postage stamps.\nNew York has been described as the "Capital of Baseball". There have been 35 Major League Baseball World Series and 73 pennants won by New York teams. It is one of only five metro areas (Los Angeles, Chicago, Baltimore–Washington, and the San Francisco Bay Area being the others) to have two baseball teams. Additionally, there have been 14 World Series in which two New York City teams played each other, known as a Subway Series and occurring most recently in 2000. No other metropolitan area has had this happen more than once (Chicago in 1906, St. Louis in 1944, and the San Francisco Bay Area in 1989). The city\'s two current Major League Baseball teams are the New York Mets, who play at Citi Field in Queens, and the New York Yankees, who play at Yankee Stadium in the Bronx. These teams compete in six games of interleague play every regular season that has also come to be called the Subway Series. The Yankees have won a record 27 championships, while the Mets have won the World Series twice. The city also was once home to the Brooklyn Dodgers (now the Los Angeles Dodgers), who won the World Series once, and the New York Giants (now the San Francisco Giants), who won the World Series five times. Both teams moved to California in 1958. There are also two Minor League Baseball teams in the city, the Brooklyn Cyclones and Staten Island Yankees.The city is represented in the National Football League by the New York Giants and the New York Jets, although both teams play their home games at MetLife Stadium in nearby East Rutherford, New Jersey, which hosted Super Bowl XLVIII in 2014.The metropolitan area is home to three National Hockey League teams. The New York Rangers, the traditional representative of the city itself and one of the league\'s Original Six, play at Madison Square Garden in Manhattan. The New York Islanders, traditionally representing Nassau and Suffolk Counties of Long Island, currently play at Barclays Center in Brooklyn and are planning a return to Nassau County by way of a new arena just outside the border with Queens at Belmont Park. The New Jersey Devils play at Prudential Center in nearby Newark, New Jersey and traditionally represent the counties of neighboring New Jersey which are coextensive with the boundaries of the New York metropolitan area and media market.\nThe city\'s National Basketball Association teams are the Brooklyn Nets, which played in and were named for New Jersey until 2012, and the New York Knicks, while the New York Liberty is the city\'s Women\'s National Basketball Association team. The first national college-level basketball championship, the National Invitation Tournament, was held in New York in 1938 and remains in the city. The city is well known for its links to basketball, which is played in nearly every park in the city by local youth, many of whom have gone on to play for major college programs and in the NBA.\nIn soccer, New York City is represented by New York City FC of Major League Soccer, who play their home games at Yankee Stadium and the New York Red Bulls, who play their home games at Red Bull Arena in nearby Harrison, New Jersey. Historically, the city is known for the New York Cosmos, the highly successful former professional soccer team which was the American home of Pelé. A new version of the New York Cosmos was formed in 2010, and began play in the second division North American Soccer League in 2013. The Cosmos play their home games at James M. Shuart Stadium on the campus of Hofstra University, just outside the New York City limits in Hempstead, New York.\nThe annual United States Open Tennis Championships is one of the world\'s four Grand Slam tennis tournaments and is held at the National Tennis Center in Flushing Meadows-Corona Park, Queens. The New York City Marathon, which courses through all five boroughs, is the world\'s largest running marathon, with 51,394 finishers in 2016 and 98,247 applicants for the 2017 race. The Millrose Games is an annual track and field meet whose featured event is the Wanamaker Mile. Boxing is also a prominent part of the city\'s sporting scene, with events like the Amateur Boxing Golden Gloves being held at Madison Square Garden each year. The city is also considered the host of the Belmont Stakes, the last, longest and oldest of horse racing\'s Triple Crown races, held just over the city\'s border at Belmont Park on the first or second Sunday of June. The city also hosted the 1932 U.S. Open golf tournament and the 1930 and 1939 PGA Championships, and has been host city for both events several times, most notably for nearby Winged Foot Golf Club. The Gaelic games are played in Riverdale, Bronx at Gaelic Park, home to the New York GAA, the only North American team to compete at the senior inter-county level.\n\n\n== Transportation ==\n\nNew York City\'s comprehensive transportation system is both complex and extensive.\n\n\n=== Rapid transit ===\nMass transit in New York City, most of which runs 24 hours a day, accounts for one in every three users of mass transit in the United States, and two-thirds of the nation\'s rail riders live in the New York City Metropolitan Area.\n\n\n==== Rail ====\nThe iconic New York City Subway system is the largest rapid transit system in the world when measured by stations in operation, with 472, and by length of routes. Nearly all of New York\'s subway system is open 24 hours a day, in contrast to the overnight shutdown common to systems in most cities, including Hong Kong, London, Paris, Seoul, and Tokyo. The New York City Subway is also the busiest metropolitan rail transit system in the Western Hemisphere, with 1.76 billion passenger rides in 2015, while Grand Central Terminal, also referred to as "Grand Central Station", is the world\'s largest railway station by number of train platforms.\n\nPublic transport is essential in New York City. 54.6% of New Yorkers commuted to work in 2005 using mass transit. This is in contrast to the rest of the United States, where 91% of commuters travel in automobiles to their workplace. According to the New York City Comptroller, workers in the New York City area spend an average of 6 hours and 18 minutes getting to work each week, the longest commute time in the nation among large cities. New York is the only US city in which a majority (52%) of households do not have a car; only 22% of Manhattanites own a car. Due to their high usage of mass transit, New Yorkers spend less of their household income on transportation than the national average, saving $19 billion annually on transportation compared to other urban Americans.New York City\'s commuter rail network is the largest in North America. The rail network, connecting New York City to its suburbs, consists of the Long Island Rail Road, Metro-North Railroad, and New Jersey Transit. The combined systems converge at Grand Central Terminal and Pennsylvania Station and contain more than 250 stations and 20 rail lines. In Queens, the elevated AirTrain people mover system connects JFK International Airport to the New York City Subway and the Long Island Rail Road; a separate AirTrain system is planned alongside the Grand Central Parkway to connect LaGuardia Airport to these transit systems. For intercity rail, New York City is served by Amtrak, whose busiest station by a significant margin is Pennsylvania Station on the West Side of Manhattan, from which Amtrak provides connections to Boston, Philadelphia, and Washington, D.C. along the Northeast Corridor, and long-distance train service to other North American cities.The Staten Island Railway rapid transit system solely serves Staten Island, operating 24 hours a day. The Port Authority Trans-Hudson (PATH train) links Midtown and Lower Manhattan to northeastern New Jersey, primarily Hoboken, Jersey City, and Newark. Like the New York City Subway, the PATH operates 24 hours a day; meaning three of the six rapid transit systems in the world which operate on 24-hour schedules are wholly or partly in New York (the others are a portion of the Chicago \'L\', the PATCO Speedline serving Philadelphia, and the Copenhagen Metro).\nMultibillion-dollar heavy rail transit projects under construction in New York City include the Second Avenue Subway, and the East Side Access project.\n\n\n==== Buses ====\n\nNew York City\'s public bus fleet runs 24/7 and is the largest in North America. The Port Authority Bus Terminal, the main intercity bus terminal of the city, serves 7,000 buses and 200,000 commuters daily, making it the busiest bus station in the world.\n\n\n=== Air ===\nNew York\'s airspace is the busiest in the United States and one of the world\'s busiest air transportation corridors. The three busiest airports in the New York metropolitan area include John F. Kennedy International Airport, Newark Liberty International Airport, and LaGuardia Airport; 130.5 million travelers used these three airports in 2016, and the city\'s airspace is the busiest in the nation. JFK and Newark Liberty were the busiest and fourth busiest U.S. gateways for international air passengers, respectively, in 2012; as of 2011, JFK was the busiest airport for international passengers in North America. Plans have advanced to expand passenger volume at a fourth airport, Stewart International Airport near Newburgh, New York, by the Port Authority of New York and New Jersey. Plans were announced in July 2015 to entirely rebuild LaGuardia Airport in a multibillion-dollar project to replace its aging facilities. Other commercial airports in or serving the New York metropolitan area include Long Island MacArthur Airport, Trenton–Mercer Airport and Westchester County Airport. The primary general aviation airport serving the area is Teterboro Airport.\n\n\n=== Ferries ===\n\nThe Staten Island Ferry is the world\'s busiest ferry route, carrying over 23 million passengers from July 2015 through June 2016 on the 5.2-mile (8.4 km) route between Staten Island and Lower Manhattan and running 24 hours a day. Other ferry systems shuttle commuters between Manhattan and other locales within the city and the metropolitan area.\nNYC Ferry, a NYCEDC initiative with routes planned to travel to all five boroughs, was launched in 2017, with second graders choosing the names of the ferries. Meanwhile, Seastreak ferry announced construction of a 600-passenger high-speed luxury ferry in September 2016, to shuttle riders between the Jersey Shore and Manhattan, anticipated to start service in 2017; this would be the largest vessel in its class.\n\n\n=== Taxis, transport startups, and trams ===\n\nOther features of the city\'s transportation infrastructure encompass more than 12,000 yellow taxicabs; various competing startup transportation network companies; and an aerial tramway that transports commuters between Roosevelt Island and Manhattan Island. Ride-sharing services have become significant competition for cab drivers in New York.\n\n\n=== Streets and highways ===\n\nDespite New York\'s heavy reliance on its vast public transit system, streets are a defining feature of the city. Manhattan\'s street grid plan greatly influenced the city\'s physical development. Several of the city\'s streets and avenues, like Broadway, Wall Street, Madison Avenue, and Seventh Avenue are also used as metonyms for national industries there: the theater, finance, advertising, and fashion organizations, respectively.\nNew York City also has an extensive web of expressways and parkways, which link the city\'s boroughs to each other and to northern New Jersey, Westchester County, Long Island, and southwestern Connecticut through various bridges and tunnels. Because these highways serve millions of outer borough and suburban residents who commute into Manhattan, it is quite common for motorists to be stranded for hours in traffic jams that are a daily occurrence, particularly during rush hour.New York City is also known for its rules regarding turning at red lights. Unlike the rest of the United States, New York State prohibits right or left turns on red in cities with a population greater than one million, to reduce traffic collisions and increase pedestrian safety. In New York City, therefore, all turns at red lights are illegal unless a sign permitting such maneuvers is present.\n\n\n==== River crossings ====\n\nNew York City is located on one of the world\'s largest natural harbors, and the boroughs of Manhattan and Staten Island are (primarily) coterminous with islands of the same names, while Queens and Brooklyn are located at the west end of the larger Long Island, and The Bronx is located at the southern tip of New York State\'s mainland. This situation of boroughs separated by water led to the development of an extensive infrastructure of well-known bridges and tunnels.\nThe George Washington Bridge is the world\'s busiest motor vehicle bridge, connecting Manhattan to Bergen County, New Jersey. The Verrazano-Narrows Bridge is the longest suspension bridge in the Americas and one of the world\'s longest. The Brooklyn Bridge is an icon of the city itself. The towers of the Brooklyn Bridge are built of limestone, granite, and Rosendale cement, and their architectural style is neo-Gothic, with characteristic pointed arches above the passageways through the stone towers. This bridge was also the longest suspension bridge in the world from its opening until 1903, and is the first steel-wire suspension bridge. The Queensboro Bridge is an important piece of cantilever architecture. The Manhattan Bridge, opened in 1909, is considered to be the forerunner of modern suspension bridges, and its design served as the model for many of the long-span suspension bridges around the world; the Manhattan Bridge, Throgs Neck Bridge, Triborough Bridge, and Verrazano-Narrows Bridge are all examples of Structural Expressionism.Manhattan Island is linked to New York City\'s outer boroughs and New Jersey by several tunnels as well. The Lincoln Tunnel, which carries 120,000 vehicles a day under the Hudson River between New Jersey and Midtown Manhattan, is the busiest vehicular tunnel in the world. The tunnel was built instead of a bridge to allow unfettered passage of large passenger and cargo ships that sailed through New York Harbor and up the Hudson River to Manhattan\'s piers. The Holland Tunnel, connecting Lower Manhattan to Jersey City, New Jersey, was the world\'s first mechanically ventilated vehicular tunnel when it opened in 1927. The Queens-Midtown Tunnel, built to relieve congestion on the bridges connecting Manhattan with Queens and Brooklyn, was the largest non-federal project in its time when it was completed in 1940. President Franklin D. Roosevelt was the first person to drive through it. The Brooklyn-Battery Tunnel (officially known as the Hugh L. Carey Tunnel) runs underneath Battery Park and connects the Financial District at the southern tip of Manhattan to Red Hook in Brooklyn.\n\n\n=== Cycling network ===\n\nCycling in New York City is associated with mixed cycling conditions that include dense urban proximities, relatively flat terrain, congested roadways with "stop-and-go" traffic, and streets with heavy pedestrian activity. The city\'s large cycling population includes utility cyclists, such as delivery and messenger services; cycling clubs for recreational cyclists; and increasingly commuters. Cycling is increasingly popular in New York City; in 2017 there were approximately 450,000 daily bike trips, compared with 170,000 daily bike trips in 2005. As of 2017, New York City had 1,333 miles of bike lanes, compared to 513 miles of bike lanes in 2006. As of 2019, there are 126 miles (203 km) of segregated or "protected" bike lanes citywide.\n\n\n== Environment ==\n\n\n=== Environmental impact reduction ===\nNew York City has focused on reducing its environmental impact and carbon footprint. Mass transit use in New York City is the highest in the United States. Also, by 2010, the city had 3,715 hybrid taxis and other clean diesel vehicles, representing around 28% of New York\'s taxi fleet in service, the most of any city in North America.New York\'s high rate of public transit use, over 200,000 daily cyclists as of 2014, and many pedestrian commuters make it the most energy-efficient major city in the United States. Walk and bicycle modes of travel account for 21% of all modes for trips in the city; nationally the rate for metro regions is about 8%. In both its 2011 and 2015 rankings, Walk Score named New York City the most walkable large city in the United States, and in 2018, Stacker ranked New York the most walkable U.S. city. Citibank sponsored the introduction of 10,000 public bicycles for the city\'s bike-share project in the summer of 2013. New York City\'s numerical "in-season cycling indicator" of bicycling in the city had hit an all-time high of 437 when measured in 2014.The city government was a petitioner in the landmark Massachusetts v. Environmental Protection Agency Supreme Court case forcing the EPA to regulate greenhouse gases as pollutants. The city is a leader in the construction of energy-efficient green office buildings, including the Hearst Tower among others. Mayor Bill de Blasio has committed to an 80% reduction in greenhouse gas emissions between 2014 and 2050 to reduce the city\'s contributions to climate change, beginning with a comprehensive "Green Buildings" plan.\n\n\n=== Water purity and availability ===\n\nNew York City is supplied with drinking water by the protected Catskill Mountains watershed. As a result of the watershed\'s integrity and undisturbed natural water filtration system, New York is one of only four major cities in the United States the majority of whose drinking water is pure enough not to require purification by water treatment plants. The city\'s municipal water system is the largest in the United States, moving over one billion gallons of water per day. The Croton Watershed north of the city is undergoing construction of a US$3.2 billion water purification plant to augment New York City\'s water supply by an estimated 290 million gallons daily, representing a greater than 20% addition to the city\'s current availability of water. The ongoing expansion of New York City Water Tunnel No. 3, an integral part of the New York City water supply system, is the largest capital construction project in the city\'s history, with segments serving Manhattan and The Bronx completed, and with segments serving Brooklyn and Queens planned for construction in 2020. In 2018, New York City announced a US$1 billion investment to protect the integrity of its water system and to maintain the purity of its unfiltered water supply.\n\n\n=== Air quality ===\nAccording to the 2016 World Health Organization Global Urban Ambient Air Pollution Database, the annual average concentration in New York City\'s air of particulate matter measuring 2.5 microns or less (PM2.5) was 7 micrograms per cubic meter, or 3 micrograms below the recommended limit of the WHO Air Quality Guidelines for the annual mean PM2.5. The New York City Department of Health and Mental Hygiene, in partnership with Queens College, conducts the New York Community Air Survey to measure pollutants at about 150 locations.\n\n\n=== Environmental revitalization ===\nNewtown Creek, a 3.5-mile (6-kilometer) a long estuary that forms part of the border between the boroughs of Brooklyn and Queens, has been designated a Superfund site for environmental clean-up and remediation of the waterway\'s recreational and economic resources for many communities. One of the most heavily used bodies of water in the Port of New York and New Jersey, it had been one of the most contaminated industrial sites in the country, containing years of discarded toxins, an estimated 30 million US gallons (110,000 m3) of spilled oil, including the Greenpoint oil spill, raw sewage from New York City\'s sewer system, and other accumulation.\n\n\n== Government and politics ==\n\n\n=== Government ===\n\nNew York City has been a metropolitan municipality with a mayor–council form of government since its consolidation in 1898. In New York City, the city government is responsible for public education, correctional institutions, public safety, recreational facilities, sanitation, water supply, and welfare services.\nThe Mayor and council members are elected to four-year terms. The City Council is a unicameral body consisting of 51 council members whose districts are defined by geographic population boundaries. Each term for the mayor and council members lasts four years and has a three consecutive-term limit, which is reset after a four-year break. The New York City Administrative Code, the New York City Rules, and the City Record are the code of local laws, compilation of regulations, and official journal, respectively.\n\nEach borough is coextensive with a judicial district of the state Unified Court System, of which the Criminal Court and the Civil Court are the local courts, while the New York Supreme Court conducts major trials and appeals. Manhattan hosts the First Department of the Supreme Court, Appellate Division while Brooklyn hosts the Second Department. There are also several extrajudicial administrative courts, which are executive agencies and not part of the state Unified Court System.\nUniquely among major American cities, New York is divided between, and is host to the main branches of, two different US district courts: the District Court for the Southern District of New York, whose main courthouse is on Foley Square near City Hall in Manhattan and whose jurisdiction includes Manhattan and the Bronx; and the District Court for the Eastern District of New York, whose main courthouse is in Brooklyn and whose jurisdiction includes Brooklyn, Queens, and Staten Island. The US Court of Appeals for the Second Circuit and US Court of International Trade are also based in New York, also on Foley Square in Manhattan.\n\n\n=== Politics ===\n\nThe present mayor is Bill de Blasio, the first Democrat since 1993. He was elected in 2013 with over 73% of the vote, and assumed office on January 1, 2014.\nThe Democratic Party holds the majority of public offices. As of April 2016, 69% of registered voters in the city are Democrats and 10% are Republicans. New York City has not been carried by a Republican in a statewide or presidential election since President Calvin Coolidge won the five boroughs in 1924. In 2012, Democrat Barack Obama became the first presidential candidate of any party to receive more than 80% of the overall vote in New York City, sweeping all five boroughs. Party platforms center on affordable housing, education, and economic development, and labor politics are of importance in the city.\nNew York is the most important source of political fundraising in the United States, as four of the top five ZIP Codes in the nation for political contributions are in Manhattan. The top ZIP Code, 10021 on the Upper East Side, generated the most money for the 2004 presidential campaigns of George W. Bush and John Kerry. The city has a strong imbalance of payments with the national and state governments. It receives 83 cents in services for every $1 it sends to the federal government in taxes (or annually sends $11.4 billion more than it receives back). City residents and businesses also sent an additional $4.1 billion in the 2009–2010 fiscal year to the state of New York than the city received in return.\n\n\n== Notable people ==\n\n\n== Global outreach ==\nIn 2006, the Sister City Program of the City of New York, Inc. was restructured and renamed New York City Global Partners. Through this program, New York City has expanded its international outreach to a network of cities worldwide, promoting the exchange of ideas and innovation between their citizenry and policymakers. New York\'s historic sister cities are denoted below by the year they joined New York City\'s partnership network.\n\n\n== See also ==\nOutline of New York City\n\n\n== Notes ==\n\n\n== References ==\n\n\n== Further reading ==\nBelden, E. Porter (1849). New York, Past, Present, and Future: Comprising a History of the City of New York, a Description of its Present Condition, and an Estimate of its Future Increase. New York: G.P. Putnam. From Google Books.\nBurgess, Anthony (1976). New York. New York: Little, Brown & Co. ISBN 978-90-6182-266-0.\nBurrows, Edwin G. & Wallace, Mike (1999), Gotham: A History of New York City to 1898, New York: Oxford University Press, ISBN 0-195-11634-8\nFederal Writers\' Project (1939). The WPA Guide to New York City (1995 reissue ed.). New York: The New Press. ISBN 978-1-56584-321-9.\nJackson, Kenneth T., ed. (1995), The Encyclopedia of New York City, New Haven: Yale University Press, ISBN 0300055366\nJackson, Kenneth T.; Dunbar, David S., eds. (2005). Empire City: New York Through the Centuries. Columbia University Press. ISBN 978-0-231-10909-3.\nLankevich, George L. (1998). American Metropolis: A History of New York City. NYU Press. ISBN 978-0-8147-5186-2.\nWhite, E.B. (1949). Here is New York (2000 reissue ed.). Little Bookroom.\nWhite, Norval & Willensky, Elliot (2000), AIA Guide to New York City (4th ed.), New York: Three Rivers Press, ISBN 978-0-8129-3107-5\nWhitehead, Colson (2003). The Colossus of New York: A City in 13 Parts. New York: Doubleday. ISBN 978-0-385-50794-3.\n\n\n== External links ==\nOfficial website\nNYC Go, official tourism website of New York City\nNew York City at Curlie\n Geographic data related to New York City at OpenStreetMap.\nCollections, 145,000 NYC photographs at the Museum of the City of New York\n"The New New York Skyline". National Geographic. November 2015. (Interactive.)' +print(ny.links[0]) # 10 Hudson Yards print() diff --git a/win_create_shortcut_lnk.py b/win_create_shortcut_lnk.py new file mode 100644 index 000000000..594aef543 --- /dev/null +++ b/win_create_shortcut_lnk.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pywin32 +from win32com.client import Dispatch + + +def create_shortcut(file_name: str, target: str, work_dir: str, arguments: str = "") -> None: + shell = Dispatch("WScript.Shell") + shortcut = shell.CreateShortCut(file_name) + shortcut.TargetPath = target + shortcut.Arguments = arguments + shortcut.WorkingDirectory = work_dir + shortcut.save() + + +if __name__ == "__main__": + from pathlib import Path + + abs_file_name = r"C:\Program Files (x86)\ConEmu\ConEmu.exe" + path = Path(abs_file_name) + + name = "My startup python scripts 1" + + create_shortcut( + file_name=f"ConEmu start task '{name}'.lnk", + target=str(path), + work_dir=str(path.parent), + arguments="/cmd {%s} -new_console" % name, + ) diff --git a/winapi__screenshot_window.py b/winapi__screenshot_window.py index 0f1c7991c..23e74ce40 100644 --- a/winapi__screenshot_window.py +++ b/winapi__screenshot_window.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/24352388/5909792 @@ -9,20 +9,23 @@ import win32gui import win32ui + from ctypes import windll + from PIL import Image -hwnd = win32gui.FindWindow(None, 'Telegram') + +hwnd = win32gui.FindWindow(None, "Telegram") # Change the line below depending on whether you want the whole window # or just the client area. -#left, top, right, bot = win32gui.GetClientRect(hwnd) +# left, top, right, bot = win32gui.GetClientRect(hwnd) left, top, right, bot = win32gui.GetWindowRect(hwnd) w = right - left h = bot - top hwndDC = win32gui.GetWindowDC(hwnd) -mfcDC = win32ui.CreateDCFromHandle(hwndDC) +mfcDC = win32ui.CreateDCFromHandle(hwndDC) saveDC = mfcDC.CreateCompatibleDC() saveBitMap = win32ui.CreateBitmap() @@ -40,9 +43,8 @@ bmpstr = saveBitMap.GetBitmapBits(True) im = Image.frombuffer( - 'RGB', - (bmpinfo['bmWidth'], bmpinfo['bmHeight']), - bmpstr, 'raw', 'BGRX', 0, 1) + "RGB", (bmpinfo["bmWidth"], bmpinfo["bmHeight"]), bmpstr, "raw", "BGRX", 0, 1 +) win32gui.DeleteObject(saveBitMap.GetHandle()) saveDC.DeleteDC() @@ -50,5 +52,5 @@ win32gui.ReleaseDC(hwnd, hwndDC) if result == 1: - #PrintWindow Succeeded + # PrintWindow Succeeded im.save("test.png") diff --git a/winapi__windows__ctypes/change_icon_cursor__animated_cursor/main.py b/winapi__windows__ctypes/change_icon_cursor__animated_cursor/main.py index d0da1a872..64a054a27 100644 --- a/winapi__windows__ctypes/change_icon_cursor__animated_cursor/main.py +++ b/winapi__windows__ctypes/change_icon_cursor__animated_cursor/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -14,12 +14,16 @@ # SOURCE: https://stackoverflow.com/a/55979357/5909792 # SOURCE: http://www.rw-designer.com/cursor-detail/106595 -FILE_NAME_CURSOR = 'fidget spinner animated normal select.ani' +FILE_NAME_CURSOR = "fidget spinner animated normal select.ani" -hold = win32gui.LoadImage(0, 32512, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_SHARED ) -hsave = ctypes.windll.user32.CopyImage(hold, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_COPYFROMRESOURCE) +hold = win32gui.LoadImage(0, 32512, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_SHARED) +hsave = ctypes.windll.user32.CopyImage( + hold, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_COPYFROMRESOURCE +) -hnew = win32gui.LoadImage(0, FILE_NAME_CURSOR, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_LOADFROMFILE) +hnew = win32gui.LoadImage( + 0, FILE_NAME_CURSOR, win32con.IMAGE_CURSOR, 0, 0, win32con.LR_LOADFROMFILE +) ctypes.windll.user32.SetSystemCursor(hnew, 32512) time.sleep(10) diff --git a/winapi__windows__ctypes/close_ad_window_avast/close_ad_window_avast.py b/winapi__windows__ctypes/close_ad_window_avast/close_ad_window_avast.py index 55a70d6f5..6aa79e1f7 100644 --- a/winapi__windows__ctypes/close_ad_window_avast/close_ad_window_avast.py +++ b/winapi__windows__ctypes/close_ad_window_avast/close_ad_window_avast.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Script to detect and close Avast advertising windows.""" -def get_logger(): - import logging - import sys +import logging +import sys + +import win32gui +import win32con + - log = logging.getLogger(__name__) +def get_logger(): + log = logging.getLogger(__file__) log.setLevel(logging.DEBUG) - formatter = logging.Formatter('[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + ) - fh = logging.FileHandler('log', encoding='utf-8') + fh = logging.FileHandler("log", encoding="utf-8") fh.setLevel(logging.DEBUG) ch = logging.StreamHandler(stream=sys.stdout) @@ -35,26 +41,24 @@ def get_logger(): # TODO: не все окна являются рекламными, это может быть окно сканирования системы -def close_ad(): - import win32gui - import win32con - - hwnd = win32gui.FindWindow('asw_av_popup_wndclass', None) +def close_ad() -> None: + hwnd = win32gui.FindWindow("asw_av_popup_wndclass", None) if hwnd: - log.debug('Found Avast advertising window, close it.') + log.debug("Found Avast advertising window, close it.") # Send close window command win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0) -if __name__ == '__main__': +if __name__ == "__main__": + import time + try: - log.debug('Start') + log.debug("Start") - import time while True: close_ad() time.sleep(0.2) finally: - log.debug('Finish') + log.debug("Finish") diff --git a/winapi__windows__ctypes/ctypes.windll.kernel32.GetTickCount.py b/winapi__windows__ctypes/ctypes.windll.kernel32.GetTickCount.py index 0830d5d45..a6df3b8e4 100644 --- a/winapi__windows__ctypes/ctypes.windll.kernel32.GetTickCount.py +++ b/winapi__windows__ctypes/ctypes.windll.kernel32.GetTickCount.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes +import time + + GetTickCount = ctypes.windll.kernel32.GetTickCount t = GetTickCount() -import time time.sleep(1) -print('Elapsed: {} ms'.format(GetTickCount() - t)) +print(f"Elapsed: {GetTickCount() - t} ms") diff --git a/winapi__windows__ctypes/ctypes__FindWindow.py b/winapi__windows__ctypes/ctypes__FindWindow.py index 95d9846f0..49128541e 100644 --- a/winapi__windows__ctypes/ctypes__FindWindow.py +++ b/winapi__windows__ctypes/ctypes__FindWindow.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes + + FindWindow = ctypes.windll.user32.FindWindowW -hwnd = FindWindow('Shell_TrayWnd', None) +hwnd = FindWindow("Shell_TrayWnd", None) print(hwnd) diff --git a/winapi__windows__ctypes/ctypes__GetWindowRect.py b/winapi__windows__ctypes/ctypes__GetWindowRect.py index 2c2d17209..5f8911c90 100644 --- a/winapi__windows__ctypes/ctypes__GetWindowRect.py +++ b/winapi__windows__ctypes/ctypes__GetWindowRect.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes @@ -17,7 +17,7 @@ GetWindowRect = ctypes.windll.user32.GetWindowRect -hwnd = FindWindow('Shell_TrayWnd', None) +hwnd = FindWindow("Shell_TrayWnd", None) print(hwnd) rect = RECT() diff --git a/winapi__windows__ctypes/ctypes__MessageBeep.py b/winapi__windows__ctypes/ctypes__MessageBeep.py index 8084c40bf..e8bb8f43b 100644 --- a/winapi__windows__ctypes/ctypes__MessageBeep.py +++ b/winapi__windows__ctypes/ctypes__MessageBeep.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes + # SOURCE: https://docs.microsoft.com/ru-ru/windows/desktop/api/winuser/nf-winuser-messagebeep # # BOOL MessageBeep( diff --git a/winapi__windows__ctypes/ctypes__MessageBox.py b/winapi__windows__ctypes/ctypes__MessageBox.py index c462af39d..beaa1d2ec 100644 --- a/winapi__windows__ctypes/ctypes__MessageBox.py +++ b/winapi__windows__ctypes/ctypes__MessageBox.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes @@ -33,15 +33,15 @@ None, "Resource not available\nDo you want to try again?", "Account Details", - MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2 + MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2, ) -print('button_id:', button_id) +print("button_id:", button_id) if button_id == IDCANCEL: - print('IDCANCEL') + print("IDCANCEL") elif button_id == IDTRYAGAIN: - print('IDTRYAGAIN') + print("IDTRYAGAIN") elif button_id == IDCONTINUE: - print('IDCONTINUE') + print("IDCONTINUE") diff --git a/winapi__windows__ctypes/get_screen_info.py b/winapi__windows__ctypes/get_screen_info.py new file mode 100644 index 000000000..b63ccb157 --- /dev/null +++ b/winapi__windows__ctypes/get_screen_info.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import ctypes +import tkinter + + +def get_screen_info() -> tuple[int, int, int]: + ctypes.windll.shcore.SetProcessDpiAwareness(1) + + root = tkinter.Tk() + + width = ctypes.windll.user32.GetSystemMetrics(0) + height = ctypes.windll.user32.GetSystemMetrics(1) + + dpi = ctypes.windll.user32.GetDpiForWindow(root.winfo_id()) + + # Destroy the window + root.destroy() + + return width, height, dpi + + +if __name__ == "__main__": + width, height, dpi = get_screen_info() + print(width, height, dpi) diff --git a/winapi__windows__ctypes/get_tasks_from_scheduler.py b/winapi__windows__ctypes/get_tasks_from_scheduler.py new file mode 100644 index 000000000..25126c3d4 --- /dev/null +++ b/winapi__windows__ctypes/get_tasks_from_scheduler.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-task-element +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/taskschd/execaction +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/taskschd/comhandleraction +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/taskschd/emailaction +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/taskschd/showmessageaction + + +import datetime as dt +import enum + +from dataclasses import dataclass, field +from typing import TypeVar + +# pip install pywin32 +import win32com.client + + +# SOURCE: https://docs.microsoft.com/en-us/windows/win32/api/taskschd/nf-taskschd-itaskfolder-gettasks#parameters +# Specifies whether to retrieve hidden tasks. Pass in TASK_ENUM_HIDDEN to retrieve all tasks in the folder +# including hidden tasks, and pass in 0 to retrieve all the tasks in the folder excluding the hidden tasks. +TASK_ENUM_HIDDEN = 1 + + +class TaskStateEnum(enum.IntEnum): + Unknown = 0 + Disabled = enum.auto() + Queued = enum.auto() + Ready = enum.auto() + Running = enum.auto() + + +# https://docs.microsoft.com/en-us/windows/win32/taskschd/action-type +class TaskActionEnum(enum.IntEnum): + Exec = 0 + ComHandler = 5 + SendEmail = 6 + ShowMessage = 7 + + +@dataclass +class ExecAction: + path: str + working_directory: str + arguments: str + + @classmethod + def get_from(cls, action: win32com.client.CDispatch) -> "ExecAction": + return cls( + path=action.Path, + working_directory=action.WorkingDirectory, + arguments=action.Arguments, + ) + + +@dataclass +class ComHandlerAction: + class_id: str + data: str + + @classmethod + def get_from(cls, action: win32com.client.CDispatch) -> "ComHandlerAction": + return cls( + class_id=action.ClassId, + data=action.Data, + ) + + +@dataclass +class EmailAction: + from_: str + to: str + subject: str + body: str + server: str + # attachments: ??? # TODO: Unknown + bcc: str + cc: str + # header_fields: ??? # TODO: Unknown + reply_to: str + + @classmethod + def get_from(cls, action: win32com.client.CDispatch) -> "EmailAction": + return cls( + from_=action.From, + to=action.To, + subject=action.Subject, + body=action.Body, + server=action.Server, + # attachments=??? # TODO: Unknown + bcc=action.Bcc, + cc=action.Cc, + # header_fields=xxx, # TODO: Unknown + reply_to=action.ReplyTo, + ) + + +@dataclass +class ShowMessageAction: + title: str + message_body: str + + @classmethod + def get_from(cls, action: win32com.client.CDispatch) -> "ShowMessageAction": + return cls( + title=action.Title, + message_body=action.MessageBody, + ) + + +ActionType = TypeVar( + "ActionType", ExecAction, ComHandlerAction, EmailAction, ShowMessageAction +) + + +@dataclass +class Task: + name: str + path: str + hidden: bool + state: str + enabled: bool + last_run_time: dt.datetime + last_task_result: int + next_run_time: dt.datetime + number_of_missed_runs: int + actions: list[ActionType] = field(default_factory=list) + + @classmethod + def get_from(cls, task: win32com.client.CDispatch) -> "Task": + try: + hidden = task.Definition.Settings.Hidden + except: + hidden = False + + try: + def_actions = task.Definition.Actions + except: + def_actions = [] + + actions = [] + + for action_com_obj in def_actions: + action_type = TaskActionEnum(action_com_obj.Type) + + if action_type == TaskActionEnum.Exec: + actions.append(ExecAction.get_from(action_com_obj)) + elif action_type == TaskActionEnum.ComHandler: + actions.append(ComHandlerAction.get_from(action_com_obj)) + elif action_type == TaskActionEnum.SendEmail: + actions.append(EmailAction.get_from(action_com_obj)) + elif action_type == TaskActionEnum.ShowMessage: + actions.append(ShowMessageAction.get_from(action_com_obj)) + + return cls( + name=task.Name, + path=task.Path, + hidden=hidden, + state=TaskStateEnum(task.State).name, + enabled=task.Enabled, + last_run_time=task.LastRunTime, + last_task_result=task.LastTaskResult, + next_run_time=task.NextRunTime, + number_of_missed_runs=task.NumberOfMissedRuns, + actions=actions, + ) + + +def get_tasks() -> list[Task]: + items = [] + + scheduler = win32com.client.Dispatch("Schedule.Service") + scheduler.Connect() + + folders = [scheduler.GetFolder("\\")] + while folders: + folder = folders.pop(0) + folders += list(folder.GetFolders(0)) + for task_com_obj in folder.GetTasks(TASK_ENUM_HIDDEN): + items.append(Task.get_from(task_com_obj)) + + return items + + +if __name__ == "__main__": + items = get_tasks() + print(f"Total task: {len(items)}") + + hidden_tasks = [task for task in items if task.hidden] + print(f"Total hidden tasks: {len(hidden_tasks)}") + + enabled_tasks = [task for task in items if task.enabled] + print(f"Total enabled tasks: {len(enabled_tasks)}") + + hidden_and_enabled_tasks = [task for task in items if task.hidden and task.enabled] + print(f"Total hidden and enabled tasks: {len(hidden_and_enabled_tasks)}") + + print() + + print("First 10 tasks:") + for i, task in enumerate(items[:10], 1): + print(f" {i}. {task}") diff --git a/winapi__windows__ctypes/hibernate_win7.py b/winapi__windows__ctypes/hibernate_win7.py index ff78e0f39..bb225467a 100644 --- a/winapi__windows__ctypes/hibernate_win7.py +++ b/winapi__windows__ctypes/hibernate_win7.py @@ -1,31 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - +__author__ = "ipetrash" + + """Скрипт отправляет win7 в режим гибернации (спящий режим)""" - + + +import os +import sys + try: - import sys if len(sys.argv) > 1: seconds_delay = int(sys.argv[1]) import time while seconds_delay > 0: - print('\rDelay before hibernate: {} secs'.format(seconds_delay), end='') + print(f"\rDelay before hibernate: {seconds_delay} secs", end="") time.sleep(1) seconds_delay -= 1 - print('\nGo to Hibernate') + print("\nGo to Hibernate") # Для включения режима гибернации нужно запустить консоль от администратора и ввести: # powercfg -hibernate on - import os - os.system('rundll32 powrprof.dll,SetSuspendState 0,1,0') + os.system("rundll32 powrprof.dll,SetSuspendState 0,1,0") except KeyboardInterrupt: - print('\nInterrupt') + print("\nInterrupt") diff --git a/winapi__windows__ctypes/lock_workstation.py b/winapi__windows__ctypes/lock_workstation.py new file mode 100644 index 000000000..d842c3519 --- /dev/null +++ b/winapi__windows__ctypes/lock_workstation.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import ctypes + + +ctypes.windll.user32.LockWorkStation() diff --git a/winapi__windows__ctypes/log_window_focus__trackwindow.py b/winapi__windows__ctypes/log_window_focus__trackwindow.py index adedcbdf0..58872c015 100644 --- a/winapi__windows__ctypes/log_window_focus__trackwindow.py +++ b/winapi__windows__ctypes/log_window_focus__trackwindow.py @@ -64,11 +64,11 @@ lastTime = 0 -def log(msg): +def log(msg) -> None: print(msg) -def logError(msg): +def logError(msg) -> None: print(msg, file=sys.stderr) @@ -129,7 +129,7 @@ def getProcessFilename(processID): kernel32.CloseHandle(hProcess) -def callback(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime): +def callback(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) -> None: global lastTime length = user32.GetWindowTextLengthW(hwnd) title = ctypes.create_unicode_buffer(length + 1) @@ -169,7 +169,7 @@ def setHook(WinEventProc, eventType): ) -def main(): +def main() -> None: ole32.CoInitialize(0) WinEventProc = WinEventProcType(callback) diff --git a/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper.py b/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper.py index cd13cace5..f6230eed0 100644 --- a/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper.py +++ b/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://www.blog.pythonlibrary.org/2014/10/22/pywin32-how-to-set-desktop-background/ @@ -13,13 +13,15 @@ import win32gui -def set_wallpaper(file_name): - key = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, win32con.KEY_SET_VALUE) +def set_wallpaper(file_name) -> None: + key = win32api.RegOpenKeyEx( + win32con.HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, win32con.KEY_SET_VALUE + ) win32api.RegSetValueEx(key, "WallpaperStyle", 0, win32con.REG_SZ, "0") win32api.RegSetValueEx(key, "TileWallpaper", 0, win32con.REG_SZ, "0") win32gui.SystemParametersInfo(win32con.SPI_SETDESKWALLPAPER, file_name, 1 + 2) if __name__ == "__main__": - file_name = r'C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg' + file_name = r"C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg" set_wallpaper(file_name) diff --git a/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper_with_ctypes.py b/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper_with_ctypes.py index 62e5cde5b..569702d34 100644 --- a/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper_with_ctypes.py +++ b/winapi__windows__ctypes/set_wallpaper_on_win/set_wallpaper_with_ctypes.py @@ -1,17 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://www.blog.pythonlibrary.org/2014/10/22/pywin32-how-to-set-desktop-background/ import ctypes + + SPI_SETDESKWALLPAPER = 20 -def set_wallpaper_with_ctypes(file_name): +def set_wallpaper_with_ctypes(file_name) -> None: # This code is based on the following two links # http://mail.python.org/pipermail/python-win32/2005-January/002893.html # http://code.activestate.com/recipes/435877-change-the-wallpaper-under-windows/ @@ -20,5 +22,5 @@ def set_wallpaper_with_ctypes(file_name): if __name__ == "__main__": - file_name = r'C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg' + file_name = r"C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg" set_wallpaper_with_ctypes(file_name) diff --git a/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/gui.py b/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/gui.py index 9b536b878..88b062e5f 100644 --- a/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/gui.py +++ b/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/gui.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import QtWidgets as qtw from main import preventing_on, preventing_off -if __name__ == '__main__': +if __name__ == "__main__": app = qtw.QApplication([]) w = qtw.QWidget() - w.setWindowTitle('Preventing entering sleep or turning off the display') + w.setWindowTitle("Preventing entering sleep or turning off the display") w.setLayout(qtw.QVBoxLayout()) - def button_clicked(checked): + def button_clicked(checked) -> None: if checked: - button.setText('On') + button.setText("On") preventing_on() else: - button.setText('Off') + button.setText("Off") preventing_off() button = qtw.QPushButton() diff --git a/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/main.py b/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/main.py index 91e9cd380..f50033a94 100644 --- a/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/main.py +++ b/winapi__windows__ctypes/winapi_SetThreadExecutionState__preventing system entering sleep or turning off display/main.py @@ -1,35 +1,41 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://msdn.microsoft.com/en-us/library/aa373208(VS.85).aspx + +import ctypes + + ES_CONTINUOUS = 0x80000000 ES_SYSTEM_REQUIRED = 0x00000001 ES_AWAYMODE_REQUIRED = 0x00000040 ES_DISPLAY_REQUIRED = 0x00000002 -import ctypes SetThreadExecutionState = ctypes.windll.kernel32.SetThreadExecutionState -def preventing_on(): +def preventing_on() -> None: # Television recording is beginning. Enable away mode and prevent the sleep idle time-out. - SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED | ES_DISPLAY_REQUIRED) + SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED | ES_DISPLAY_REQUIRED + ) -def preventing_off(): +def preventing_off() -> None: # Clear EXECUTION_STATE flags to disable away mode and allow the system to idle to sleep normally. SetThreadExecutionState(ES_CONTINUOUS) -if __name__ == '__main__': +if __name__ == "__main__": + import time + preventing_on() # Wait 1 hours - import time time.sleep(60 * 60) # # infinity diff --git a/winapi__windows__ctypes/winapi__GetDesktopListViewHandle.py b/winapi__windows__ctypes/winapi__GetDesktopListViewHandle.py index e6412a3a5..41f61f745 100644 --- a/winapi__windows__ctypes/winapi__GetDesktopListViewHandle.py +++ b/winapi__windows__ctypes/winapi__GetDesktopListViewHandle.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import ctypes +from win32con import GW_CHILD def GetDesktopListViewHandle(): @@ -24,7 +28,6 @@ def GetDesktopListViewHandle(): """ - import ctypes FindWindow = ctypes.windll.user32.FindWindowW GetWindow = ctypes.windll.user32.GetWindow @@ -33,19 +36,17 @@ def GetClassName(hwnd): ctypes.windll.user32.GetClassNameW(hwnd, buff, 99) return buff.value - from win32con import GW_CHILD - # Ищем окно с классом "Progman" ("Program Manager") - hwnd = FindWindow('Progman', None) + hwnd = FindWindow("Progman", None) hwnd = GetWindow(hwnd, GW_CHILD) # SHELLDLL_DefView hwnd = GetWindow(hwnd, GW_CHILD) # SysListView32 - if GetClassName(hwnd) != 'SysListView32': + if GetClassName(hwnd) != "SysListView32": return 0 return hwnd -if __name__ == '__main__': +if __name__ == "__main__": handle = GetDesktopListViewHandle() - print('handle:', handle) + print("handle:", handle) diff --git a/winapi__windows__ctypes/winapi__GetKeyboardLayout_for_console.py b/winapi__windows__ctypes/winapi__GetKeyboardLayout_for_console.py index 04484342d..bf8ec0130 100644 --- a/winapi__windows__ctypes/winapi__GetKeyboardLayout_for_console.py +++ b/winapi__windows__ctypes/winapi__GetKeyboardLayout_for_console.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для получения правильного значения раскладки клавиатуры в Windows для консольных @@ -16,6 +16,8 @@ from ctypes import * + + user32 = windll.user32 kernel32 = windll.kernel32 @@ -25,7 +27,7 @@ class RECT(Structure): ("left", c_ulong), ("top", c_ulong), ("right", c_ulong), - ("bottom", c_ulong) + ("bottom", c_ulong), ] @@ -39,11 +41,11 @@ class GUITHREADINFO(Structure): ("hwndMenuOwner", c_ulong), ("hwndMoveSize", c_ulong), ("hwndCaret", c_ulong), - ("rcCaret", RECT) + ("rcCaret", RECT), ] -if __name__ == '__main__': +if __name__ == "__main__": gti = GUITHREADINFO(cbSize=sizeof(GUITHREADINFO)) user32.GetGUIThreadInfo(0, byref(gti)) diff --git a/winapi__windows__ctypes/winapi__GetTextLastError.py b/winapi__windows__ctypes/winapi__GetTextLastError.py index ea4b5ad05..7323c7be6 100644 --- a/winapi__windows__ctypes/winapi__GetTextLastError.py +++ b/winapi__windows__ctypes/winapi__GetTextLastError.py @@ -1,47 +1,52 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def GetTextLastError(error_code=None): +import ctypes + +from win32con import ( + FORMAT_MESSAGE_FROM_SYSTEM, + FORMAT_MESSAGE_ALLOCATE_BUFFER, + FORMAT_MESSAGE_IGNORE_INSERTS, +) + + +def GetTextLastError(error_code=None) -> str: """ Функция возвращает текстовое описание ошибки. Если не передавать код ошибки, будет возвращаться описание ошибки из GetLastError(). """ - import ctypes GetLastError = ctypes.windll.kernel32.GetLastError FormatMessage = ctypes.windll.kernel32.FormatMessageA LocalFree = ctypes.windll.kernel32.LocalFree - from win32con import ( - FORMAT_MESSAGE_FROM_SYSTEM, - FORMAT_MESSAGE_ALLOCATE_BUFFER, - FORMAT_MESSAGE_IGNORE_INSERTS) - if error_code is None: error_code = GetLastError() message_buffer = ctypes.c_char_p() FormatMessage( - FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, + FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_IGNORE_INSERTS, None, error_code, 0, ctypes.byref(message_buffer), 0, - None + None, ) error_message = message_buffer.value LocalFree(message_buffer) - error_message = error_message.decode('cp1251').strip() - return '{} - {}'.format(error_code, error_message) + error_message = error_message.decode("cp1251").strip() + return f"{error_code} - {error_message}" -if __name__ == '__main__': +if __name__ == "__main__": print(GetTextLastError()) print(GetTextLastError(128)) diff --git a/winapi__windows__ctypes/winapi__close_child_windows.py b/winapi__windows__ctypes/winapi__close_child_windows.py index e1553aece..d22d938d9 100644 --- a/winapi__windows__ctypes/winapi__close_child_windows.py +++ b/winapi__windows__ctypes/winapi__close_child_windows.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -15,20 +15,20 @@ import win32con -def all_ok(hwnd, param): +def all_ok(hwnd, param) -> bool: text = win32gui.GetWindowText(hwnd) class_name = win32gui.GetClassName(hwnd) - print('#{:0>8x} "{}": {}'.format(hwnd, text, class_name)) + print(f'#{hwnd:0>8x} "{text}": {class_name}') # Закрытие панели инструментов - if class_name == 'ToolbarWindow32': + if class_name == "ToolbarWindow32": win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) return True -def close_toolbars(): - hwnd = win32gui.FindWindow('Notepad++', None) +def close_toolbars() -> None: + hwnd = win32gui.FindWindow("Notepad++", None) if not hwnd: print('Window "Notepad++" not found!') return @@ -36,5 +36,5 @@ def close_toolbars(): win32gui.EnumChildWindows(hwnd, all_ok, None) -if __name__ == '__main__': +if __name__ == "__main__": close_toolbars() diff --git a/winapi__windows__ctypes/winapi__get_desktop_resolution.py b/winapi__windows__ctypes/winapi__get_desktop_resolution.py index 9b59ac6d3..a6cddcd87 100644 --- a/winapi__windows__ctypes/winapi__get_desktop_resolution.py +++ b/winapi__windows__ctypes/winapi__get_desktop_resolution.py @@ -1,30 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def get_desktop_resolution(): +import ctypes +from ctypes.wintypes import RECT + +from win32con import MONITOR_DEFAULTTONEAREST + +from winapi_GetDesktopListViewHandle import GetDesktopListViewHandle + + +def get_desktop_resolution() -> tuple[int, int]: """ Функция возвращает разрешение экрана. """ - from ctypes.wintypes import RECT - from win32con import MONITOR_DEFAULTTONEAREST - - import ctypes MonitorFromWindow = ctypes.windll.user32.MonitorFromWindow GetMonitorInfo = ctypes.windll.user32.GetMonitorInfoW - from winapi_GetDesktopListViewHandle import GetDesktopListViewHandle - class MONITORINFO(ctypes.Structure): _fields_ = [ - ('cbSize', ctypes.c_int), - ('rcMonitor', RECT), - ('rcWork', RECT), - ('dwFlags', ctypes.c_int), + ("cbSize", ctypes.c_int), + ("rcMonitor", RECT), + ("rcWork", RECT), + ("dwFlags", ctypes.c_int), ] hwnd = GetDesktopListViewHandle() @@ -38,6 +40,6 @@ class MONITORINFO(ctypes.Structure): return width, height -if __name__ == '__main__': +if __name__ == "__main__": w, h = get_desktop_resolution() - print(w, h, sep='x') + print(w, h, sep="x") diff --git a/winapi__windows__ctypes/winapi__get_key_state.py b/winapi__windows__ctypes/winapi__get_key_state.py index f397b33fe..3407ee632 100644 --- a/winapi__windows__ctypes/winapi__get_key_state.py +++ b/winapi__windows__ctypes/winapi__get_key_state.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Ловим нажатие кнопки и выходим.""" -if __name__ == '__main__': - import time - import win32api +import time +import sys - while True: - if win32api.GetAsyncKeyState(ord('Q')): - print('press Q') - quit() +import win32api - time.sleep(0.01) + +while True: + if win32api.GetAsyncKeyState(ord("Q")): + print("Press Q") + sys.exit() + + time.sleep(0.01) diff --git a/winapi__windows__ctypes/winapi__get_usb_list.py b/winapi__windows__ctypes/winapi__get_usb_list.py index a9b8224f7..127d82e8a 100644 --- a/winapi__windows__ctypes/winapi__get_usb_list.py +++ b/winapi__windows__ctypes/winapi__get_usb_list.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def locate_usb() -> list: - import win32file - from winapi__get_logical_drives import get_logical_drives +import win32file +from winapi__get_logical_drives import get_logical_drives + +def locate_usb() -> list: usb_list = list() for drive_name in get_logical_drives(): @@ -19,5 +20,5 @@ def locate_usb() -> list: return usb_list -if __name__ == '__main__': +if __name__ == "__main__": print(locate_usb()) diff --git a/winapi__windows__ctypes/winapi__input_in_notepad.py b/winapi__windows__ctypes/winapi__input_in_notepad.py index 96803303c..69a36e87d 100644 --- a/winapi__windows__ctypes/winapi__input_in_notepad.py +++ b/winapi__windows__ctypes/winapi__input_in_notepad.py @@ -1,19 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import win32gui import win32api import win32con + h = win32gui.FindWindow(None, "Untitled - Notepad") print(h) if h: child = win32gui.FindWindowEx(h, None, "Edit", None) - pl = win32api.SendMessage(child, win32con.WM_CHAR, ord('D'), 1) + pl = win32api.SendMessage(child, win32con.WM_CHAR, ord("D"), 1) # OR: @@ -23,5 +24,5 @@ # pl1_F5 = win32api.PostMessage(child, win32con.WM_KEYDOWN, win32con.VK_F5, lparam1) # pl2_F5 = win32api.PostMessage(child, win32con.WM_KEYUP, win32con.VK_F5, lparam2) - pl1_F5 = win32api.PostMessage(child, win32con.WM_KEYDOWN, ord('D'), lparam1) - pl2_F5 = win32api.PostMessage(child, win32con.WM_KEYUP, ord('D'), lparam2) + pl1_F5 = win32api.PostMessage(child, win32con.WM_KEYDOWN, ord("D"), lparam1) + pl2_F5 = win32api.PostMessage(child, win32con.WM_KEYUP, ord("D"), lparam2) diff --git a/winapi__windows__ctypes/winapi__micro_cursor_move.py b/winapi__windows__ctypes/winapi__micro_cursor_move.py index efea3a373..c0f64ed5f 100644 --- a/winapi__windows__ctypes/winapi__micro_cursor_move.py +++ b/winapi__windows__ctypes/winapi__micro_cursor_move.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Для того, чтобы винда не разлогинивалась от бездействия.""" -if __name__ == '__main__': - import time - import win32api +import time +import win32api - delay = 60 - try: - while True: - x, y = win32api.GetCursorPos() +delay = 60 - win32api.SetCursorPos((x + 1, y)) - win32api.SetCursorPos((x - 1, y)) +try: + while True: + x, y = win32api.GetCursorPos() - time.sleep(delay) + win32api.SetCursorPos((x + 1, y)) + win32api.SetCursorPos((x - 1, y)) - except KeyboardInterrupt: - pass + time.sleep(delay) + +except KeyboardInterrupt: + pass diff --git a/winapi__windows__ctypes/winapi__process_list.py b/winapi__windows__ctypes/winapi__process_list.py index adc0b7a64..2594518de 100644 --- a/winapi__windows__ctypes/winapi__process_list.py +++ b/winapi__windows__ctypes/winapi__process_list.py @@ -2,45 +2,49 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://mail.python.org/pipermail/python-win32/2007-June/006174.html + import copy import ctypes import sys import win32con + TH32CS_SNAPPROCESS = 0x00000002 class PROCESSENTRY32(ctypes.Structure): - _fields_ = [("dwSize", ctypes.c_ulong), - ("cntUsage", ctypes.c_ulong), - ("th32ProcessID", ctypes.c_ulong), - ("th32DefaultHeapID", ctypes.c_ulong), - ("th32ModuleID", ctypes.c_ulong), - ("cntThreads", ctypes.c_ulong), - ("th32ParentProcessID", ctypes.c_ulong), - ("pcPriClassBase", ctypes.c_ulong), - ("dwFlags", ctypes.c_ulong), - ("szExeFile", ctypes.c_char * 260)] - - def __str__(self): + _fields_ = [ + ("dwSize", ctypes.c_ulong), + ("cntUsage", ctypes.c_ulong), + ("th32ProcessID", ctypes.c_ulong), + ("th32DefaultHeapID", ctypes.c_ulong), + ("th32ModuleID", ctypes.c_ulong), + ("cntThreads", ctypes.c_ulong), + ("th32ParentProcessID", ctypes.c_ulong), + ("pcPriClassBase", ctypes.c_ulong), + ("dwFlags", ctypes.c_ulong), + ("szExeFile", ctypes.c_char * 260), + ] + + def __str__(self) -> str: return ( - 'szExeFile={} ' - 'th32ProcessID={} ' - 'cntThreads={} ' - 'cntUsage={} ' - 'dwFlags={} ' - 'dwSize={} ' - 'pcPriClassBase={} ' - 'th32DefaultHeapID={} ' - 'th32ModuleID={} ' - 'th32ParentProcessID={} ' - ''.format( + "szExeFile={} " + "th32ProcessID={} " + "cntThreads={} " + "cntUsage={} " + "dwFlags={} " + "dwSize={} " + "pcPriClassBase={} " + "th32DefaultHeapID={} " + "th32ModuleID={} " + "th32ParentProcessID={} " + "".format( self.szExeFile, self.th32ProcessID, self.cntThreads, @@ -51,7 +55,8 @@ def __str__(self): self.th32DefaultHeapID, self.th32ModuleID, self.th32ParentProcessID, - )) + ) + ) def process_list(): diff --git a/winapi__windows__ctypes/winapi__qt_get_icon_file_name.py b/winapi__windows__ctypes/winapi__qt_get_icon_file_name.py index 4379e4d3c..1ea015808 100644 --- a/winapi__windows__ctypes/winapi__qt_get_icon_file_name.py +++ b/winapi__windows__ctypes/winapi__qt_get_icon_file_name.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ctypes +from ctypes import wintypes + +from PySide.QtGui import QImage + +from win32gui import DrawIconEx, GetIconInfo def get_file_icon(path, large=True): - import ctypes SHGetFileInfo = ctypes.windll.shell32.SHGetFileInfoW SHGFI_ICON = 0x100 @@ -22,7 +26,8 @@ class SHFILEINFO(ctypes.Structure): ("iIcon", ctypes.c_int32), ("dwAttributes", ctypes.c_uint32), ("szDisplayName", ctypes.c_wchar * 260), - ("szTypeName", ctypes.c_wchar * 80)] + ("szTypeName", ctypes.c_wchar * 80), + ] info = SHFILEINFO() flags = SHGFI_ICON | SHGFI_SYSICONINDEX @@ -40,9 +45,6 @@ class SHFILEINFO(ctypes.Structure): return info.hIcon -from ctypes import wintypes - - class BITMAPINFOHEADER(ctypes.Structure): _fields_ = [ ("biSize", wintypes.DWORD), @@ -55,14 +57,12 @@ class BITMAPINFOHEADER(ctypes.Structure): ("biXPelsPerMeter", ctypes.c_long), ("biYPelsPerMeter", ctypes.c_long), ("biClrUsed", wintypes.DWORD), - ("biClrImportant", wintypes.DWORD) + ("biClrImportant", wintypes.DWORD), ] class BITMAPINFO(ctypes.Structure): - _fields_ = [ - ("bmiHeader", BITMAPINFOHEADER) - ] + _fields_ = [("bmiHeader", BITMAPINFOHEADER)] def qt_fromWinHBITMAP(hdc, h_bitmap, w, h): @@ -104,7 +104,6 @@ def qt_fromWinHBITMAP(hdc, h_bitmap, w, h): } """ - import ctypes GetDIBits = ctypes.windll.gdi32.GetDIBits DIB_RGB_COLORS = 0 BI_RGB = 0 @@ -118,7 +117,6 @@ def qt_fromWinHBITMAP(hdc, h_bitmap, w, h): bitmapInfo.bmiHeader.biCompression = BI_RGB bitmapInfo.bmiHeader.biSizeImage = w * h * 4 - from PySide.QtGui import QImage image = QImage(w, h, QImage.Format_ARGB32_Premultiplied) if image.isNull(): return image @@ -126,12 +124,23 @@ def qt_fromWinHBITMAP(hdc, h_bitmap, w, h): # Get bitmap bits data = ctypes.create_string_buffer(bitmapInfo.bmiHeader.biSizeImage) - if GetDIBits(hdc, h_bitmap, 0, h, ctypes.byref(data), ctypes.byref(bitmapInfo), DIB_RGB_COLORS): + if GetDIBits( + hdc, + h_bitmap, + 0, + h, + ctypes.byref(data), + ctypes.byref(bitmapInfo), + DIB_RGB_COLORS, + ): # Create image and copy data into image. for y in range(h): dest = image.scanLine(y) - src = data[y * image.bytesPerLine(): y * image.bytesPerLine() + image.bytesPerLine()] + src = data[ + y * image.bytesPerLine() : y * image.bytesPerLine() + + image.bytesPerLine() + ] for i in range(image.bytesPerLine()): dest[i] = src[i] @@ -216,7 +225,6 @@ def fromWinHICON(h_icon): } """ - import ctypes BI_RGB = 0 GetDC = ctypes.windll.user32.GetDC @@ -233,7 +241,6 @@ def fromWinHICON(h_icon): hdc = CreateCompatibleDC(screenDevice) ReleaseDC(0, screenDevice) - from win32gui import GetIconInfo iconinfo = GetIconInfo(h_icon) flag, xHotspot, yHotspot, hbmMask, hbmColor = iconinfo @@ -258,7 +265,6 @@ def fromWinHICON(h_icon): winBitmap = CreateDIBSection(hdc, ctypes.byref(bitmapInfo), DIB_RGB_COLORS, 0, 0, 0) oldhdc = SelectObject(hdc, winBitmap) - from win32gui import DrawIconEx DI_NORMAL = 0x0003 DrawIconEx(hdc, 0, 0, h_icon, w, h, 0, 0, DI_NORMAL) @@ -304,18 +310,19 @@ def fromWinHICON(h_icon): return image -if __name__ == '__main__': - h_icon = get_file_icon(r'C:\Users\ipetrash\Projects\alarm-clock\main.py') - h_icon = get_file_icon(r'C:\Users\ipetrash\Desktop\Будильник.lnk') - print('h_icon:', h_icon) - +if __name__ == "__main__": import sys from PySide.QtGui import QApplication + from win32gui import DestroyIcon + + h_icon = get_file_icon(r"C:\Users\ipetrash\Projects\alarm-clock\main.py") + h_icon = get_file_icon(r"C:\Users\ipetrash\Desktop\Будильник.lnk") + print("h_icon:", h_icon) + QApplication(sys.argv) px = fromWinHICON(h_icon) print(px, px.size()) - px.save('winapi_qt_get_icon_file_name.py.png') + px.save("winapi_qt_get_icon_file_name.py.png") - from win32gui import DestroyIcon DestroyIcon(h_icon) diff --git a/winapi__windows__ctypes/winapi__send_message.py b/winapi__windows__ctypes/winapi__send_message.py index e9647daa2..c21caf9e6 100644 --- a/winapi__windows__ctypes/winapi__send_message.py +++ b/winapi__windows__ctypes/winapi__send_message.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # TODO: почему-то не работает @@ -10,13 +10,14 @@ import win32api import win32con -hwnd = win32gui.FindWindow('TAIMPTrayControl', None) + +hwnd = win32gui.FindWindow("TAIMPTrayControl", None) print(hwnd) # hwnd = win32gui.FindWindow('TApplication', '') # print(hwnd) -hwnd = win32gui.FindWindow('TAIMPMainForm', None) +hwnd = win32gui.FindWindow("TAIMPMainForm", None) print(hwnd) key = win32con.VK_F2 diff --git a/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list.py b/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list.py index 4540c8ecc..b2d7d6de8 100644 --- a/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list.py +++ b/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list.py @@ -1,10 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import struct import ctypes +from ctypes.wintypes import POINT, RECT + +import commctrl +from commctrl import ( + LVIF_TEXT, + LVM_GETITEMTEXT, + LVM_GETITEMPOSITION, + LVIR_BOUNDS, + LVM_GETITEMRECT, +) + +from win32con import PROCESS_ALL_ACCESS, GW_CHILD, MEM_RESERVE, MEM_COMMIT, PAGE_READWRITE, MEM_RELEASE def GetDesktopListViewHandle(): @@ -27,7 +40,6 @@ def GetDesktopListViewHandle(): """ - import ctypes FindWindow = ctypes.windll.user32.FindWindowW GetWindow = ctypes.windll.user32.GetWindow @@ -36,14 +48,12 @@ def GetClassName(hwnd): ctypes.windll.user32.GetClassNameW(hwnd, buff, 99) return buff.value - from win32con import GW_CHILD - # Ищем окно с классом "Progman" ("Program Manager") - hwnd = FindWindow('Progman', None) + hwnd = FindWindow("Progman", None) hwnd = GetWindow(hwnd, GW_CHILD) # SHELLDLL_DefView hwnd = GetWindow(hwnd, GW_CHILD) # SysListView32 - if GetClassName(hwnd) != 'SysListView32': + if GetClassName(hwnd) != "SysListView32": return 0 return hwnd @@ -59,8 +69,6 @@ def ListView_GetItemCount(hwnd): """ - import commctrl - import ctypes SendMessage = ctypes.windll.user32.SendMessageW return SendMessage(hwnd, commctrl.LVM_GETITEMCOUNT, 0, 0) @@ -68,29 +76,25 @@ def ListView_GetItemCount(hwnd): class LVITEMW(ctypes.Structure): _fields_ = [ - ('mask', ctypes.c_uint32), - ('iItem', ctypes.c_int32), - ('iSubItem', ctypes.c_int32), - ('state', ctypes.c_uint32), - ('stateMask', ctypes.c_uint32), - ('pszText', ctypes.c_uint64), - ('cchTextMax', ctypes.c_int32), - ('iImage', ctypes.c_int32), - ('lParam', ctypes.c_uint64), # On 32 bit should be c_long - ('iIndent', ctypes.c_int32), - ('iGroupId', ctypes.c_int32), - ('cColumns', ctypes.c_uint32), - ('puColumns', ctypes.c_uint64), - ('piColFmt', ctypes.c_int64), - ('iGroup', ctypes.c_int32), + ("mask", ctypes.c_uint32), + ("iItem", ctypes.c_int32), + ("iSubItem", ctypes.c_int32), + ("state", ctypes.c_uint32), + ("stateMask", ctypes.c_uint32), + ("pszText", ctypes.c_uint64), + ("cchTextMax", ctypes.c_int32), + ("iImage", ctypes.c_int32), + ("lParam", ctypes.c_uint64), # On 32 bit should be c_long + ("iIndent", ctypes.c_int32), + ("iGroupId", ctypes.c_int32), + ("cColumns", ctypes.c_uint32), + ("puColumns", ctypes.c_uint64), + ("piColFmt", ctypes.c_int64), + ("iGroup", ctypes.c_int32), ] def get_desktop_process_handle(hwnd=None): - import ctypes - import struct - from win32con import PROCESS_ALL_ACCESS - GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId OpenProcess = ctypes.windll.kernel32.OpenProcess @@ -105,11 +109,6 @@ def get_desktop_process_handle(hwnd=None): def get_desktop_icons_list(): - import ctypes - from commctrl import LVIF_TEXT, LVM_GETITEMTEXT, LVM_GETITEMPOSITION, LVIR_BOUNDS, LVM_GETITEMRECT - from win32con import MEM_RESERVE, MEM_COMMIT, PAGE_READWRITE, MEM_RELEASE - from ctypes.wintypes import POINT, RECT - SendMessage = ctypes.windll.user32.SendMessageW VirtualAllocEx = ctypes.windll.kernel32.VirtualAllocEx WriteProcessMemory = ctypes.windll.kernel32.WriteProcessMemory @@ -124,7 +123,9 @@ def get_desktop_icons_list(): try: hwnd = GetDesktopListViewHandle() h_process = get_desktop_process_handle(hwnd) - buffer_txt = VirtualAllocEx(h_process, 0, MAX_LEN, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) + buffer_txt = VirtualAllocEx( + h_process, 0, MAX_LEN, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE + ) copied = ctypes.create_string_buffer(4) p_copied = ctypes.addressof(copied) @@ -135,30 +136,60 @@ def get_desktop_icons_list(): lvitem.cchTextMax = ctypes.c_int32(MAX_LEN) lvitem.iSubItem = ctypes.c_int32(0) - p_buffer_lvi = VirtualAllocEx(h_process, 0, MAX_LEN, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) - WriteProcessMemory(h_process, p_buffer_lvi, ctypes.addressof(lvitem), ctypes.sizeof(LVITEMW), p_copied) + p_buffer_lvi = VirtualAllocEx( + h_process, 0, MAX_LEN, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE + ) + WriteProcessMemory( + h_process, + p_buffer_lvi, + ctypes.addressof(lvitem), + ctypes.sizeof(LVITEMW), + p_copied, + ) num_items = ListView_GetItemCount(hwnd) - p_buffer_point = VirtualAllocEx(h_process, 0, ctypes.sizeof(POINT), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) - p_buffer_rect = VirtualAllocEx(h_process, 0, ctypes.sizeof(RECT), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) + p_buffer_point = VirtualAllocEx( + h_process, 0, ctypes.sizeof(POINT), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE + ) + p_buffer_rect = VirtualAllocEx( + h_process, 0, ctypes.sizeof(RECT), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE + ) for i in range(num_items): # Get icon text SendMessage(hwnd, LVM_GETITEMTEXT, i, p_buffer_lvi) target_bufftxt = ctypes.create_string_buffer(MAX_LEN) - ReadProcessMemory(h_process, buffer_txt, ctypes.addressof(target_bufftxt), MAX_LEN, p_copied) - name = target_bufftxt.value.decode('cp1251') + ReadProcessMemory( + h_process, + buffer_txt, + ctypes.addressof(target_bufftxt), + MAX_LEN, + p_copied, + ) + name = target_bufftxt.value.decode("cp1251") # Get icon position p = POINT() SendMessage(hwnd, LVM_GETITEMPOSITION, i, p_buffer_point) - ReadProcessMemory(h_process, p_buffer_point, ctypes.addressof(p), ctypes.sizeof(POINT), p_copied) + ReadProcessMemory( + h_process, + p_buffer_point, + ctypes.addressof(p), + ctypes.sizeof(POINT), + p_copied, + ) rect = RECT() rect.left = LVIR_BOUNDS SendMessage(hwnd, LVM_GETITEMRECT, i, p_buffer_rect) - ReadProcessMemory(h_process, p_buffer_rect, ctypes.addressof(rect), ctypes.sizeof(RECT), p_copied) + ReadProcessMemory( + h_process, + p_buffer_rect, + ctypes.addressof(rect), + ctypes.sizeof(RECT), + p_copied, + ) icons_list.append((i, name, p, rect)) @@ -191,14 +222,11 @@ def get_desktop_icons_list(): return icons_list -if __name__ == '__main__': +if __name__ == "__main__": icons_list = get_desktop_icons_list() # Сортировка по индексу for i, name, pos, rect in sorted(icons_list, key=lambda x: x[0]): - print('{0: >3}. "{1}": {2.x}x{2.y}, {3}x{4}'.format(i + 1, - name, - pos, - rect.right - rect.left, - rect.bottom - rect.top, - )) + print( + f'{i + 1: >3}. "{name}": {pos.x}x{pos.y}, {rect.right - rect.left}x{rect.bottom - rect.top}' + ) diff --git a/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list_gui.py b/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list_gui.py index db5e92ff0..5183d818a 100644 --- a/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list_gui.py +++ b/winapi__windows__ctypes/winapi_get_desktop_icon_list/get_desktop_icon_list_gui.py @@ -1,7 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import ctypes + +from ctypes.wintypes import RECT +from win32con import MONITOR_DEFAULTTONEAREST + +try: + from PyQt5.QtGui import * + from PyQt5.QtWidgets import * + from PyQt5.QtCore import * + +except: + try: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + + except: + from PySide.QtGui import * + from PySide.QtCore import * + +from get_desktop_icon_list import GetDesktopListViewHandle def get_desktop_resolution(): @@ -10,22 +32,17 @@ def get_desktop_resolution(): """ - from ctypes.wintypes import RECT - from win32con import MONITOR_DEFAULTTONEAREST - - import ctypes MonitorFromWindow = ctypes.windll.user32.MonitorFromWindow GetMonitorInfo = ctypes.windll.user32.GetMonitorInfoW class MONITORINFO(ctypes.Structure): _fields_ = [ - ('cbSize', ctypes.c_int), - ('rcMonitor', RECT), - ('rcWork', RECT), - ('dwFlags', ctypes.c_int), + ("cbSize", ctypes.c_int), + ("rcMonitor", RECT), + ("rcWork", RECT), + ("dwFlags", ctypes.c_int), ] - from get_desktop_icon_list import GetDesktopListViewHandle hwnd = GetDesktopListViewHandle() monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) info = MONITORINFO() @@ -37,22 +54,9 @@ class MONITORINFO(ctypes.Structure): return width, height -try: - from PyQt5.QtGui import * - from PyQt5.QtWidgets import * - from PyQt5.QtCore import * - -except: - try: - from PyQt4.QtGui import * - from PyQt4.QtCore import * - - except: - from PySide.QtGui import * - from PySide.QtCore import * - +if __name__ == "__main__": + from get_desktop_icon_list import get_desktop_icons_list -if __name__ == '__main__': app = QApplication([]) width_desktop, height_desktop = get_desktop_resolution() @@ -62,13 +66,12 @@ class MONITORINFO(ctypes.Structure): scene.addRect(scene_rect) scene.setSceneRect(scene_rect) - from get_desktop_icon_list import get_desktop_icons_list for i, name, pos, rect in get_desktop_icons_list(): x, y, w, h = pos.x, pos.y, rect.right - rect.left, rect.bottom - rect.top scene.addRect(x, y, w, h) view = QGraphicsView() - view.setWindowTitle('winapi_get_desktop_icon_list') + view.setWindowTitle("winapi_get_desktop_icon_list") view.setScene(scene) n = 3 diff --git a/winapi__windows__ctypes/windows__toast_balloontip_notifications/main.py b/winapi__windows__ctypes/windows__toast_balloontip_notifications/main.py deleted file mode 100644 index 983993f5c..000000000 --- a/winapi__windows__ctypes/windows__toast_balloontip_notifications/main.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://gist.github.com/wontoncc/1808234 -# -# Analog: https://github.com/K-DawG007/Stack-Watch/blob/master/windows_popup.py -# Analog: https://github.com/jithurjacob/Windows-10-Toast-Notifications - -from win32gui import * -import win32con -import sys -import os -import time -import uuid - - -class WindowsBalloonTip: - @staticmethod - def balloon_tip(title, msg, duration=5, icon_path_name=None): - message_map = { - win32con.WM_DESTROY: WindowsBalloonTip.on_destroy, - } - - # Register the Window class. - wc = WNDCLASS() - hinst = wc.hInstance = GetModuleHandle(None) - - # Random class name - wc.lpszClassName = str(uuid.uuid1()) - wc.lpfnWndProc = message_map # could also specify a wndproc. - - class_atom = RegisterClass(wc) - - # Create the Window. - style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU - hwnd = CreateWindow(class_atom, "Taskbar", style, 0, 0, - win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, - 0, 0, hinst, None) - UpdateWindow(hwnd) - - if not icon_path_name: - icon_path_name = os.path.abspath(os.path.join(sys.path[0], "balloontip.ico")) - - icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE - try: - hicon = LoadImage(hinst, icon_path_name, win32con.IMAGE_ICON, 0, 0, icon_flags) - except: - hicon = LoadIcon(0, win32con.IDI_APPLICATION) - - flags = NIF_ICON | NIF_MESSAGE | NIF_TIP - nid = (hwnd, 0, flags, win32con.WM_USER + 20, hicon, "tooltip") - Shell_NotifyIcon(NIM_ADD, nid) - Shell_NotifyIcon(NIM_MODIFY, - (hwnd, 0, NIF_INFO, win32con.WM_USER + 20, hicon, "Balloon tooltip", msg, 200, title)) - - time.sleep(duration) - - DestroyWindow(hwnd) - UnregisterClass(wc.lpszClassName, None) - - @staticmethod - def on_destroy(hwnd, msg, wparam, lparam): - nid = (hwnd, 0) - Shell_NotifyIcon(NIM_DELETE, nid) - PostQuitMessage(0) # Terminate the app. - - -if __name__ == '__main__': - WindowsBalloonTip.balloon_tip('First', 'My Text!', duration=2) - WindowsBalloonTip.balloon_tip('Second', 'My NEW Text!', duration=3) - WindowsBalloonTip.balloon_tip('Three', 'With invalid icons!', icon_path_name='fdfs.ico') diff --git a/winapi__windows__ctypes/windows__toast_balloontip_notifications/notifications.py b/winapi__windows__ctypes/windows__toast_balloontip_notifications/notifications.py new file mode 100644 index 000000000..cf785ea67 --- /dev/null +++ b/winapi__windows__ctypes/windows__toast_balloontip_notifications/notifications.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://gist.github.com/wontoncc/1808234 +# +# Analog: https://github.com/K-DawG007/Stack-Watch/blob/master/windows_popup.py +# Analog: https://github.com/jithurjacob/Windows-10-Toast-Notifications + + +import sys +import os +import time +import uuid + +import win32con +from win32gui import * + + +class WindowsBalloonTip: + @staticmethod + def balloon_tip(title, msg, duration=5, icon_path_name=None) -> None: + message_map = { + win32con.WM_DESTROY: WindowsBalloonTip.on_destroy, + } + + # Register the Window class. + wc = WNDCLASS() + hinst = wc.hInstance = GetModuleHandle(None) + + # Random class name + wc.lpszClassName = str(uuid.uuid1()) + wc.lpfnWndProc = message_map # could also specify a wndproc. + + class_atom = RegisterClass(wc) + + # Create the Window. + style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU + hwnd = CreateWindow( + class_atom, + "Taskbar", + style, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + hinst, + None, + ) + UpdateWindow(hwnd) + + if not icon_path_name: + icon_path_name = os.path.abspath( + os.path.join(sys.path[0], "balloontip.ico") + ) + + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + try: + hicon = LoadImage( + hinst, icon_path_name, win32con.IMAGE_ICON, 0, 0, icon_flags + ) + except: + hicon = LoadIcon(0, win32con.IDI_APPLICATION) + + flags = NIF_ICON | NIF_MESSAGE | NIF_TIP + nid = (hwnd, 0, flags, win32con.WM_USER + 20, hicon, "tooltip") + Shell_NotifyIcon(NIM_ADD, nid) + Shell_NotifyIcon( + NIM_MODIFY, + ( + hwnd, + 0, + NIF_INFO, + win32con.WM_USER + 20, + hicon, + "Balloon tooltip", + msg, + 200, + title, + ), + ) + + time.sleep(duration) + + DestroyWindow(hwnd) + UnregisterClass(wc.lpszClassName, None) + + @staticmethod + def on_destroy(hwnd, msg, wparam, lparam) -> None: + nid = (hwnd, 0) + Shell_NotifyIcon(NIM_DELETE, nid) + PostQuitMessage(0) # Terminate the app. + + +if __name__ == "__main__": + WindowsBalloonTip.balloon_tip("First", "My Text!", duration=2) + WindowsBalloonTip.balloon_tip("Second", "My NEW Text!", duration=3) + WindowsBalloonTip.balloon_tip( + "Three", "With invalid icons!", icon_path_name="fdfs.ico" + ) diff --git a/winapi__windows__ctypes/windows__toast_balloontip_notifications/run_notify.py b/winapi__windows__ctypes/windows__toast_balloontip_notifications/run_notify.py new file mode 100644 index 000000000..d078d1628 --- /dev/null +++ b/winapi__windows__ctypes/windows__toast_balloontip_notifications/run_notify.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from threading import Thread +from notifications import WindowsBalloonTip + + +def run(title: str, text: str, duration: int = 20) -> None: + WindowsBalloonTip.balloon_tip(title, text, duration) + + +# def run_in_process(title: str, text: str, duration: int = 20): +# Process(target=run, args=(title, text, duration)).start() + + +def run_in_thread(title: str, text: str, duration: int = 20) -> None: + Thread(target=run, args=(title, text, duration)).start() + + +if __name__ == "__main__": + run_in_thread("Уведомление1", "Проверь!!!!") + run_in_thread("Уведомление2", "Проверь!!!!") + run_in_thread("Уведомление3", "Проверь!!!!") + # Process(target=run, args=('Уведомление', 'Проверь!!!!'), daemon=True).start() + # run_in_process('Уведомление', 'Проверь 2!!!!') + # Process(target=run, args=('Уведомление', 'Проверь 3!!!!'), daemon=True).start() + # + # import time + # time.sleep(5) diff --git a/winapi__windows__ctypes/winsound__Beep.py b/winapi__windows__ctypes/winsound__Beep.py index 67c06e481..ea90fda72 100644 --- a/winapi__windows__ctypes/winsound__Beep.py +++ b/winapi__windows__ctypes/winsound__Beep.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import winsound + + winsound.Beep(1000, 500) diff --git a/winapi__windows__ctypes/winuser__EnumClipboardFormats.py b/winapi__windows__ctypes/winuser__EnumClipboardFormats.py new file mode 100644 index 000000000..b97245d80 --- /dev/null +++ b/winapi__windows__ctypes/winuser__EnumClipboardFormats.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import win32clipboard + + +VALUE_BY_FORMAT: dict[int, str] = { + getattr(win32clipboard, name): name + for name in dir(win32clipboard) + if name.startswith("CF_") +} + +win32clipboard.OpenClipboard() +try: + formats: list[int] = [] + last_format = 0 + while True: + fmt = win32clipboard.EnumClipboardFormats(last_format) + if not fmt: + break + + formats.append(fmt) + last_format = fmt + + for fmt in formats: + fmt_name = VALUE_BY_FORMAT.get(fmt) + if not fmt_name: + fmt_name = win32clipboard.GetClipboardFormatName(fmt) + + data = win32clipboard.GetClipboardData(fmt) + value = data[:100] if isinstance(data, (str, bytes)) else data + + print(f"{fmt_name} ({fmt}): size {len(data)}: {value}") + +finally: + win32clipboard.CloseClipboard() diff --git a/winreg__examples/_CreateKey_SetValue__examples.py b/winreg__examples/_CreateKey_SetValue__examples.py new file mode 100644 index 000000000..68f311956 --- /dev/null +++ b/winreg__examples/_CreateKey_SetValue__examples.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import winreg + + +key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software") +winreg.CreateKey(key, "SAMP") +winreg.CloseKey(key) + +key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\SAMP") +winreg.SetValue(key, "PlayerName", winreg.REG_SZ, "Simon") +winreg.CloseKey(key) + +keyValue = "Software\\SAMP\\PlayerName" +key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, keyValue, 0, winreg.KEY_ALL_ACCESS) +winreg.SetValueEx(key, "Name", None, winreg.REG_SZ, "SimonSimon") + +winreg.CloseKey(key) diff --git a/winreg__examples/_OpenKey_QueryValueEx__examples.py b/winreg__examples/_OpenKey_QueryValueEx__examples.py new file mode 100644 index 000000000..0f9e6f320 --- /dev/null +++ b/winreg__examples/_OpenKey_QueryValueEx__examples.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import winreg + + +def get_reg(reg_path, name): + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_READ + ) as registry_key: + value, regtype = winreg.QueryValueEx(registry_key, name) + return value + + except WindowsError: + return None + + +if __name__ == "__main__": + reg_path = r"Control Panel\Mouse" + print(get_reg(reg_path, "MouseSensitivity")) + # 10 diff --git a/winreg__examples/common.py b/winreg__examples/common.py new file mode 100644 index 000000000..353de325c --- /dev/null +++ b/winreg__examples/common.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import winreg + +from typing import Optional, Any +from pathlib import Path +from winreg import ( + QueryInfoKey, + EnumKey, + EnumValue, + OpenKey, + HKEYType, + REG_EXPAND_SZ, + ExpandEnvironmentStrings, +) + +from exceptions import RegistryKeyNotFoundException, RegistryValueNotFoundException +from constants import VALUE_BY_TYPE + + +def expand_registry_key(key: str) -> str: + return { + "HKCR": "HKEY_CLASSES_ROOT", + "HKCU": "HKEY_CURRENT_USER", + "HKLM": "HKEY_LOCAL_MACHINE", + "HCU": "HKEY_USERS", + }.get(key, key) + + +def expand_path(path: str) -> str: + registry_key_name, relative_path = path.split("\\", maxsplit=1) + registry_key_name = expand_registry_key(registry_key_name) + + return rf"{registry_key_name}\{relative_path}" + + +def get_key(path: str) -> Optional[HKEYType]: + # Example: + # path = r"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + # registry_key_name = "HKEY_LOCAL_MACHINE" + # relative_path = r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + registry_key_name, relative_path = path.split("\\", maxsplit=1) + registry_key_name = expand_registry_key(registry_key_name) + + registry_key = getattr(winreg, registry_key_name) + + try: + return OpenKey(registry_key, relative_path) + except: + return + + +class RegistryValue: + def __init__(self, name: str, value_type: int, value: Any) -> None: + self.name: str = name + + self.value_type: int = value_type + self.value_type_str: str = VALUE_BY_TYPE[value_type] + + if self.value_type == REG_EXPAND_SZ: + value = ExpandEnvironmentStrings(value) + + self.value: Any = value + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(name='{self.name}', type={self.value_type_str}, value={self.value})" + + +class RegistryKey: + def __init__(self, path: str) -> None: + path = expand_path(path) + + self.hkey: HKEYType = get_key(path) + if not self.hkey: + raise RegistryKeyNotFoundException(path) + + self.path: str = path + self.name: str = path.split("\\")[-1] + + number_of_keys, number_of_values, last_modified_timestamp = QueryInfoKey( + self.hkey + ) + self.number_of_keys: int = number_of_keys + self.number_of_values: int = number_of_values + self.last_modified_timestamp: int = last_modified_timestamp + + delta = dt.timedelta(seconds=self.last_modified_timestamp / 1e7) + self.last_modified: dt.datetime = dt.datetime(1601, 1, 1) + delta + + def __getitem__(self, name: str) -> RegistryValue: + return self.value(name) + + def __truediv__(self, sub_key_name: str) -> "RegistryKey": + return self.subkey(sub_key_name) + + def __eq__(self, other: "RegistryKey") -> bool: + return hash(self.path) == hash(other.path) + + def __hash__(self) -> int: + return hash(self.path) + + @classmethod + def get_subkey(cls, path: str, sub_key_name: str) -> "RegistryKey": + return cls(rf"{path}\{sub_key_name}") + + @classmethod + def get_or_none(cls, path: str) -> Optional["RegistryKey"]: + try: + return cls(path) + except RegistryKeyNotFoundException: + return + + def subkeys(self) -> list["RegistryKey"]: + items = [] + for i in range(self.number_of_keys): + sub_key_name = EnumKey(self.hkey, i) + items.append(RegistryKey.get_subkey(self.path, sub_key_name)) + return items + + def subkey(self, name: str) -> "RegistryKey": + for k in self.subkeys(): + if k.name.upper() == name.upper(): + return k + raise RegistryKeyNotFoundException(rf"{path}\{name}") + + def values(self) -> list[RegistryValue]: + items = [] + for i in range(self.number_of_values): + name, value, value_type = EnumValue(self.hkey, i) + items.append(RegistryValue(name=name, value_type=value_type, value=value)) + return items + + def value(self, name: str) -> RegistryValue: + for v in self.values(): + if v.name.upper() == name.upper(): + return v + raise RegistryValueNotFoundException(self.path, name) + + def get_raw_value(self, name: str, default: Any = None) -> Any: + try: + return self.value(name).value + except RegistryValueNotFoundException: + return default + + def get_str_value(self, name: str, default: str = "") -> str: + return str(self.get_raw_value(name, default)) + + def get_path_value(self, name: str) -> Optional[Path]: + path = self.get_str_value(name) + if not path: + return + + return Path(path) + + def get_raw_values_as_dict(self, default: Any = None) -> dict[str, Any]: + return {v.name: v.value if v.value else default for v in self.values()} + + def get_str_values_as_dict(self, default: str = "") -> dict[str, str]: + return { + name: str(value) if value else default + for name, value in self.get_raw_values_as_dict().items() + } + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(path='{self.path}', " + f"number_of_keys={self.number_of_keys}, number_of_values={self.number_of_values})" + ) + + +if __name__ == "__main__": + assert get_key( + r"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + ) + assert get_key( + r"HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + ) + + assert get_key( + r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + ) + assert get_key( + r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + ) + + assert ( + expand_path(r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run") + == r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" + ) + + path = r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" + key = RegistryKey(path) + print(key == RegistryKey(path)) + print(hash(key)) + print(hash(RegistryKey(path))) + print({key: "111"}) + print(key) + print(key.path) # TODO: добавить проверку + print(key.name) # TODO: добавить проверку + print(key.hkey) + print(key.number_of_keys) + print(key.number_of_values) + print(key.last_modified_timestamp) + print(key.last_modified) + print(len(key.subkeys()), key.subkeys()) + print(len(key.values()), key.values()) + print(key.value("Common Programs")) + print(key["Common Programs"]) + print(key.subkey("Backup")) + print(key / "Backup") + print(key.subkey("BACKUP")) + print(key.value("COMMON PROGRAMS")) + + # TODO: catch exception, use uuid + # print(key.subkey('111')) + # print(key / '111') + # print(key.value('111')) + # print(key['111']) diff --git a/winreg__examples/constants.py b/winreg__examples/constants.py new file mode 100644 index 000000000..ac91a3a3e --- /dev/null +++ b/winreg__examples/constants.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import winreg + + +# SOURCE: https://docs.python.org/3/library/winreg.html#value-types +TYPE_BY_VALUE: dict[str, int] = { + "REG_SZ": winreg.REG_SZ, + "REG_EXPAND_SZ": winreg.REG_EXPAND_SZ, + "REG_BINARY": winreg.REG_BINARY, + "REG_DWORD": winreg.REG_DWORD, + "REG_MULTI_SZ": winreg.REG_MULTI_SZ, + "REG_QWORD": winreg.REG_QWORD, + "REG_NONE": winreg.REG_NONE, + "REG_DWORD_BIG_ENDIAN": winreg.REG_DWORD_BIG_ENDIAN, + "REG_LINK": winreg.REG_LINK, + "REG_RESOURCE_LIST": winreg.REG_RESOURCE_LIST, + "REG_FULL_RESOURCE_DESCRIPTOR": winreg.REG_FULL_RESOURCE_DESCRIPTOR, + "REG_RESOURCE_REQUIREMENTS_LIST": winreg.REG_RESOURCE_REQUIREMENTS_LIST, +} +VALUE_BY_TYPE: dict[int, str] = {v: k for k, v in TYPE_BY_VALUE.items()} diff --git a/winreg__examples/exceptions.py b/winreg__examples/exceptions.py new file mode 100644 index 000000000..cd7643799 --- /dev/null +++ b/winreg__examples/exceptions.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +class RegistryException(Exception): + pass + + +class RegistryKeyNotFoundException(RegistryException): + def __init__(self, path: str) -> None: + self.path = path + + super().__init__(f"Registry key not found, path='{self.path}'") + + +class RegistryValueNotFoundException(RegistryException): + def __init__(self, path: str, name: str) -> None: + self.path = path + self.name = name + + super().__init__( + f"Registry value not found, path='{self.path}', name='{self.name}'" + ) diff --git a/winreg__examples/get_active_setup_installed_components.py b/winreg__examples/get_active_setup_installed_components.py new file mode 100644 index 000000000..442ee1b80 --- /dev/null +++ b/winreg__examples/get_active_setup_installed_components.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://helgeklein.com/blog/active-setup-explained/ +# https://windowsnotes.ru/registry-2/active-setup/ +# https://ru.stackoverflow.com/a/1351313/201445 + + +from dataclasses import dataclass, field +from typing import Any + +from common import RegistryKey + + +PATHS = [ + r"HKLM\Software\Microsoft\Active Setup\Installed Components", + r"HKLM\Software\Wow6432Node\Microsoft\Active Setup\Installed Components", + r"HKCU\Software\Microsoft\Active Setup\Installed Components", + r"HKCU\Software\Wow6432Node\Microsoft\Active Setup\Installed Components", +] + + +@dataclass +class Component: + guid: str + default: str = "" + is_installed: bool = True + locale: str = "" + stub_path: str = "" + version: str = "" + other_fields: dict[str, Any] = field(default_factory=dict) + + @classmethod + def create(cls, guid: str, fields: dict[str, Any]) -> "Component": + return cls( + guid=guid, + default=fields.pop("", ""), + is_installed=fields.pop("IsInstalled", True), + locale=fields.pop("Locale", ""), + stub_path=fields.pop("StubPath", ""), + version=fields.pop("Version", ""), + other_fields=fields, + ) + + +def get_active_setup_components(exists_stub_path=True) -> dict[str, list[Component]]: + path_by_items = dict() + + for path in PATHS: + key = RegistryKey(path) + path = key.path + + if path not in path_by_items: + path_by_items[path] = [] + + for sub_key in key.subkeys(): + component = Component.create( + sub_key.name, + sub_key.get_str_values_as_dict(), + ) + + # Если задана проверка наличия stub_path и он пустой + if exists_stub_path and not component.stub_path.strip(): + continue + + path_by_items[path].append(component) + + return path_by_items + + +if __name__ == "__main__": + + def _print_this(path_by_components: dict[str, list[Component]]) -> None: + for path, components in path_by_components.items(): + print(f"{path} ({len(components)}):") + for component in components: + print(f" {component}") + + print() + + path_by_components = get_active_setup_components() + _print_this(path_by_components) + + print("\n" + "-" * 100 + "\n") + + path_by_components = get_active_setup_components(exists_stub_path=False) + _print_this(path_by_components) diff --git a/winreg__examples/get_boot_execute.py b/winreg__examples/get_boot_execute.py new file mode 100644 index 000000000..f0ebd83d1 --- /dev/null +++ b/winreg__examples/get_boot_execute.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html + + +from common import RegistryKey + + +ROOT_PATH = r"HKLM\SYSTEM\CurrentControlSet\Control\Session Manager" +PATHS = [ + (ROOT_PATH, "BootExecute"), + (ROOT_PATH, "Execute"), + (ROOT_PATH, "SetupExecute"), +] + + +def get_boot_execute() -> dict[str, list[str]]: + key_by_value = dict() + for path, name in PATHS: + key = RegistryKey.get_or_none(path) + if not key: + continue + + if value := key.get_str_value(name): + key_by_value[f"{key.path}, {name}"] = value + + return key_by_value + + +if __name__ == "__main__": + print(get_boot_execute()) + # {'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager, BootExecute': ['autocheck autochk /m /P \\Device\\HarddiskVolume10', 'autocheck autochk /m /P \\Device\\HarddiskVolume38', 'autocheck autochk /m /P \\Device\\HarddiskVolume30', 'autocheck autochk *']} diff --git a/winreg__examples/get_by_path.py b/winreg__examples/get_by_path.py deleted file mode 100644 index 8336b5473..000000000 --- a/winreg__examples/get_by_path.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import winreg - - -def get_reg(reg_path, name): - try: - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_READ) as registry_key: - value, regtype = winreg.QueryValueEx(registry_key, name) - return value - - except WindowsError: - return None - - -if __name__ == '__main__': - reg_path = r"Control Panel\Mouse" - print(get_reg(reg_path, 'MouseSensitivity')) diff --git a/winreg__examples/get_command_processor.py b/winreg__examples/get_command_processor.py new file mode 100644 index 000000000..ce7be6440 --- /dev/null +++ b/winreg__examples/get_command_processor.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html +# SOURCE: http://datadump.ru/virus-detection/ + + +from common import RegistryKey + + +PATHS = [ + (r"HKLM\Software\Microsoft\Command Processor", "AutoRun"), + (r"HKCU\Software\Microsoft\Command Processor", "AutoRun"), +] + + +def get_command_processor() -> dict[str, str]: + path_by_value = dict() + for path, name in PATHS: + key = RegistryKey.get_or_none(path) + if not key: + continue + + if value := key.get_str_value(name): + path_by_value[rf"{key.path}, {name}"] = value + + return path_by_value + + +if __name__ == "__main__": + print(get_command_processor()) diff --git a/winreg__examples/get_current_user_sid.py b/winreg__examples/get_current_user_sid.py new file mode 100644 index 000000000..db3f2a73d --- /dev/null +++ b/winreg__examples/get_current_user_sid.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from common import RegistryKey + + +PATH = r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" +DIR_CURRENT_USER = str(Path("~").expanduser()) + + +def get_current_user_sid() -> str: + for key_sid in RegistryKey(PATH).subkeys(): + profile_image_path = key_sid.get_str_value("ProfileImagePath") + if profile_image_path == DIR_CURRENT_USER: + return key_sid.name + + raise Exception(f"Current user SID for {DIR_CURRENT_USER} not found!") + + +if __name__ == "__main__": + print(get_current_user_sid()) diff --git a/winreg__examples/get_default_browser.py b/winreg__examples/get_default_browser.py new file mode 100644 index 000000000..ec5ed541e --- /dev/null +++ b/winreg__examples/get_default_browser.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from winreg import OpenKey, HKEY_CURRENT_USER, HKEY_CLASSES_ROOT, QueryValueEx + + +def get_browser_command() -> str: + # SOURCE: https://stackoverflow.com/a/12444963/5909792 + path = ( + r"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice" + ) + with OpenKey(HKEY_CURRENT_USER, path) as key: + browser_id = QueryValueEx(key, "Progid")[0] + + path = browser_id + r"\shell\open\command" + with OpenKey(HKEY_CLASSES_ROOT, path) as key: + command = QueryValueEx(key, "")[0] + + return command + + +if __name__ == "__main__": + print(get_browser_command()) + # "C:\Program Files\Mozilla Firefox\firefox.exe" -osint -url "%1" diff --git a/winreg__examples/get_group_policy_scripts.py b/winreg__examples/get_group_policy_scripts.py new file mode 100644 index 000000000..0bc743afd --- /dev/null +++ b/winreg__examples/get_group_policy_scripts.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import RegistryKey + + +PATHS = [ + r"HKCU\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\Group Policy\Scripts", + r"HKCU\Software\Policies\Microsoft\Windows\System\Scripts", + r"HKLM\Software\Policies\Microsoft\Windows\System\Scripts", +] + + +def get_scripts() -> dict[str, str]: + path_by_value = dict() + + for path in PATHS: + key = RegistryKey.get_or_none(path) + if not key: + continue + + # Example: \Logon + for key_type in key.subkeys(): + # Example: \Logon\0 + for key_group in key_type.subkeys(): + file_sys_path = key_group.get_str_value("FileSysPath") + + # Example: \Logon\0\0 + for key_script in key_group.subkeys(): + script = key_script.get_str_value("Script") + parameters = key_script.get_str_value("Parameters") + value = f"FileSysPath={file_sys_path}, Script={script}, Parameters={parameters}" + + path_by_value[key_script.path] = value + + return path_by_value + + +if __name__ == "__main__": + for path, value in get_scripts().items(): + print(path, value) + # HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logon\0\0 FileSysPath=\\...\User, Script=domain.bat, Parameters= + # HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logon\1\0 FileSysPath=\\...\User, Script=start.bat, Parameters= diff --git a/winreg__examples/get_image_file_execution_options.py b/winreg__examples/get_image_file_execution_options.py new file mode 100644 index 000000000..0b1e2a466 --- /dev/null +++ b/winreg__examples/get_image_file_execution_options.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html +# SOURCE: http://datadump.ru/virus-detection/ + + +from common import RegistryKey + + +PATHS = [ + r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options", + r"HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Image File Execution Options", +] + + +def get_image_file_execution_options() -> dict[str, str]: + path_by_debuggers = dict() + for path in PATHS: + for sub_key in RegistryKey(path).subkeys(): + if debugger := sub_key.get_str_value("debugger"): + path_by_debuggers[sub_key.path] = debugger + + return path_by_debuggers + + +if __name__ == "__main__": + print(get_image_file_execution_options()) + # {'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\taskmgr.exe': '"C:\\Program Files (x86)\\System Explorer\\SystemExplorer.exe"', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\taskmgr.exe': '"C:\\Program Files (x86)\\System Explorer\\SystemExplorer.exe"'} diff --git a/winreg__examples/get_known_dlls.py b/winreg__examples/get_known_dlls.py new file mode 100644 index 000000000..e4ac1482e --- /dev/null +++ b/winreg__examples/get_known_dlls.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html + + +from common import RegistryKey + + +PATH = r"HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs" + + +def get_known_dlls() -> dict[str, str]: + key = RegistryKey(PATH) + return { + rf"{key.path}, {name}": value + for name, value in key.get_str_values_as_dict().items() + } + + +if __name__ == "__main__": + items = get_known_dlls() + print(len(items), items) + # 39 {'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, _wow64cpu': 'wow64cpu.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, _wowarmhw': 'wowarmhw.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, _xtajit': 'xtajit.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, advapi32': 'advapi32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, clbcatq': 'clbcatq.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, combase': 'combase.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, COMDLG32': 'COMDLG32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, coml2': 'coml2.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, DifxApi': 'difxapi.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, gdi32': 'gdi32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, gdiplus': 'gdiplus.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, IMAGEHLP': 'IMAGEHLP.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, IMM32': 'IMM32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, kernel32': 'kernel32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, MSCTF': 'MSCTF.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, MSVCRT': 'MSVCRT.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, NORMALIZ': 'NORMALIZ.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, NSI': 'NSI.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, ole32': 'ole32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, OLEAUT32': 'OLEAUT32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, PSAPI': 'PSAPI.DLL', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, rpcrt4': 'rpcrt4.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, sechost': 'sechost.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, Setupapi': 'Setupapi.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, SHCORE': 'SHCORE.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, SHELL32': 'SHELL32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, SHLWAPI': 'SHLWAPI.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, user32': 'user32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, WLDAP32': 'WLDAP32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, wow64': 'wow64.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, wow64win': 'wow64win.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, WS2_32': 'WS2_32.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, DllDirectory': 'C:\\Windows\\system32', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, DllDirectory32': 'C:\\Windows\\syswow64', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, IERTUTIL': 'IERTUTIL.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, LPK': 'LPK.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, URLMON': 'URLMON.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, USP10': 'USP10.dll', 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs, WININET': 'WININET.dll'} diff --git a/winreg__examples/get_processor_name.py b/winreg__examples/get_processor_name.py new file mode 100644 index 000000000..6dd9922e8 --- /dev/null +++ b/winreg__examples/get_processor_name.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import RegistryKey + + +PATH = r"HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor\0" + + +processor_name: str = RegistryKey.get_or_none(PATH).get_str_value("ProcessorNameString") +print(processor_name) +# Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz diff --git a/winreg__examples/get_processor_name_from_cmd.py b/winreg__examples/get_processor_name_from_cmd.py new file mode 100644 index 000000000..10e406996 --- /dev/null +++ b/winreg__examples/get_processor_name_from_cmd.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import subprocess + + +PATH = r"HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor\0" +VALUE = "ProcessorNameString" + +# Example: +# "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0 +# ProcessorNameString REG_SZ Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz" +result: str = subprocess.check_output( + rf'REG QUERY {PATH} /v {VALUE}', + encoding="utf-8", +) + +value = None +for line in result.splitlines(): + if VALUE in line: + value = line.split("REG_SZ")[1].strip() + break + +print(value) +# Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz diff --git a/winreg__examples/get_processor_name_from_powershell.py b/winreg__examples/get_processor_name_from_powershell.py new file mode 100644 index 000000000..3d944169e --- /dev/null +++ b/winreg__examples/get_processor_name_from_powershell.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import subprocess + + +PATH = r"HKLM:\HARDWARE\DESCRIPTION\System\CentralProcessor\0" +VALUE = "ProcessorNameString" + +result: str = subprocess.check_output( + rf'powershell -c "Get-ItemPropertyValue -Path {PATH} -Name {VALUE}"', + encoding="utf-8", +) +value = result.strip() +print(value) +# Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz diff --git a/winreg__examples/get_run_paths.py b/winreg__examples/get_run_paths.py new file mode 100644 index 000000000..2df6a3086 --- /dev/null +++ b/winreg__examples/get_run_paths.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.windxp.com.ru/autrun.htm +# SOURCE: http://www.infosecurity.ru/_gazeta/content/090904/art3.shtml +# SOURCE: https://www.saule-spb.ru/library/autorun.html + + +from collections import defaultdict +from common import RegistryKey + + +PATHS = [ + r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\Run", + r"HKCU\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run", + r"HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run", + + r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce", + r"HKCU\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", + r"HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", + + r"HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", + + r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnceEx", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnceEx", + r"HKCU\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnceEx", + r"HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnceEx", + + r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\Run", + r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\Runonce", + r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\RunonceEx", + + r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\Run", + r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\Runonce", + r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\RunonceEx", + + (r"HKLM\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp", "InitialProgram"), + (r"HKLM\System\CurrentControlSet\Control\Terminal Server\Wds\rdpwd", "StartupPrograms"), + + r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup", + + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows", "IconServiceLib"), + + (r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows", "Load"), + (r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows", "Run"), + + (r"HKCU\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "Shell"), + + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "AppSetup"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "GinaDLL"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "Shell"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "System"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "Taskman"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "UIHost"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "Userinit"), + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "VMApplet"), + r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\AlternateShells\AvailableShells", + + (r"HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot", "AlternateShell"), + (r"HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram", "ImageName"), + + (r"HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System", "Shell"), + (r"HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System", "Shell"), + + (r"HKCU\Environment", "UserInitMprLogonScript"), + (r"HKLM\Environment", "UserInitMprLogonScript"), + + r"HKLM\Software\Microsoft\Windows CE Services\AutoStartOnConnect", + r"HKLM\Software\Microsoft\Windows CE Services\AutoStartOnDisconnect", + + r"HKLM\Software\Wow6432Node\Microsoft\Windows CE Services\AutoStartOnConnect", + r"HKLM\Software\Wow6432Node\Microsoft\Windows CE Services\AutoStartOnDisconnect", + + (r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows", "AppInit_DLLs"), + (r"HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows", "AppInit_DLLs"), + + (r"HKCU\Control Panel\Desktop", "SCRNSAVE.EXE"), + (r"HKLM\SYSTEM\Setup", "CmdLine"), +] + + +def get_key_by_values() -> dict[str, dict[str, str]]: + path_by_values = defaultdict(dict) + + for path in PATHS: + if isinstance(path, tuple): + path, name = path + else: + name = None + + key = RegistryKey.get_or_none(path) + if not key: + continue + + path = key.path + + if name: + if value := key.get_str_value(name): + path_by_values[path][name] = value + else: + path_by_values[path].update(key.get_str_values_as_dict()) + + return path_by_values + + +def get_run_paths() -> dict[str, str]: + run_paths = dict() + for path, name_by_value in get_key_by_values().items(): + for name, value in name_by_value.items(): + run_paths[f"{path}, {name}"] = value + + return run_paths + + +if __name__ == "__main__": + for path, name_by_value in get_key_by_values().items(): + if not name_by_value: + continue + + print(path) + + for i, (name, value) in enumerate(name_by_value.items(), 1): + print(f" {i}. {name}: {value}") + + print() + r""" + HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run + 1. Lync: "C:\Program Files (x86)\Microsoft Office\Office15\lync.exe" /fromrunkey + 2. Zoom: "C:\Users\IPetrash\AppData\Roaming\Zoom\bin\Zoom.exe" + + HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run + 1. RTHDVCPL: "C:\Program Files\Realtek\Audio\HDA\RtkNGUI64.exe" -s + 2. DocFetcher-Daemon: C:\Program Files (x86)\DocFetcher\docfetcher-daemon-windows.exe + 3. egui: "C:\Program Files\ESET\ESET Security\ecmds.exe" /run /hide /proxy + 4. SecurityHealth: C:\Program Files\Windows Defender\MSASCuiL.exe + + HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Run + 1. Dropbox: "C:\Program Files (x86)\Dropbox\Client\Dropbox.exe" /systemstartup + 2. SystemExplorerAutoStart: "C:\Program Files (x86)\System Explorer\SystemExplorer.exe" /TRAY + 3. Zet Warrior: "C:\Program Files (x86)\Zet Warrior\Monitor.exe" + + HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon + 1. Shell: explorer.exe + 2. Userinit: C:\Windows\system32\userinit.exe, + + HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SafeBoot + 1. AlternateShell: cmd.exe + + """ + + run_paths = get_run_paths() + print(len(run_paths), run_paths) diff --git a/winreg__examples/get_startup_paths.py b/winreg__examples/get_startup_paths.py new file mode 100644 index 000000000..f17c9adcd --- /dev/null +++ b/winreg__examples/get_startup_paths.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fnmatch import fnmatch +from pathlib import Path + +from common import RegistryKey + + +PATHS = [ + (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "Common Startup"), + (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "Common AltStartup"), + (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", "Common Startup"), + (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", "Common AltStartup"), + + (r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "Startup"), + (r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "AltStartup"), + (r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", "Startup"), + (r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", "AltStartup"), +] + +DEFAULT_IGNORED_BY_MASK = ("*.ini",) + + +def get_path_files(path: Path, ignored_by_mask=DEFAULT_IGNORED_BY_MASK) -> list[Path]: + items = [] + if path and path.exists(): + for file in path.iterdir(): + if not file.is_file() or any( + fnmatch(file.name, mask) for mask in ignored_by_mask + ): + continue + + items.append(file) + + return items + + +def get_path_by_files(ignored_by_mask=DEFAULT_IGNORED_BY_MASK) -> dict[str, list[Path]]: + path_by_files = dict() + + for key_path, value in PATHS: + key = RegistryKey(key_path) + path = key.get_path_value(value) + path_by_files[f"{key.path}, {value}"] = get_path_files(path, ignored_by_mask) + + return path_by_files + + +def get_all_files(ignored_by_mask=DEFAULT_IGNORED_BY_MASK) -> list[Path]: + items = [] + + for files in get_path_by_files(ignored_by_mask).values(): + for file in files: + if file not in items: + items.append(file) + + return items + + +if __name__ == "__main__": + all_files = get_all_files() + print(f"All files ({len(all_files)}):") + for i, file in enumerate(all_files, 1): + print(f' {i:2}. "{file}"') diff --git a/winreg__examples/get_uninstall.py b/winreg__examples/get_uninstall.py new file mode 100644 index 000000000..32ead6795 --- /dev/null +++ b/winreg__examples/get_uninstall.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import RegistryKey + + +PATHS = [ + r"HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall", + r"HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall", + r"HKCU\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + r"HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall", +] + +programs = [] + +for path in PATHS: + key = RegistryKey.get_or_none(path) + if not key: + continue + + for sub_key in key.subkeys(): + name = sub_key.get_str_value("DisplayName") + if not name: + continue + + programs.append( + [ + name, + sub_key.get_str_value("DisplayVersion"), + sub_key.get_str_value("Publisher"), + sub_key.get_str_value("InstallDate"), + sub_key.name, # GUID + ] + ) + +# Sort by InstallDate +programs.sort(key=lambda x: x[3], reverse=True) + +for i, (name, version, publisher, install_date, _) in enumerate(programs, 1): + print(f"{i}. {name!r}, {version!r}, {publisher!r}, {install_date!r}") diff --git a/winreg__examples/get_winlogon_notify.py b/winreg__examples/get_winlogon_notify.py new file mode 100644 index 000000000..f7479ef9f --- /dev/null +++ b/winreg__examples/get_winlogon_notify.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html +# SOURCE: http://datadump.ru/virus-detection/ +# SOURCE: https://rsdn.org/article/baseserv/winlogon.xml +# SOURCE: https://www.saule-spb.ru/library/vx/look2mereg.html + + +from common import RegistryKey + + +PATH = r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify" + + +def get_winlogon_notify() -> dict[str, dict[str, str]]: + key = RegistryKey.get_or_none(PATH) + subkeys = key.subkeys() if key else [] + + return { + sub_key.path: sub_key.get_str_values_as_dict() + for sub_key in subkeys + } + + +if __name__ == "__main__": + print(get_winlogon_notify()) diff --git a/winreg__examples/get_winsock_providers.py b/winreg__examples/get_winsock_providers.py new file mode 100644 index 000000000..4b6905be6 --- /dev/null +++ b/winreg__examples/get_winsock_providers.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.saule-spb.ru/library/autorun.html +# SOURCE: http://datadump.ru/virus-detection/ + + +from common import RegistryKey + + +PATHS = [ + ( + r"HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\NameSpace_Catalog5", + "DisplayString", + ), + ( + r"HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Protocol_Catalog9", + "ProtocolName", + ), +] + + +def get_winsock_providers() -> dict[str, str]: + path_by_value = dict() + for path, name in PATHS: + for catalog in ["Catalog_Entries", "Catalog_Entries64"]: + key = RegistryKey(path) / catalog + for sub_key in key.subkeys(): + if value := sub_key.get_str_value(name): + path_by_value[sub_key.path] = value + + return path_by_value + + +if __name__ == "__main__": + path_by_value = get_winsock_providers() + print(path_by_value) + print() + + for path, value in path_by_value.items(): + print(path, value) + break + # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\NameSpace_Catalog5\Catalog_Entries\000000000001 @%SystemRoot%\system32\napinsp.dll,-1000 diff --git a/winreg__examples/printing_modified_value__for_AlienShooter2LegendPC.py b/winreg__examples/printing_modified_value__for_AlienShooter2LegendPC.py new file mode 100644 index 000000000..d010e7321 --- /dev/null +++ b/winreg__examples/printing_modified_value__for_AlienShooter2LegendPC.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import time + +from common import RegistryKey + + +key = RegistryKey(r"HKEY_CURRENT_USER\Software\SigmaTeam\AlienShooter2LegendPC") +name_by_value = key.get_str_values_as_dict() + +while True: + modified_keys = [] + + for v in key.values(): + name = v.name + + if name_by_value.get(name) != v.value: + modified_keys.append(name) + name_by_value[name] = v.value + + if modified_keys: + print(dt.datetime.now()) + print(*modified_keys, sep="\n") + print() + + time.sleep(5) diff --git a/winreg__examples/shell_bags_1_desktop.py b/winreg__examples/shell_bags_1_desktop.py new file mode 100644 index 000000000..37a246c5a --- /dev/null +++ b/winreg__examples/shell_bags_1_desktop.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import RegistryKey +from winapi__windows__ctypes.get_screen_info import get_screen_info + + +key = RegistryKey(r"HKCU\Software\Microsoft\Windows\Shell\Bags\1\Desktop") +values = key.values() +for v in values: + print(v) + +print() + +width, height, dpi = get_screen_info() + +print(f"ItemPos for current screen:") +for v in values: + name = f"ItemPos{width}x{height}x{dpi}" + if name in v.name: + print(v) diff --git a/winreg__examples/test.py b/winreg__examples/test.py deleted file mode 100644 index f97dcb7a6..000000000 --- a/winreg__examples/test.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import winreg - -key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software') -winreg.CreateKey(key, 'SAMP') -winreg.CloseKey(key) - -key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\SAMP') -winreg.SetValue(key, 'PlayerName', winreg.REG_SZ, "Simon") -winreg.CloseKey(key) - -keyValue = 'Software\\SAMP\\PlayerName' -key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, keyValue, 0, winreg.KEY_ALL_ACCESS) -winreg.SetValueEx(key, 'Name', None, winreg.REG_SZ, "SimonSimon") - -winreg.CloseKey(key) diff --git a/word_count/word_count.py b/word_count/word_count.py index 594d54eeb..453641684 100644 --- a/word_count/word_count.py +++ b/word_count/word_count.py @@ -2,37 +2,37 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт подсчитывает количество слов в тексте.""" -if __name__ == '__main__': - import re - from collections import Counter +import re +from collections import Counter - text = """Конечно, там еще много работы. Нужно корректно переводить слова с несколькими значениями, улучшать перевод - разных форм глаголов, сделать настраиваемый уровень перевода для разных уровней владения английским, готовить - упражнения для запоминания новых слов. Этим я буду заниматься на досуге в ближайшие месяцы. """ - # Ищем слова в кирилице - for i, match in enumerate(re.finditer(r'[а-яА-Я]+', text), 1): - print(i, match) +text = """Конечно, там еще много работы. Нужно корректно переводить слова с несколькими значениями, улучшать перевод +разных форм глаголов, сделать настраиваемый уровень перевода для разных уровней владения английским, готовить +упражнения для запоминания новых слов. Этим я буду заниматься на досуге в ближайшие месяцы. """ - print() +# Ищем слова в кирилице +for i, match in enumerate(re.finditer(r"[а-яА-Я]+", text), 1): + print(i, match) - # Разделяем текст по любым символам, кроме буквенного или цифрового символа или знака подчёркивания - found = re.split(r'\W+', text) - found = [c for c in found if c] - print(len(found), found) +print() - print() +# Разделяем текст по любым символам, кроме буквенного или цифрового символа или знака подчёркивания +found = re.split(r"\W+", text) +found = [c for c in found if c] +print(len(found), found) - # Ищем буквенные или цифровые символы или знаки подчёркивания, имеющие границу - words = re.findall(r"\b\w+\b", text) - print("Words: %s\nCount: %s" % (words, len(words))) +print() - word_count = Counter(words) - for word, c in word_count.items(): - print("'%s': %s" % (word, c)) +# Ищем буквенные или цифровые символы или знаки подчёркивания, имеющие границу +words = re.findall(r"\b\w+\b", text) +print("Words: %s\nCount: %s" % (words, len(words))) + +word_count = Counter(words) +for word, c in word_count.items(): + print("'%s': %s" % (word, c)) diff --git a/word_to_emoji/append_words_from/ru_wiktionary_org.py b/word_to_emoji/append_words_from/ru_wiktionary_org.py new file mode 100644 index 000000000..c415e7015 --- /dev/null +++ b/word_to_emoji/append_words_from/ru_wiktionary_org.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +sys.path.append(str(ROOT_DIR.parent)) +from html_parsing.ru_wiktionary_org__wiki__Список_частотных_слов_русского_языка_2013 import get_words + +import db + + +print("Before count:", db.Word2Emoji.select().count()) + +for word in get_words(): + db.Word2Emoji.add(word) + +print("After count:", db.Word2Emoji.select().count()) diff --git a/word_to_emoji/config.py b/word_to_emoji/config.py new file mode 100644 index 000000000..d9c0d5b8d --- /dev/null +++ b/word_to_emoji/config.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent diff --git a/word_to_emoji/database/database.sqlite b/word_to_emoji/database/database.sqlite new file mode 100644 index 000000000..60931e050 Binary files /dev/null and b/word_to_emoji/database/database.sqlite differ diff --git a/word_to_emoji/database/database.sqlite-shm b/word_to_emoji/database/database.sqlite-shm new file mode 100644 index 000000000..958975fc7 Binary files /dev/null and b/word_to_emoji/database/database.sqlite-shm differ diff --git a/word_to_emoji/database/database.sqlite-wal b/word_to_emoji/database/database.sqlite-wal new file mode 100644 index 000000000..0bc58b122 Binary files /dev/null and b/word_to_emoji/database/database.sqlite-wal differ diff --git a/word_to_emoji/db.py b/word_to_emoji/db.py new file mode 100644 index 000000000..0b07edcf9 --- /dev/null +++ b/word_to_emoji/db.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import sys +import time + +# pip install peewee +from peewee import Model, TextField, ForeignKeyField, CharField +from playhouse.sqliteq import SqliteQueueDatabase + +from word_to_emoji.config import DIR + +# Для импортирования shorten +sys.path.append(str(DIR.parent)) +from shorten import shorten + +from pymorphy2__examples.normal_form import get_normal_form + + +DB_DIR_NAME = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME = str(DB_DIR_NAME / "database.sqlite") + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +def preprocess_emoji(emoji: str) -> str: + if not emoji: + return emoji + + return re.sub(r"\s{2,}", "", emoji.strip()) + + +class BaseModel(Model): + class Meta: + database = db + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Word2Emoji(BaseModel): + word = TextField(unique=True) + emoji = TextField(null=True) + + @classmethod + def add(cls, word: str, emoji: str = None, convert_to_normal_form=True) -> None: + word = word.strip() + if convert_to_normal_form: + word = get_normal_form(word) + obj = cls.get_or_none(cls.word == word) + if obj: + emoji = preprocess_emoji(emoji) + if emoji: + obj.emoji = emoji + obj.save() + + else: + cls.create(word=word, emoji=emoji) + + @classmethod + def get_emoji(cls, word: str) -> str | None: + word = word.strip() + word = get_normal_form(word) + val = cls.select().where(cls.word == word).first() + if val: + return val.emoji + + @classmethod + def get_unprocessed_words(cls) -> list[str]: + return [x.word for x in cls.select().where(cls.emoji.is_null(True))] + + +db.connect() +db.create_tables([Word2Emoji]) + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + +if __name__ == "__main__": + Word2Emoji.add("любовь", "💏") + print(Word2Emoji.get_emoji("любовь")) + + Word2Emoji.add("любовь") + print(Word2Emoji.get_emoji("любовь")) + + print() + + items = Word2Emoji.get_unprocessed_words() + print( + f'Unprocessed words ({len(items)}): [{", ".join(map(repr, items[:10]))}, ...]' + ) + + print() + + for x in Word2Emoji.select().limit(5): + print(x) diff --git a/word_to_emoji/etc/removing_double_spaces_in_emoji.py b/word_to_emoji/etc/removing_double_spaces_in_emoji.py new file mode 100644 index 000000000..77f5bfc72 --- /dev/null +++ b/word_to_emoji/etc/removing_double_spaces_in_emoji.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +import db + + +for x in db.Word2Emoji.select().where(db.Word2Emoji.emoji.contains(" ")): + emoji = db.preprocess_emoji(x.emoji) + if emoji != x.emoji: + x.emoji = emoji + x.save() diff --git a/word_to_emoji/filling_via_emojipedia.py b/word_to_emoji/filling_via_emojipedia.py new file mode 100644 index 000000000..545b0e5f1 --- /dev/null +++ b/word_to_emoji/filling_via_emojipedia.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import traceback + +# pip install translate +from translate import Translator + +# pip install goslate +import goslate + +from word_to_emoji import db +from html_parsing.emojipedia_org__search import get_emoji + + +translator = Translator(from_lang="ru", to_lang="en") +gs = goslate.Goslate() + + +def ru2en(text: str) -> str: + text_en = translator.translate(text) + + # Если translate перестал работать попробуем через другой перевести + if ( + "MYMEMORY WARNING: YOU USED ALL AVAILABLE FREE TRANSLATIONS FOR TODAY" + in text_en + ): + text_en = gs.translate(text, "en") + + return text_en + + +while True: + for word_ru in reversed(db.Word2Emoji.get_unprocessed_words()): + # Если эмодзи уже есть + if db.Word2Emoji.get_emoji(word_ru): + continue + + while True: + try: + # На emojipedia поиск нужен на английском + word = ru2en(word_ru) + emoji = get_emoji(word) + print(f"Add {word_ru!r} ({word!r}) -> {emoji!r}") + if not emoji: + continue + + db.Word2Emoji.add(word_ru, emoji) + break + + except: + print(traceback.format_exc()) + time.sleep(5 * 60) + + finally: + time.sleep(5) + + time.sleep(5) diff --git a/word_to_emoji/filling_via_translate_yandex.py b/word_to_emoji/filling_via_translate_yandex.py new file mode 100644 index 000000000..1f4527bcc --- /dev/null +++ b/word_to_emoji/filling_via_translate_yandex.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import traceback + +# pip install selenium +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.webdriver.common.by import By + +from word_to_emoji import db + + +URL = "https://translate.yandex.ru/?lang=ru-emj&text=" + + +driver = webdriver.Firefox() +driver.implicitly_wait(5) + +try: + while True: + words = db.Word2Emoji.get_unprocessed_words() + print("Unprocessed words:", len(words)) + + for word in words: + # Если эмодзи уже есть + if db.Word2Emoji.get_emoji(word): + continue + + try: + url = URL + word + driver.get(url) + print(f"Title: {driver.title!r}") + + while True: + try: + emoji = driver.find_element( + By.CSS_SELECTOR, "#translation" + ).text.strip() + + # Перевод должен быть, если его нет, значит от сайта еще не пришел ответ + if not emoji: + time.sleep(2) + continue + + print(f"Add {word!r} -> {emoji!r}") + db.Word2Emoji.add(word, emoji) + break + + # Иногда элемент не будет доступен, например при запросе капчи + # Такие вещи нужно руками решать и пока они не решены, скрипт будет ожидать + except (TimeoutException, NoSuchElementException): + time.sleep(10) + + except: + print(traceback.format_exc()) + + finally: + time.sleep(5) + + time.sleep(5) + +finally: + driver.quit() diff --git a/word_to_emoji/text_to_emoji.py b/word_to_emoji/text_to_emoji.py new file mode 100644 index 000000000..d947734bf --- /dev/null +++ b/word_to_emoji/text_to_emoji.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + +# pip install pymorphy2 +import pymorphy2 +from pymorphy2.tokenizers import simple_word_tokenize + +from word_to_emoji import db + + +morph = pymorphy2.MorphAnalyzer() + + +def get_tokens(text: str, ignore_punctuations=False) -> list[pymorphy2.analyzer.Parse]: + tokens = [morph.parse(word)[0] for word in simple_word_tokenize(text)] + if ignore_punctuations: + tokens = [token for token in tokens if "PNCT" not in token.tag] + return tokens + + +def text_to_emoji(text: str) -> str: + for token in set(get_tokens(text, ignore_punctuations=True)): + word = token.word + emoji = db.Word2Emoji.get_emoji(word) + if emoji: + text = re.sub(rf"\b{re.escape(word)}\b", emoji, text, flags=re.IGNORECASE) + + return text + + +if __name__ == "__main__": + text = text_to_emoji("Смотри в оба глаза") + print(text) + print(repr(text)) + + print() + + text = ( + "xxx: у мальчиков есть воображаемые друзья, а у девочек - воображаемый жыр )))" + ) + text = text_to_emoji(text) + print(text) + + print(text_to_emoji("Собака хочет есть")) diff --git a/world_seed_in_binary_2D.py b/world_seed_in_binary_2D.py new file mode 100644 index 000000000..b7a2a5239 --- /dev/null +++ b/world_seed_in_binary_2D.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import hashlib +import random +import string + +from itertools import cycle + + +def get_random_seed(length: int = 8) -> str: + return "".join(random.choices(string.ascii_letters + string.digits, k=length)) + + +def get_bits_seed(seed: str) -> str: + seed = bytes(seed, encoding="utf-8") + return "".join(bin(c)[2:][:8].zfill(8) for c in hashlib.sha256(seed).digest()) + + +def create_world(rows: int, cols: int) -> list[list[int]]: + return [[0] * cols for _ in range(rows)] + + +def print_world(world: list[list[int]]) -> None: + print("\n".join(" ".join(map(str, row)) for row in world)) + + +def fill_world(world: list[list[int]], seed: str) -> None: + bits = get_bits_seed(seed) + bits = cycle(bits) + + for row in range(len(world)): + for col in range(len(world[0])): + world[row][col] = int(next(bits)) + + +if __name__ == "__main__": + for i in range(8, 64 + 1): + assert len(get_random_seed(length=i)) == i + + print("Random seed:", get_random_seed()) + print() + + assert ( + get_bits_seed("1") + == "0110101110000110101100100111001111111111001101001111110011100001100111010110101110000000010011101111111101011010001111110101011101000111101011011010010011101010101000100010111100011101010010011100000000011110010100101101110110110111100001110101101101001011" + ) + assert ( + get_bits_seed("123") + == "1010011001100101101001000101100100100000010000100010111110011101010000010111111001001000011001111110111111011100010011111011100010100000010010100001111100111111111111110001111110100000011111101001100110001110100001101111011111110111101000100111101011100011" + ) + + world = create_world(rows=5, cols=10) + print_world(world) + + print() + + fill_world(world, seed="123") + print_world(world) diff --git a/www_md5decryption_com__online.py b/www_md5decryption_com__online.py index 6522fcdda..385688348 100644 --- a/www_md5decryption_com__online.py +++ b/www_md5decryption_com__online.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import requests import re +import requests def md5decryption(hash_): - form_data = {'hash': hash_, 'submit': 'Decrypt It!'} - rs = requests.post('http://www.md5decryption.com/', data=form_data) + form_data = {"hash": hash_, "submit": "Decrypt It!"} + rs = requests.post("http://www.md5decryption.com/", data=form_data) - match = re.search('Decrypted Text:(.+)', rs.text) + match = re.search("Decrypted Text:(.+)", rs.text) # Если не нашли if not match: @@ -21,13 +21,13 @@ def md5decryption(hash_): text = match.group(1) # Выцепляем ответ (пример "
    kombat") - match = re.search('
    (.+?)', text) + match = re.search("(.+?)", text) if match: return match.group(1) return text -if __name__ == '__main__': - print(md5decryption('45af13298a22119fa84debdfc6b2d909')) # kombat - print(md5decryption('ed076287532e86365e841e92bfc50d8c')) # Hello World! +if __name__ == "__main__": + print(md5decryption("45af13298a22119fa84debdfc6b2d909")) # kombat + print(md5decryption("ed076287532e86365e841e92bfc50d8c")) # Hello World! diff --git a/www_prog_org_ru/login.py b/www_prog_org_ru/login.py index 9e94d2669..bdfcc9a2f 100644 --- a/www_prog_org_ru/login.py +++ b/www_prog_org_ru/login.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - from robobrowser import RoboBrowser - browser = RoboBrowser(parser='lxml') - browser.open('http://www.prog.org.ru/index.php') - signup_form = browser.get_form() - signup_form['user'].value = 'LOGIN' - signup_form['passwrd'].value = 'PASSWORD' +from robobrowser import RoboBrowser - # Submit the form - browser.submit_form(signup_form) - # Тут будет url перенаправления по которому нужно снова перейти - browser.open(browser.response.url) +browser = RoboBrowser(parser="lxml") +browser.open("http://www.prog.org.ru/index.php") - print('Здравствуйте,' in browser.response.text) - print(browser.select('.titlebg2')[0].text.strip()) +signup_form = browser.get_form() +signup_form["user"].value = "LOGIN" +signup_form["passwrd"].value = "PASSWORD" + +# Submit the form +browser.submit_form(signup_form) + +# Тут будет url перенаправления по которому нужно снова перейти +browser.open(browser.response.url) + +print("Здравствуйте," in browser.response.text) +print(browser.select(".titlebg2")[0].text.strip()) diff --git a/x == 1 and x == 2 and x == 3.py b/x == 1 and x == 2 and x == 3.py index 9f90338bb..a67655a4d 100644 --- a/x == 1 and x == 2 and x == 3.py +++ b/x == 1 and x == 2 and x == 3.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class X: - def __eq__(self, other): + def __eq__(self, other) -> bool: return True diff --git a/xbox_controller_xinput__examples/get_battery.py b/xbox_controller_xinput__examples/get_battery.py new file mode 100644 index 000000000..718336733 --- /dev/null +++ b/xbox_controller_xinput__examples/get_battery.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from operator import attrgetter + +# SOURCE: https://github.com/r4dian/Xbox-Controller-for-Python/blob/6932394fe5170cb52e9eee4ee601aa9415ed036a/xinput.py +import xinput + + +joysticks = xinput.XInputJoystick.enumerate_devices() +device_numbers = list(map(attrgetter("device_number"), joysticks)) +print("Found %d devices: %s" % (len(joysticks), device_numbers)) + +if not joysticks: + sys.exit(0) + +j = joysticks[0] +print(j.get_battery_information()) +# ('Alkaline', 'Medium') diff --git a/xbox_controller_xinput__examples/set_vibration.py b/xbox_controller_xinput__examples/set_vibration.py new file mode 100644 index 000000000..732fe1b9e --- /dev/null +++ b/xbox_controller_xinput__examples/set_vibration.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time +from operator import attrgetter + +# SOURCE: https://github.com/r4dian/Xbox-Controller-for-Python/blob/6932394fe5170cb52e9eee4ee601aa9415ed036a/xinput.py +import xinput + + +joysticks = xinput.XInputJoystick.enumerate_devices() +device_numbers = list(map(attrgetter("device_number"), joysticks)) +print("Found %d devices: %s" % (len(joysticks), device_numbers)) + +if not joysticks: + sys.exit(0) + +j = joysticks[0] + +j.set_vibration(100, 100) + +time.sleep(2) + +j.set_vibration(0, 0) diff --git a/xbox_controller_xinput__examples/xinput.py b/xbox_controller_xinput__examples/xinput.py new file mode 100644 index 000000000..cfb393708 --- /dev/null +++ b/xbox_controller_xinput__examples/xinput.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python + +""" +A module for getting input from Microsoft XBox 360 controllers via the XInput library on Windows. + +Adapted from Jason R. Coombs' code here: +http://pydoc.net/Python/jaraco.input/1.0.1/jaraco.input.win32.xinput/ +under the MIT licence terms + +Upgraded to Python 3 +Modified to add deadzones, reduce noise, and support vibration +Only req is Pyglet 1.2alpha1 or higher: +pip install --upgrade http://pyglet.googlecode.com/archive/tip.zip +""" + +import ctypes +import sys +import time +from operator import itemgetter, attrgetter +from itertools import count, starmap +from pyglet import event + +# structs according to +# http://msdn.microsoft.com/en-gb/library/windows/desktop/ee417001%28v=vs.85%29.aspx + + +class XINPUT_GAMEPAD(ctypes.Structure): + _fields_ = [ + ('buttons', ctypes.c_ushort), # wButtons + ('left_trigger', ctypes.c_ubyte), # bLeftTrigger + ('right_trigger', ctypes.c_ubyte), # bLeftTrigger + ('l_thumb_x', ctypes.c_short), # sThumbLX + ('l_thumb_y', ctypes.c_short), # sThumbLY + ('r_thumb_x', ctypes.c_short), # sThumbRx + ('r_thumb_y', ctypes.c_short), # sThumbRy + ] + + +class XINPUT_STATE(ctypes.Structure): + _fields_ = [ + ('packet_number', ctypes.c_ulong), # dwPacketNumber + ('gamepad', XINPUT_GAMEPAD), # Gamepad + ] + + +class XINPUT_VIBRATION(ctypes.Structure): + _fields_ = [("wLeftMotorSpeed", ctypes.c_ushort), + ("wRightMotorSpeed", ctypes.c_ushort)] + +class XINPUT_BATTERY_INFORMATION(ctypes.Structure): + _fields_ = [("BatteryType", ctypes.c_ubyte), + ("BatteryLevel", ctypes.c_ubyte)] + +xinput = ctypes.windll.xinput1_4 +#xinput = ctypes.windll.xinput9_1_0 # this is the Win 8 version ? +# xinput1_2, xinput1_1 (32-bit Vista SP1) +# xinput1_3 (64-bit Vista SP1) + + +def struct_dict(struct): + """ + take a ctypes.Structure and return its field/value pairs + as a dict. + + >>> 'buttons' in struct_dict(XINPUT_GAMEPAD) + True + >>> struct_dict(XINPUT_GAMEPAD)['buttons'].__class__.__name__ + 'CField' + """ + get_pair = lambda field_type: ( + field_type[0], getattr(struct, field_type[0])) + return dict(list(map(get_pair, struct._fields_))) + + +def get_bit_values(number, size=32): + """ + Get bit values as a list for a given number + + >>> get_bit_values(1) == [0]*31 + [1] + True + + >>> get_bit_values(0xDEADBEEF) + [1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1] + + You may override the default word size of 32-bits to match your actual + application. + >>> get_bit_values(0x3, 2) + [1, 1] + + >>> get_bit_values(0x3, 4) + [0, 0, 1, 1] + """ + res = list(gen_bit_values(number)) + res.reverse() + # 0-pad the most significant bit + res = [0] * (size - len(res)) + res + return res + + +def gen_bit_values(number): + """ + Return a zero or one for each bit of a numeric value up to the most + significant 1 bit, beginning with the least significant bit. + """ + number = int(number) + while number: + yield number & 0x1 + number >>= 1 + +ERROR_DEVICE_NOT_CONNECTED = 1167 +ERROR_SUCCESS = 0 + + +class XInputJoystick(event.EventDispatcher): + """ + XInputJoystick + + A stateful wrapper, using pyglet event model, that binds to one + XInput device and dispatches events when states change. + + Example: + controller_one = XInputJoystick(0) + """ + max_devices = 4 + + def __init__(self, device_number, normalize_axes=True): + values = vars() + del values['self'] + self.__dict__.update(values) + + super(XInputJoystick, self).__init__() + + self._last_state = self.get_state() + self.received_packets = 0 + self.missed_packets = 0 + + # Set the method that will be called to normalize + # the values for analog axis. + choices = [self.translate_identity, self.translate_using_data_size] + self.translate = choices[normalize_axes] + + def translate_using_data_size(self, value, data_size): + # normalizes analog data to [0,1] for unsigned data + # and [-0.5,0.5] for signed data + data_bits = 8 * data_size + return float(value) / (2 ** data_bits - 1) + + def translate_identity(self, value, data_size=None): + return value + + def get_state(self): + "Get the state of the controller represented by this object" + state = XINPUT_STATE() + res = xinput.XInputGetState(self.device_number, ctypes.byref(state)) + if res == ERROR_SUCCESS: + return state + if res != ERROR_DEVICE_NOT_CONNECTED: + raise RuntimeError( + "Unknown error %d attempting to get state of device %d" % (res, self.device_number)) + # else return None (device is not connected) + + def is_connected(self): + return self._last_state is not None + + @staticmethod + def enumerate_devices(): + "Returns the devices that are connected" + devices = list( + map(XInputJoystick, list(range(XInputJoystick.max_devices)))) + return [d for d in devices if d.is_connected()] + + def set_vibration(self, left_motor, right_motor): + "Control the speed of both motors seperately" + # Set up function argument types and return type + XInputSetState = xinput.XInputSetState + XInputSetState.argtypes = [ctypes.c_uint, ctypes.POINTER(XINPUT_VIBRATION)] + XInputSetState.restype = ctypes.c_uint + + vibration = XINPUT_VIBRATION( + int(left_motor * 65535), int(right_motor * 65535)) + XInputSetState(self.device_number, ctypes.byref(vibration)) + + def get_battery_information(self): + "Get battery type & charge level" + BATTERY_DEVTYPE_GAMEPAD = 0x00 + BATTERY_DEVTYPE_HEADSET = 0x01 + # Set up function argument types and return type + XInputGetBatteryInformation = xinput.XInputGetBatteryInformation + XInputGetBatteryInformation.argtypes = [ctypes.c_uint, ctypes.c_ubyte, ctypes.POINTER(XINPUT_BATTERY_INFORMATION)] + XInputGetBatteryInformation.restype = ctypes.c_uint + + battery = XINPUT_BATTERY_INFORMATION(0,0) + XInputGetBatteryInformation(self.device_number, BATTERY_DEVTYPE_GAMEPAD, ctypes.byref(battery)) + + #define BATTERY_TYPE_DISCONNECTED 0x00 + #define BATTERY_TYPE_WIRED 0x01 + #define BATTERY_TYPE_ALKALINE 0x02 + #define BATTERY_TYPE_NIMH 0x03 + #define BATTERY_TYPE_UNKNOWN 0xFF + #define BATTERY_LEVEL_EMPTY 0x00 + #define BATTERY_LEVEL_LOW 0x01 + #define BATTERY_LEVEL_MEDIUM 0x02 + #define BATTERY_LEVEL_FULL 0x03 + batt_type = "Unknown" if battery.BatteryType == 0xFF else ["Disconnected", "Wired", "Alkaline","Nimh"][battery.BatteryType] + level = ["Empty", "Low", "Medium", "Full"][battery.BatteryLevel] + return batt_type, level + + def dispatch_events(self): + "The main event loop for a joystick" + state = self.get_state() + if not state: + raise RuntimeError( + "Joystick %d is not connected" % self.device_number) + if state.packet_number != self._last_state.packet_number: + # state has changed, handle the change + self.update_packet_count(state) + self.handle_changed_state(state) + self._last_state = state + + def update_packet_count(self, state): + "Keep track of received and missed packets for performance tuning" + self.received_packets += 1 + missed_packets = state.packet_number - \ + self._last_state.packet_number - 1 + if missed_packets: + self.dispatch_event('on_missed_packet', missed_packets) + self.missed_packets += missed_packets + + def handle_changed_state(self, state): + "Dispatch various events as a result of the state changing" + self.dispatch_event('on_state_changed', state) + self.dispatch_axis_events(state) + self.dispatch_button_events(state) + + def dispatch_axis_events(self, state): + # axis fields are everything but the buttons + axis_fields = dict(XINPUT_GAMEPAD._fields_) + axis_fields.pop('buttons') + for axis, type in list(axis_fields.items()): + old_val = getattr(self._last_state.gamepad, axis) + new_val = getattr(state.gamepad, axis) + data_size = ctypes.sizeof(type) + old_val = self.translate(old_val, data_size) + new_val = self.translate(new_val, data_size) + + # an attempt to add deadzones and dampen noise + # done by feel rather than following http://msdn.microsoft.com/en-gb/library/windows/desktop/ee417001%28v=vs.85%29.aspx#dead_zone + # ags, 2014-07-01 + if ((old_val != new_val and (new_val > 0.08000000000000000 or new_val < -0.08000000000000000) and abs(old_val - new_val) > 0.00000000500000000) or + (axis == 'right_trigger' or axis == 'left_trigger') and new_val == 0 and abs(old_val - new_val) > 0.00000000500000000): + self.dispatch_event('on_axis', axis, new_val) + + def dispatch_button_events(self, state): + changed = state.gamepad.buttons ^ self._last_state.gamepad.buttons + changed = get_bit_values(changed, 16) + buttons_state = get_bit_values(state.gamepad.buttons, 16) + changed.reverse() + buttons_state.reverse() + button_numbers = count(1) + changed_buttons = list( + filter(itemgetter(0), list(zip(changed, button_numbers, buttons_state)))) + tuple(starmap(self.dispatch_button_event, changed_buttons)) + + def dispatch_button_event(self, changed, number, pressed): + self.dispatch_event('on_button', number, pressed) + + # stub methods for event handlers + def on_state_changed(self, state): + pass + + def on_axis(self, axis, value): + pass + + def on_button(self, button, pressed): + pass + + def on_missed_packet(self, number): + pass + +list(map(XInputJoystick.register_event_type, [ + 'on_state_changed', + 'on_axis', + 'on_button', + 'on_missed_packet', +])) + + +def determine_optimal_sample_rate(joystick=None): + """ + Poll the joystick slowly (beginning at 1 sample per second) + and monitor the packet stream for missed packets, indicating + that the sample rate is too slow to avoid missing packets. + Missed packets will translate to a lost information about the + joystick state. + As missed packets are registered, increase the sample rate until + the target reliability is reached. + """ + # in my experience, you want to probe at 200-2000Hz for optimal + # performance + if joystick is None: + joystick = XInputJoystick.enumerate_devices()[0] + + j = joystick + + print("Move the joystick or generate button events characteristic of your app") + print("Hit Ctrl-C or press button 6 (<, Back) to quit.") + + # here I use the joystick object to store some state data that + # would otherwise not be in scope in the event handlers + + # begin at 1Hz and work up until missed messages are eliminated + j.probe_frequency = 1 # Hz + j.quit = False + j.target_reliability = .99 # okay to lose 1 in 100 messages + + @j.event + def on_button(button, pressed): + # flag the process to quit if the < button ('back') is pressed. + j.quit = (button == 6 and pressed) + + @j.event + def on_missed_packet(number): + print('missed %(number)d packets' % vars()) + total = j.received_packets + j.missed_packets + reliability = j.received_packets / float(total) + if reliability < j.target_reliability: + j.missed_packets = j.received_packets = 0 + j.probe_frequency *= 1.5 + + while not j.quit: + j.dispatch_events() + time.sleep(1.0 / j.probe_frequency) + print("final probe frequency was %s Hz" % j.probe_frequency) + + +def sample_first_joystick(): + """ + Grab 1st available gamepad, logging changes to the screen. + L & R analogue triggers set the vibration motor speed. + """ + joysticks = XInputJoystick.enumerate_devices() + device_numbers = list(map(attrgetter('device_number'), joysticks)) + + print('found %d devices: %s' % (len(joysticks), device_numbers)) + + if not joysticks: + sys.exit(0) + + j = joysticks[0] + print('using %d' % j.device_number) + + battery = j.get_battery_information() + print(battery) + + @j.event + def on_button(button, pressed): + print('button', button, pressed) + + left_speed = 0 + right_speed = 0 + + @j.event + def on_axis(axis, value): + left_speed = 0 + right_speed = 0 + + print('axis', axis, value) + if axis == "left_trigger": + left_speed = value + elif axis == "right_trigger": + right_speed = value + j.set_vibration(left_speed, right_speed) + + while True: + j.dispatch_events() + time.sleep(.01) + + +if __name__ == "__main__": + sample_first_joystick() + # determine_optimal_sample_rate() diff --git a/xml_html__xpath__css_selector__gui/main.py b/xml_html__xpath__css_selector__gui/main.py index 0f6713eb4..b75782410 100644 --- a/xml_html__xpath__css_selector__gui/main.py +++ b/xml_html__xpath__css_selector__gui/main.py @@ -1,70 +1,81 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys import traceback - -from PyQt5 import Qt - -from lxml import etree -from lxml import html - -# pip install cssselect +from PyQt5.QtWidgets import ( + QWidget, + QLineEdit, + QRadioButton, + QLabel, + QPlainTextEdit, + QSizePolicy, + QPushButton, + QButtonGroup, + QSplitter, + QHBoxLayout, + QVBoxLayout, + QErrorMessage, + QTextEdit, +) +from PyQt5.QtCore import Qt + +from lxml import etree, html + +# pip install cssselect==1.3.0 from lxml.cssselect import CSSSelector -def to_str(x): +def to_str(el: "Element") -> str: try: - # return etree.tounicode(x, method='html') - return etree.tostring(x, method='html', encoding='unicode') + return etree.tostring(el, method="html", encoding="unicode") except: - return x + return str(el) -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + Qt.QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -class MainWindow(Qt.QWidget): - def __init__(self): +class MainWindow(QWidget): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('xml_html__xpath__css_selector__gui') + self.setWindowTitle("xml_html__xpath__css_selector__gui") - self.le_xpath_css = Qt.QLineEdit() + self.le_xpath_css = QLineEdit() - self.rb_xpath = Qt.QRadioButton('XPath') - self.rb_css_selector = Qt.QRadioButton('CSS selector') + self.rb_xpath = QRadioButton("XPath") + self.rb_css_selector = QRadioButton("CSS selector") self.rb_css_selector.setChecked(True) - self.text_edit_input = Qt.QPlainTextEdit() - self.text_edit_output = Qt.QPlainTextEdit() + self.text_edit_input = QPlainTextEdit() + self.text_edit_output = QPlainTextEdit() - self.label_error = Qt.QLabel() + self.label_error = QLabel() self.label_error.setStyleSheet("QLabel { color : red; }") - self.label_error.setTextInteractionFlags(Qt.Qt.TextSelectableByMouse) - self.label_error.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Preferred) + self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = Qt.QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message: str = "" + self.last_detail_error_message: str = "" self.le_xpath_css.textEdited.connect(self.on_process) self.rb_xpath.clicked.connect(self.on_process) @@ -72,38 +83,39 @@ def __init__(self): self.text_edit_input.textChanged.connect(self.on_process) self.button_detail_error.clicked.connect(self.show_detail_error_message) - self.rb_parser_html = Qt.QRadioButton('HTML') + self.rb_parser_html = QRadioButton("HTML") self.rb_parser_html.setChecked(True) - self.rb_parser_xml = Qt.QRadioButton('XML') - self.button_parser_group = Qt.QButtonGroup() + self.rb_parser_xml = QRadioButton("XML") + + self.button_parser_group = QButtonGroup() self.button_parser_group.addButton(self.rb_parser_xml) self.button_parser_group.addButton(self.rb_parser_html) self.button_parser_group.buttonClicked.connect(self.on_process) - splitter = Qt.QSplitter() - splitter.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding) + splitter = QSplitter() + splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) splitter.addWidget(self.text_edit_input) splitter.addWidget(self.text_edit_output) - layout_xpath_css = Qt.QHBoxLayout() + layout_xpath_css = QHBoxLayout() layout_xpath_css.addWidget(self.le_xpath_css) layout_xpath_css.addWidget(self.rb_xpath) layout_xpath_css.addWidget(self.rb_css_selector) - layout = Qt.QVBoxLayout() + layout = QVBoxLayout() layout.addLayout(layout_xpath_css) layout.addWidget(splitter) - layout_input_parser = Qt.QHBoxLayout() - layout_input_parser.addWidget(Qt.QLabel('Parser:')) + layout_input_parser = QHBoxLayout() + layout_input_parser.addWidget(QLabel("Parser:")) layout_input_parser.addWidget(self.rb_parser_html) layout_input_parser.addWidget(self.rb_parser_xml) layout_input_parser.addStretch() layout.addLayout(layout_input_parser) - layout_error = Qt.QHBoxLayout() + layout_error = QHBoxLayout() layout_error.addWidget(self.label_error) layout_error.addWidget(self.button_detail_error) @@ -111,13 +123,13 @@ def __init__(self): self.setLayout(layout) - def on_process(self): + def on_process(self) -> None: self.text_edit_output.clear() self.label_error.clear() self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message = "" + self.last_detail_error_message = "" try: text = self.text_edit_input.toPlainText() @@ -136,19 +148,19 @@ def on_process(self): if self.rb_xpath.isChecked(): result = root.xpath(search_text) else: - selector = CSSSelector(search_text, translator='html' if is_html_parser else 'xml') + selector = CSSSelector( + search_text, + translator="html" if is_html_parser else "xml", + ) result = selector(root) print(len(result), result) result = map(to_str, result) - output = '\n'.join('{}. {}'.format(i, x) for i, x in enumerate(result, 1)) + output = "\n".join(f"{i}. {x}" for i, x in enumerate(result, 1)) self.text_edit_output.setPlainText(output) except Exception as e: - # # Выводим ошибку в консоль - # traceback.print_exc() - # Сохраняем в переменную tb = traceback.format_exc() @@ -156,30 +168,33 @@ def on_process(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message - mb = Qt.QErrorMessage() - mb.setWindowTitle('Error') + mb = QErrorMessage() + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. - mb.findChild(Qt.QTextEdit).setPlainText(message) + mb.findChild(QTextEdit).setPlainText(message) mb.exec_() -if __name__ == '__main__': - app = Qt.QApplication([]) +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication([]) mw = MainWindow() mw.resize(650, 500) mw.show() # For example - mw.text_edit_input.setPlainText('''\ + mw.text_edit_input.setPlainText( + """\ Простой хлеб @@ -188,7 +203,8 @@ def show_detail_error_message(self): Тёплая вода - ''') + """ + ) # //Ingredient[@amount=0.25] mw.le_xpath_css.setText("Ingredient[amount='0.25']") mw.on_process() diff --git a/xml_unescape.py b/xml_unescape.py index b23b5ad9a..a5285e00e 100644 --- a/xml_unescape.py +++ b/xml_unescape.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'Запрос информации у ГИС ГМП. Поддержка аппаратных ключей' - import html + + +text = "Запрос информации у ГИС ГМП. Поддержка аппаратных ключей" print(html.unescape(text)) # Запрос информации у ГИС ГМП. Поддержка аппаратных ключей diff --git a/xor_crypto.py b/xor_crypto.py index 0653fdfef..d85b44431 100644 --- a/xor_crypto.py +++ b/xor_crypto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from itertools import cycle @@ -14,7 +14,7 @@ def crypto_xor_1(message: str, secret: int) -> str: """ - return ''.join(chr(ord(c) ^ secret) for c in message) + return "".join(chr(ord(c) ^ secret) for c in message) def crypto_xor_2(message: str, secret: str) -> str: @@ -32,7 +32,7 @@ def crypto_xor_2(message: str, secret: str) -> str: new_chars.append(num_chr) - return ''.join(chr(c) for c in new_chars) + return "".join(chr(c) for c in new_chars) def crypto_xor_3(message: str, secret: str) -> str: @@ -57,7 +57,7 @@ def crypto_xor_3(message: str, secret: str) -> str: if i >= len(secret): i = 0 - return ''.join(chr(c) for c in new_chars) + return "".join(chr(c) for c in new_chars) # NOTE: Аналог crypto_xor_3 @@ -72,7 +72,7 @@ def crypto_xor_4(message: str, secret: str) -> str: """ - return ''.join(chr(ord(c) ^ ord(k)) for c, k in zip(message, cycle(secret))) + return "".join(chr(ord(c) ^ ord(k)) for c, k in zip(message, cycle(secret))) def encrypt_xor_hex(message: str, secret: str) -> str: @@ -81,7 +81,7 @@ def encrypt_xor_hex(message: str, secret: str) -> str: """ - return crypto_xor_4(message, secret).encode('utf-8').hex() + return crypto_xor_4(message, secret).encode("utf-8").hex() def decrypt_xor_hex(message_hex: str, secret: str) -> str: @@ -90,51 +90,51 @@ def decrypt_xor_hex(message_hex: str, secret: str) -> str: """ - message = bytes.fromhex(message_hex).decode('utf-8') + message = bytes.fromhex(message_hex).decode("utf-8") return crypto_xor_4(message, secret) -if __name__ == '__main__': - message = 'Hello World!' +if __name__ == "__main__": + message = "Hello World!" secret_num = 50 - secret_key = 'secret' + secret_key = "secret" - print('secret:', secret_num) + print("secret:", secret_num) encrypt_text = crypto_xor_1(message, secret_num) - print('Encrypt:', repr(encrypt_text)) + print("Encrypt:", repr(encrypt_text)) decrypt_text = crypto_xor_1(encrypt_text, secret_num) - print('Decrypt:', decrypt_text) + print("Decrypt:", decrypt_text) assert crypto_xor_1(crypto_xor_1(message, secret_num), secret_num) == message print() - print('secret:', secret_key) + print("secret:", secret_key) encrypt_text = crypto_xor_2(message, secret_key) - print('Encrypt:', repr(encrypt_text)) + print("Encrypt:", repr(encrypt_text)) decrypt_text = crypto_xor_2(encrypt_text, secret_key) - print('Decrypt:', decrypt_text) + print("Decrypt:", decrypt_text) assert crypto_xor_2(crypto_xor_2(message, secret_key), secret_key) == message print() - print('secret:', secret_key) + print("secret:", secret_key) encrypt_text = crypto_xor_3(message, secret_key) - print('Encrypt:', repr(encrypt_text)) + print("Encrypt:", repr(encrypt_text)) decrypt_text = crypto_xor_3(encrypt_text, secret_key) - print('Decrypt:', decrypt_text) + print("Decrypt:", decrypt_text) assert crypto_xor_3(crypto_xor_3(message, secret_key), secret_key) == message print() - print('secret:', secret_key) + print("secret:", secret_key) encrypt_text = crypto_xor_4(message, secret_key) - print('Encrypt:', repr(encrypt_text)) + print("Encrypt:", repr(encrypt_text)) decrypt_text = crypto_xor_4(encrypt_text, secret_key) - print('Decrypt:', decrypt_text) + print("Decrypt:", decrypt_text) assert crypto_xor_4(crypto_xor_4(message, secret_key), secret_key) == message print() - print('secret:', secret_key) + print("secret:", secret_key) encrypt_text = encrypt_xor_hex(message, secret_key) - print('Encrypt:', repr(encrypt_text)) + print("Encrypt:", repr(encrypt_text)) decrypt_text = decrypt_xor_hex(encrypt_text, secret_key) - print('Decrypt:', decrypt_text) + print("Decrypt:", decrypt_text) assert decrypt_xor_hex(encrypt_xor_hex(message, secret_key), secret_key) == message print() diff --git a/xsd_to_py__generateDS__examples/generate_py_from_xsd.cmd b/xsd_to_py__generateDS__examples/generate_py_from_xsd.cmd new file mode 100644 index 000000000..35563c1a9 --- /dev/null +++ b/xsd_to_py__generateDS__examples/generate_py_from_xsd.cmd @@ -0,0 +1 @@ +python generateDS.py -o task.py task.xsd \ No newline at end of file diff --git a/xsd_to_py__generateDS__examples/main.py b/xsd_to_py__generateDS__examples/main.py new file mode 100644 index 000000000..cfef1c637 --- /dev/null +++ b/xsd_to_py__generateDS__examples/main.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: http://www.davekuhlman.org/generateDS.html +# pip install generateDS + + +from pathlib import Path + +# Generate from generate_py_from_xsd.cmd +import task + + +dir_tasks = Path(r"C:\Windows\System32\Tasks") +files = [f for f in dir_tasks.rglob("*") if f.is_file()] +for f in files: + print(f) + task_obj = task.parse(f, silence=True) + actions = [] + actions += task_obj.Actions.Exec + actions += task_obj.Actions.ComHandler + actions += task_obj.Actions.SendEmail + actions += task_obj.Actions.ShowMessage + + print(f" URI: {task_obj.RegistrationInfo.URI}") + print(f" Enabled: {task_obj.Settings.Enabled}") + print(f" Actions ({len(actions)}):", actions) + print() diff --git a/xsd_to_py__generateDS__examples/task.xsd b/xsd_to_py__generateDS__examples/task.xsd new file mode 100644 index 000000000..0c3bad8d9 --- /dev/null +++ b/xsd_to_py__generateDS__examples/task.xsd @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ya_test_task.py b/ya_test_task.py index a9a9d7967..1466174be 100644 --- a/ya_test_task.py +++ b/ya_test_task.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -URL_GET_ALL_STOPS = 'http://mobileapps.krd.ru:9000/api/v2/db/stops' -URL_GET_STOP_ROUTE = 'http://mobileapps.krd.ru:9000/api/v2/db/routes?stopId=' - import requests +from PySide.QtGui import * +from PySide.QtCore import * + + +URL_GET_ALL_STOPS = "http://mobileapps.krd.ru:9000/api/v2/db/stops" +URL_GET_STOP_ROUTE = "http://mobileapps.krd.ru:9000/api/v2/db/routes?stopId=" + def get_all_stops(): """ @@ -19,10 +23,10 @@ def get_all_stops(): rs = requests.get(URL_GET_ALL_STOPS) rs = rs.json() - if rs['status'] != 200: - raise Exception(rs['message']) + if rs["status"] != 200: + raise Exception(rs["message"]) - return [(stop['id'], stop['name']) for stop in rs['data']] + return [(stop["id"], stop["name"]) for stop in rs["data"]] def get_stop_route(stop_id): @@ -36,20 +40,14 @@ def get_stop_route(stop_id): rs = requests.get(URL_GET_STOP_ROUTE + stop_id) rs = rs.json() - if rs['status'] != 200: - raise Exception(rs['message']) + if rs["status"] != 200: + raise Exception(rs["message"]) - return [(stop['shortName'], stop['name']) for stop in rs['data']] - - -import sys - -from PySide.QtGui import * -from PySide.QtCore import * + return [(stop["shortName"], stop["name"]) for stop in rs["data"]] class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.stop_list_widget = QListWidget() @@ -57,7 +55,7 @@ def __init__(self): self.route_list_widget = QListWidget() - self.fill_stops_list_button = QPushButton('Заполнить список остановок') + self.fill_stops_list_button = QPushButton("Заполнить список остановок") self.fill_stops_list_button.clicked.connect(self.fill_list_stops) layout = QHBoxLayout() @@ -71,7 +69,7 @@ def __init__(self): self.setLayout(main_layout) - def fill_list_stops(self): + def fill_list_stops(self) -> None: self.route_list_widget.clear() self.stop_list_widget.clear() @@ -84,7 +82,7 @@ def fill_list_stops(self): self.stop_list_widget.addItem(item) - def item_stop_click(self, item): + def item_stop_click(self, item) -> None: stop_id = item.data(Qt.UserRole) self.route_list_widget.clear() @@ -93,11 +91,13 @@ def item_stop_click(self, item): for data in get_stop_route(stop_id): short_name, name = data - item = QListWidgetItem('{}: {}'.format(short_name, name)) + item = QListWidgetItem(f"{short_name}: {name}") self.route_list_widget.addItem(item) -if __name__ == '__main__': +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) w = MainWindow() diff --git a/yaml__examples/double_vs_single_quotes.py b/yaml__examples/double_vs_single_quotes.py new file mode 100644 index 000000000..580f61326 --- /dev/null +++ b/yaml__examples/double_vs_single_quotes.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install PyYAML +import yaml + + +data = yaml.safe_load(r""" +double: "123\n456" +single: '123\n456' +""") + +import json +print(json.dumps(data, indent=4, ensure_ascii=False)) +r""" +{ + "double": "123\n456", + "single": "123\\n456" +} +""" \ No newline at end of file diff --git a/yaml__examples/inheritance_with_anchors.py b/yaml__examples/inheritance_with_anchors.py new file mode 100644 index 000000000..24f7f30aa --- /dev/null +++ b/yaml__examples/inheritance_with_anchors.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install PyYAML +import yaml + + +data = yaml.safe_load( + """ +__base_server: &__base_server + title: "base_server" + description: null + ip: "127.0.0.1" + ports: + - 80 + - 8080 + +server_1: + <<: *__base_server + ip: "localhost" + description: | + Server running on address + + See about + +server_2: + <<: *__base_server + ip: "localhost" + ports: [123] + """ +) + +import json +print(json.dumps(data, indent=4, ensure_ascii=False)) +""" +{ + "__base_server": { + "title": "base_server", + "description": null, + "ip": "127.0.0.1", + "ports": [ + 80, + 8080 + ] + }, + "server_1": { + "title": "base_server", + "description": "Server running on address\n\nSee about\n", + "ip": "localhost", + "ports": [ + 80, + 8080 + ] + }, + "server_2": { + "title": "base_server", + "description": null, + "ip": "localhost", + "ports": [ + 123 + ] + } +} +""" \ No newline at end of file diff --git a/yandex_map_api.py b/yandex_map_api.py index 9ead11a44..d61450d53 100644 --- a/yandex_map_api.py +++ b/yandex_map_api.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -api_key = None -url = 'https://search-maps.yandex.ru/v1/?text=Магнитогорск, бизнец-центра&type=biz&lang=ru_RU&apikey={}'.format(api_key) import requests + + +api_key = None +url = f"https://search-maps.yandex.ru/v1/?text=Магнитогорск, бизнец-центра&type=biz&lang=ru_RU&apikey={api_key}" print(requests.get(url).json()) diff --git a/yandex_search_img.py b/yandex_search_img.py index b18b2b387..463da18d9 100644 --- a/yandex_search_img.py +++ b/yandex_search_img.py @@ -1,21 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'Котята' -url = 'http://yandex.ru/images/search?text=' + text +from urllib.parse import urljoin import requests -rs = requests.get(url) -print(rs) - from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'lxml') -from urllib.parse import urljoin -for img in root.select('img.serp-item__thumb'): - url_img = urljoin(url, img['src']) - print(url_img) +def get_images(text: str) -> list[str]: + url = "https://yandex.ru/images/search?text=" + text + + rs = requests.get(url) + root = BeautifulSoup(rs.content, "html.parser") + + return [ + urljoin(rs.url, img["src"]) + for img in root.select("img.serp-item__thumb") + ] + + +if __name__ == "__main__": + text = "Котята" + urls = get_images(text) + print(f"Urls ({len(urls)})") + for i, url in enumerate(urls, 1): + print(f"{i}. {url}") diff --git a/youtube/download_video.py b/youtube/download_video.py index c6c6754f3..e8a624e31 100644 --- a/youtube/download_video.py +++ b/youtube/download_video.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -14,12 +14,13 @@ def download_youtube(url): try: retcode = ydl.download([url]) except youtube_dl.MaxDownloadsReached: - ydl.to_screen('--max-download limit reached, aborting.') + ydl.to_screen("--max-download limit reached, aborting.") retcode = 101 return retcode -url = 'bupTAbtQYic' + +url = "bupTAbtQYic" code = download_youtube(url) sys.exit(code) diff --git a/youtube/use_pafy_module.py b/youtube/use_pafy_module.py index 46ec658d0..26bb8df5f 100644 --- a/youtube/use_pafy_module.py +++ b/youtube/use_pafy_module.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://np1.github.io/pafy/ # https://github.com/mps-youtube/pafy import pafy -video = pafy.new('https://www.youtube.com/watch?v=lz4r-TtIOjA') + + +video = pafy.new("https://www.youtube.com/watch?v=lz4r-TtIOjA") print(video.title) print(video.rating) diff --git a/zalgo/zalgo.py b/zalgo/zalgo.py index c91a55367..195561b91 100644 --- a/zalgo/zalgo.py +++ b/zalgo/zalgo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Marak/zalgo.js/blob/master/zalgo.js @@ -11,42 +11,125 @@ SOUL = { "up": [ - '̍', '̎', '̄', '̅', - '̿', '̑', '̆', '̐', - '͒', '͗', '͑', '̇', - '̈', '̊', '͂', '̓', - '̈', '͊', '͋', '͌', - '̃', '̂', '̌', '͐', - '̀', '́', '̋', '̏', - '̒', '̓', '̔', '̽', - '̉', 'ͣ', 'ͤ', 'ͥ', - 'ͦ', 'ͧ', 'ͨ', 'ͩ', - 'ͪ', 'ͫ', 'ͬ', 'ͭ', - 'ͮ', 'ͯ', '̾', '͛', - '͆', '̚' + "̍", + "̎", + "̄", + "̅", + "̿", + "̑", + "̆", + "̐", + "͒", + "͗", + "͑", + "̇", + "̈", + "̊", + "͂", + "̓", + "̈", + "͊", + "͋", + "͌", + "̃", + "̂", + "̌", + "͐", + "̀", + "́", + "̋", + "̏", + "̒", + "̓", + "̔", + "̽", + "̉", + "ͣ", + "ͤ", + "ͥ", + "ͦ", + "ͧ", + "ͨ", + "ͩ", + "ͪ", + "ͫ", + "ͬ", + "ͭ", + "ͮ", + "ͯ", + "̾", + "͛", + "͆", + "̚", ], "down": [ - '̖', '̗', '̘', '̙', - '̜', '̝', '̞', '̟', - '̠', '̤', '̥', '̦', - '̩', '̪', '̫', '̬', - '̭', '̮', '̯', '̰', - '̱', '̲', '̳', '̹', - '̺', '̻', '̼', 'ͅ', - '͇', '͈', '͉', '͍', - '͎', '͓', '͔', '͕', - '͖', '͙', '͚', '̣' + "̖", + "̗", + "̘", + "̙", + "̜", + "̝", + "̞", + "̟", + "̠", + "̤", + "̥", + "̦", + "̩", + "̪", + "̫", + "̬", + "̭", + "̮", + "̯", + "̰", + "̱", + "̲", + "̳", + "̹", + "̺", + "̻", + "̼", + "ͅ", + "͇", + "͈", + "͉", + "͍", + "͎", + "͓", + "͔", + "͕", + "͖", + "͙", + "͚", + "̣", ], "mid": [ - '̕', '̛', '̀', '́', - '͘', '̡', '̢', '̧', - '̨', '̴', '̵', '̶', - '͜', '͝', '͞', - '͟', '͠', '͢', '̸', - '̷', '͡', ' ҉' - ] + "̕", + "̛", + "̀", + "́", + "͘", + "̡", + "̢", + "̧", + "̨", + "̴", + "̵", + "̶", + "͜", + "͝", + "͞", + "͟", + "͠", + "͢", + "̸", + "̷", + "͡", + " ҉", + ], } -ALL = SOUL['up'] + SOUL['down'] + SOUL['mid'] +ALL = SOUL["up"] + SOUL["down"] + SOUL["mid"] def _get_random_number(range: int) -> int: @@ -54,7 +137,7 @@ def _get_random_number(range: int) -> int: def he_comes(text: str, options: dict = None) -> str: - result = '' + result = "" options = options or dict() options["up"] = options.get("up", True) @@ -68,25 +151,21 @@ def he_comes(text: str, options: dict = None) -> str: result += l - counts = { - "up": 0, - "down": 0, - "mid": 0 - } - - if options['size'] == 'mini': - counts['up'] = _get_random_number(8) - counts['min'] = _get_random_number(2) - counts['down'] = _get_random_number(8) - - elif options['size'] == 'maxi': - counts['up'] = _get_random_number(16) + 3 - counts['min'] = _get_random_number(4) + 1 - counts['down'] = _get_random_number(64) + 3 + counts = {"up": 0, "down": 0, "mid": 0} + + if options["size"] == "mini": + counts["up"] = _get_random_number(8) + counts["min"] = _get_random_number(2) + counts["down"] = _get_random_number(8) + + elif options["size"] == "maxi": + counts["up"] = _get_random_number(16) + 3 + counts["min"] = _get_random_number(4) + 1 + counts["down"] = _get_random_number(64) + 3 else: - counts['up'] = _get_random_number(8) + 1 - counts['mid'] = _get_random_number(6) / 2 - counts['down'] = _get_random_number(8) + 1 + counts["up"] = _get_random_number(8) + 1 + counts["mid"] = _get_random_number(6) / 2 + counts["down"] = _get_random_number(8) + 1 for index in ["up", "mid", "down"]: for _ in range(counts[index]): @@ -96,8 +175,8 @@ def he_comes(text: str, options: dict = None) -> str: return result -if __name__ == '__main__': - for text in ["I'm zalgo", "it's a chain", 'zalgo, he comes']: +if __name__ == "__main__": + for text in ["I'm zalgo", "it's a chain", "zalgo, he comes"]: result = he_comes(text) print(text) print(result) diff --git a/zeep_examples__WSDL/hello_world.py b/zeep_examples__WSDL/hello_world.py index c33d9e522..64fb9f828 100644 --- a/zeep_examples__WSDL/hello_world.py +++ b/zeep_examples__WSDL/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mvantellingen/python-zeep @@ -10,6 +10,7 @@ # pip install zeep from zeep import Client + # client = Client('http://www.webservicex.net/ConvertSpeed.asmx?WSDL') # result = client.service.ConvertSpeed(100, 'kilometersPerhour', 'milesPerhour') # diff --git a/zelle_graphics/hello_world.py b/zelle_graphics/hello_world.py index 7b041997c..422c27f83 100644 --- a/zelle_graphics/hello_world.py +++ b/zelle_graphics/hello_world.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from graphics import * + + win = GraphWin("My Circle", 100, 100) c = Circle(Point(50, 50), 10) c.draw(win) diff --git a/zelle_graphics/pipette.py b/zelle_graphics/pipette.py index addd5d231..a4063453a 100644 --- a/zelle_graphics/pipette.py +++ b/zelle_graphics/pipette.py @@ -1,23 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://habrahabr.ru/post/346146/ +import time + # pip install pyautogui import pyautogui + from graphics import GraphWin, Circle, Point, Entry, color_rgb -import time -win = GraphWin("pipetka", 200, 200, autoflush=True) # создаем графическую форму размером 200х200 и элементы на ней + +win = GraphWin( + "pipetka", 200, 200, autoflush=True +) # создаем графическую форму размером 200х200 и элементы на ней x, y = pyautogui.position() # получаем в x, y координаты мыши r, g, b = pyautogui.pixel(x, y) # получаем в r, g, b цвет ColorDot = Circle(Point(100, 100), 25) # создаем точку, отображающую цвет -ColorDot.setFill(color_rgb(r, g, b)) # устанавливает ей заливку из ранее полученных цветов + +# Устанавливает ей заливку из ранее полученных цветов +ColorDot.setFill(color_rgb(r, g, b)) ColorDot.draw(win) # рисуем на форме win RGBtext = Entry(Point(win.getWidth() / 2, 25), 10) # создаем RGB вывод @@ -37,7 +44,7 @@ ColorDot.setFill(color_rgb(r, g, b)) # Обновляем цвет RGBtext.setText(pyautogui.pixel(x, y)) # Обновляем RGB RGBstring.setText(color_rgb(r, g, b)) # Обновляем web цвет - Coordstring.setText('{} x {}'.format(x, y)) # Обновляем координаты + Coordstring.setText(f"{x} x {y}") # Обновляем координаты if win.isClosed(): break diff --git a/zip_file_example/append_directory/dir_1/dir_1.1/dir_1.1.1/1.py b/zip_file_example/append_directory/dir_1/dir_1.1/dir_1.1.1/1.py index 06334aa61..f25732790 100644 --- a/zip_file_example/append_directory/dir_1/dir_1.1/dir_1.1.1/1.py +++ b/zip_file_example/append_directory/dir_1/dir_1.1/dir_1.1.1/1.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/zip_file_example/append_directory/dir_1/dir_2.1/dir_2.1.1/2.py b/zip_file_example/append_directory/dir_1/dir_2.1/dir_2.1.1/2.py index 06334aa61..f25732790 100644 --- a/zip_file_example/append_directory/dir_1/dir_2.1/dir_2.1.1/2.py +++ b/zip_file_example/append_directory/dir_1/dir_2.1/dir_2.1.1/2.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/zip_file_example/append_directory/main.py b/zip_file_example/append_directory/main.py index ed9b596c7..0d08fda08 100644 --- a/zip_file_example/append_directory/main.py +++ b/zip_file_example/append_directory/main.py @@ -1,18 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -DIR_NAME = 'dir_1' +import os +import zipfile -import zipfile -with zipfile.ZipFile(DIR_NAME + '.zip', mode='w', compression=zipfile.ZIP_DEFLATED) as zf: - import os - for root, dirs, files in os.walk(DIR_NAME): - for file in files: - file_name = os.path.join(root, file) - zf.write(file_name) +def make_zipfile(source_dir, output_filename) -> None: + relroot = os.path.abspath(os.path.join(source_dir, os.pardir)) + with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip: + for root, dirs, files in os.walk(source_dir): + # add directory (needed for empty dirs) + zip.write(root, os.path.relpath(root, relroot)) + for file in files: + filename = os.path.join(root, file) + if os.path.isfile(filename): # regular files only + arcname = os.path.join(os.path.relpath(root, relroot), file) + zip.write(filename, arcname) + - print(file_name) +DIR_NAME = "dir_1" +make_zipfile(DIR_NAME, DIR_NAME + ".zip") diff --git a/zip_file_example/append_file/main.py b/zip_file_example/append_file/main.py index 4a2556bb2..d29df314c 100644 --- a/zip_file_example/append_file/main.py +++ b/zip_file_example/append_file/main.py @@ -1,14 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Добавляем файл в архив import zipfile -with zipfile.ZipFile('out.zip', mode='w', compression=zipfile.ZIP_DEFLATED) as f: - f.write('file_1.txt') - f.write('sub_dir/file_1.1.txt', 'file_1.1.txt') - f.write('sub_dir/file_1.1.txt') # out.zip/sub_dir/file_1.1.txt - f.write('sub_dir/file_1.1.txt', 'new_sub_dir/file_1.1.txt') # out.zip/new_sub_dir/file_1.1.txt + +with zipfile.ZipFile("out.zip", mode="w", compression=zipfile.ZIP_DEFLATED) as f: + f.write("file_1.txt") + f.write("sub_dir/file_1.1.txt", "file_1.1.txt") + + f.write("sub_dir/file_1.1.txt") + # out.zip/sub_dir/file_1.1.txt + + f.write( + "sub_dir/file_1.1.txt", "new_sub_dir/file_1.1.txt" + ) + # out.zip/new_sub_dir/file_1.1.txt diff --git a/zip_file_example/download_volume_readmanga.py b/zip_file_example/download_volume_readmanga.py index f34008fc6..8aedeff5d 100644 --- a/zip_file_example/download_volume_readmanga.py +++ b/zip_file_example/download_volume_readmanga.py @@ -1,47 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для скачивания главы по указанному url.""" +import re +import os +import zipfile + +from urllib.request import urlretrieve + +import requests + + def get_url_images(url): - print('Start get_url_images with url:', url) + print("Start get_url_images with url:", url) - import requests rs = requests.get(url) - pattern = '\.init\(.*(\[\[.+\]\]).*\)' - - import re + pattern = "\.init\(.*(\[\[.+\]\]).*\)" match = re.search(pattern, rs.text) if match: match = match.group(1) - print('Match:', match) + print("Match:", match) # NOTE: если погуглить у меня примеры то можно найти более лучшие чем eval: через json или ast urls = eval(match) - print('After eval:', urls) + print("After eval:", urls) return [i[1] + i[0] + i[2] for i in urls] - raise Exception('Не получилось из страницы вытащить список картинок главы. ' - 'Используемое регулярное выражение: ', pattern) + raise Exception( + "Не получилось из страницы вытащить список картинок главы. " + "Используемое регулярное выражение: ", + pattern, + ) -def save_urls_to_zip(zip_file_name, urls): +def save_urls_to_zip(zip_file_name, urls) -> None: if not urls: - print('Cписок изображений пустой.') + print("Cписок изображений пустой.") return # Создаем архив, у которого именем будет номер главы - import zipfile - with zipfile.ZipFile(zip_file_name, mode='w', compression=zipfile.ZIP_DEFLATED) as f: - import os - from urllib.request import urlretrieve - + with zipfile.ZipFile( + zip_file_name, mode="w", compression=zipfile.ZIP_DEFLATED + ) as f: for img_url in urls: # Вытаскиваем имя файла file_name = os.path.basename(img_url) @@ -59,19 +66,19 @@ def save_urls_to_zip(zip_file_name, urls): os.remove(file_name) -if __name__ == '__main__': - url = 'http://readmanga.me/one__piece/vol60/591' +if __name__ == "__main__": + import traceback + + url = "http://readmanga.me/one__piece/vol60/591" try: urls = get_url_images(url) - print('Urls:', urls) - print('Images:', len(urls)) + print("Urls:", urls) + print("Images:", len(urls)) - import os - file_name = os.path.basename(url) + '.zip' + file_name = os.path.basename(url) + ".zip" save_urls_to_zip(file_name, urls) - print('Save to filename:', file_name) + print("Save to filename:", file_name) except Exception as e: - import traceback - print('Error: {}\n\n{}'.format(e, traceback.format_exc())) + print(f"Error: {e}\n\n{traceback.format_exc()}") diff --git a/zip_file_example/download_zip_and_extract_first_json_file_in_memory.py b/zip_file_example/download_zip_and_extract_first_json_file_in_memory.py index 246a669d8..a319072f8 100644 --- a/zip_file_example/download_zip_and_extract_first_json_file_in_memory.py +++ b/zip_file_example/download_zip_and_extract_first_json_file_in_memory.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import json +import io +import zipfile + import requests -rs = requests.get('https://op.mos.ru/EHDWSREST/catalog/export/get?id=84505') -import zipfile -import io + +rs = requests.get("https://op.mos.ru/EHDWSREST/catalog/export/get?id=84505") zip_data = io.BytesIO(rs.content) with zipfile.ZipFile(zip_data) as zip_file: @@ -16,9 +19,8 @@ print(json_file_name) json_data = zip_file.read(json_file_name) - json_data = json_data.decode('cp1251') + json_data = json_data.decode("cp1251") print(json_data[:50]) - import json obj = json.loads(json_data) - print(obj[0]['AdmArea']) + print(obj[0]["AdmArea"]) diff --git a/zip_file_example/extract_directory/main.py b/zip_file_example/extract_directory/main.py index 6adb11c50..1f81aeb03 100644 --- a/zip_file_example/extract_directory/main.py +++ b/zip_file_example/extract_directory/main.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -DIR_NAME = 'dir_1' +DIR_NAME = "dir_1" -if __name__ == '__main__': +if __name__ == "__main__": import zipfile - with zipfile.ZipFile(DIR_NAME + '.zip', mode='r') as zf: + + with zipfile.ZipFile(DIR_NAME + ".zip", mode="r") as zf: zf.extractall() diff --git a/zip_file_example/extract_file_in_memory/main.py b/zip_file_example/extract_file_in_memory/main.py index c2e22f500..c093f40ef 100644 --- a/zip_file_example/extract_file_in_memory/main.py +++ b/zip_file_example/extract_file_in_memory/main.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def sizeof_fmt(num): - for x in ['bytes', 'KB', 'MB', 'GB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) +import zipfile - num /= 1024.0 +from pathlib import Path - return "%3.1f %s" % (num, 'TB') +# pip install humanize +from humanize import naturalsize as sizeof_fmt -import zipfile -with zipfile.ZipFile('Doc_df7c89c378c04e8daf69257ea95d9a2e.zip') as f: - data_file = f.read('Doc_df7c89c378c04e8daf69257ea95d9a2e.html') +FILE_NAME = Path("Doc_df7c89c378c04e8daf69257ea95d9a2e.zip") + +print("Zip size:", sizeof_fmt(len(FILE_NAME.read_bytes()))) + + +with zipfile.ZipFile("Doc_df7c89c378c04e8daf69257ea95d9a2e.zip") as f: + data_file = f.read("Doc_df7c89c378c04e8daf69257ea95d9a2e.html") size = sizeof_fmt(len(data_file)) - print('Total: {}'.format(size)) - print('data_file[:100]: {}'.format(data_file[:100])) + print(f"File size: {size}") + print(f"data_file[:100]: {data_file[:100]}") diff --git a/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_all_but_included_files_from_xpi_zip_file.py b/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_all_but_included_files_from_xpi_zip_file.py index 2f2e54ef3..7794f864f 100644 --- a/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_all_but_included_files_from_xpi_zip_file.py +++ b/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_all_but_included_files_from_xpi_zip_file.py @@ -1,47 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -"""Плагин удаляет из архива все файлы кроме указанных. -xpi файл -- плагин для FireFox, является zip архивом.""" +""" +Плагин удаляет из архива все файлы кроме указанных. +xpi файл -- плагин для FireFox, является zip архивом. +""" -import os.path +import argparse import fnmatch -from zipfile import ZipFile - +import os.path -INCLUDE = ['data/*', 'index.js', 'bootstrap.js', 'package.json', 'install.rdf'] +from zipfile import ZipFile -import argparse +INCLUDE = ["data/*", "index.js", "bootstrap.js", "package.json", "install.rdf"] def create_parser(): - parser = argparse.ArgumentParser(description='Remove all but included files from zip.py.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('zip_file_name', type=str) - parser.add_argument('--include', nargs='*', default=INCLUDE) - parser.add_argument('--add_include', action='store_true') + parser = argparse.ArgumentParser( + description="Remove all but included files from zip.py.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("zip_file_name", type=str) + parser.add_argument("--include", nargs="*", default=INCLUDE) + parser.add_argument("--add_include", action="store_true") return parser.parse_args() -def do(zip_file_name, include): - print('zip_file_name:', zip_file_name) - print('Include files:', include) +def do(zip_file_name, include) -> None: + print("zip_file_name:", zip_file_name) + print("Include files:", include) # Измененный zip - out_zip_file_name = '_' + zip_file_name + out_zip_file_name = "_" + zip_file_name - try: - print('open {} and {} zip arhives'.format(zip_file_name, out_zip_file_name)) - zin = ZipFile(zip_file_name, 'r') - zout = ZipFile(out_zip_file_name, 'w') - - print('start fill {} zip arhive'.format(out_zip_file_name)) + print(f"open {zip_file_name} and {out_zip_file_name} zip arhives") + with ZipFile(zip_file_name, "r") as zin, ZipFile(out_zip_file_name, "w") as zout: + print(f"start fill {out_zip_file_name} zip arhive") for item in zin.infolist(): buffer = zin.read(item.filename) @@ -49,24 +49,20 @@ def do(zip_file_name, include): if any((fnmatch.fnmatch(item.filename, pattern) for pattern in include)): zout.writestr(item, buffer) else: - print('Delete', item.filename) - - print('finish fill {} zip arhive'.format(out_zip_file_name)) + print("Delete", item.filename) - finally: - zout.close() - zin.close() + print(f"finish fill {out_zip_file_name} zip arhive") - # Удаляем оригинальный - print('remove original {} zip file'.format(zip_file_name)) - os.remove(zip_file_name) + # Удаляем оригинальный + print(f"remove original {zip_file_name} zip file") + os.remove(zip_file_name) - # Переименновываем измененный zip в оригинальный - print('rename {} zip file as original {}'.format(out_zip_file_name, zip_file_name)) - os.rename(out_zip_file_name, zip_file_name) + # Переименовываем измененный zip в оригинальный + print(f"rename {out_zip_file_name} zip file as original {zip_file_name}") + os.rename(out_zip_file_name, zip_file_name) -if __name__ == '__main__': +if __name__ == "__main__": args = create_parser() include = set(INCLUDE + args.include if args.add_include else args.include) diff --git a/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_exclude_files_from_xpi_zip_file.py b/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_exclude_files_from_xpi_zip_file.py index 6ed6b7f4e..6249333d6 100644 --- a/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_exclude_files_from_xpi_zip_file.py +++ b/zip_file_example/remove_unnecessary_files_from_xpi_zip_file/remove_exclude_files_from_xpi_zip_file.py @@ -1,69 +1,65 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -"""Плагин удаляет из архива указанные в exclude файлы. -xpi файл -- плагин для FireFox, является zip архивом.""" +""" +Плагин удаляет из архива указанные в exclude файлы. +xpi файл -- плагин для FireFox, является zip архивом. +""" +import argparse import os.path -from zipfile import ZipFile +from zipfile import ZipFile -EXCLUDE = ['README.md', 'run.bat', 'xpi.bat'] - -import argparse +EXCLUDE = ["README.md", "run.bat", "xpi.bat"] def create_parser(): - parser = argparse.ArgumentParser(description='Remove unnecessary files from zip.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('zip_file_name', type=str) - parser.add_argument('--exclude', nargs='*', default=EXCLUDE) - parser.add_argument('--add_exclude', action='store_true') + parser = argparse.ArgumentParser( + description="Remove unnecessary files from zip.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("zip_file_name", type=str) + parser.add_argument("--exclude", nargs="*", default=EXCLUDE) + parser.add_argument("--add_exclude", action="store_true") return parser.parse_args() -def do(zip_file_name, exclude): - print('zip_file_name:', zip_file_name) +def do(zip_file_name, exclude) -> None: + print("zip_file_name:", zip_file_name) - print('Delete files:', EXCLUDE) + print("Delete files:", EXCLUDE) # Измененный zip - out_zip_file_name = '_' + zip_file_name - - try: - print('open {} and {} zip arhives'.format(zip_file_name, out_zip_file_name)) - zin = ZipFile(zip_file_name, 'r') - zout = ZipFile(out_zip_file_name, 'w') + out_zip_file_name = "_" + zip_file_name - print('start fill {} zip arhive'.format(out_zip_file_name)) + print(f"open {zip_file_name} and {out_zip_file_name} zip arhives") + with ZipFile(zip_file_name, "r") as zin, ZipFile(out_zip_file_name, "w") as zout: + print(f"start fill {out_zip_file_name} zip arhive") for item in zin.infolist(): buffer = zin.read(item.filename) if os.path.basename(item.filename) not in exclude: zout.writestr(item, buffer) - print('finish fill {} zip arhive'.format(out_zip_file_name)) - - finally: - zout.close() - zin.close() + print(f"finish fill {out_zip_file_name} zip arhive") - # Удаляем оригинальный - print('remove original {} zip file'.format(zip_file_name)) - os.remove(zip_file_name) + # Удаляем оригинальный + print(f"remove original {zip_file_name} zip file") + os.remove(zip_file_name) - # Переименновываем измененный zip в оригинальный - print('rename {} zip file as original {}'.format(out_zip_file_name, zip_file_name)) - os.rename(out_zip_file_name, zip_file_name) + # Переименовываем измененный zip в оригинальный + print(f"rename {out_zip_file_name} zip file as original {zip_file_name}") + os.rename(out_zip_file_name, zip_file_name) -if __name__ == '__main__': +if __name__ == "__main__": args = create_parser() exclude = EXCLUDE + args.exclude if args.add_exclude else args.exclude diff --git a/zip_file_example/shutil_make_archive/dir_1/dir_1.1/dir_1.1.1/1.py b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/dir_1.1.1/1.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/dir_1.1.1/1.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/zip_file_example/shutil_make_archive/dir_1/dir_1.1/file_1.1 b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/file_1.1 new file mode 100644 index 000000000..e69de29bb diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" "b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" new file mode 100644 index 000000000..e69de29bb diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" "b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" new file mode 100644 index 000000000..6f26a7053 --- /dev/null +++ "b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\237\321\200\320\276\320\262\320\265\321\200\320\272\320\260/\320\222\321\202\320\276\321\200\320\260\321\217 \320\277\321\200\320\276\320\262\320\265\321\200\320\272\320\260" "b/zip_file_example/shutil_make_archive/dir_1/dir_1.1/\320\237\321\200\320\276\320\262\320\265\321\200\320\272\320\260/\320\222\321\202\320\276\321\200\320\260\321\217 \320\277\321\200\320\276\320\262\320\265\321\200\320\272\320\260" new file mode 100644 index 000000000..e69de29bb diff --git a/zip_file_example/shutil_make_archive/dir_1/dir_2.1/dir_2.1.1/2.py b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/dir_2.1.1/2.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/dir_2.1.1/2.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_2.1/dir_2.1.2/dir_2.1.2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" "b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/dir_2.1.2/dir_2.1.2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" new file mode 100644 index 000000000..e69de29bb diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" "b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202 2.txt" new file mode 100644 index 000000000..e69de29bb diff --git "a/zip_file_example/shutil_make_archive/dir_1/dir_2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" "b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" new file mode 100644 index 000000000..6f26a7053 --- /dev/null +++ "b/zip_file_example/shutil_make_archive/dir_1/dir_2.1/\320\235\320\276\320\262\321\213\320\271 \321\202\320\265\320\272\321\201\321\202\320\276\320\262\321\213\320\271 \320\264\320\276\320\272\321\203\320\274\320\265\320\275\321\202.txt" @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/zip_file_example/shutil_make_archive/dir_1/readme.txt b/zip_file_example/shutil_make_archive/dir_1/readme.txt new file mode 100644 index 000000000..d8b8eed9a --- /dev/null +++ b/zip_file_example/shutil_make_archive/dir_1/readme.txt @@ -0,0 +1,2 @@ +hello world + \ No newline at end of file diff --git a/zip_file_example/shutil_make_archive/main.py b/zip_file_example/shutil_make_archive/main.py new file mode 100644 index 000000000..aee9fea31 --- /dev/null +++ b/zip_file_example/shutil_make_archive/main.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shutil + + +DIR_NAME = "dir_1" +shutil.make_archive(DIR_NAME, "zip", DIR_NAME) diff --git a/zip_file_example/shutil_unpack_archive/main.py b/zip_file_example/shutil_unpack_archive/main.py new file mode 100644 index 000000000..fac5648ff --- /dev/null +++ b/zip_file_example/shutil_unpack_archive/main.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shutil + + +DIR_NAME = "../extract_directory/dir_1.zip" +OUTPUT = "dir_1" + +shutil.unpack_archive(DIR_NAME, OUTPUT) diff --git a/zip_file_example/zip_file_example.py b/zip_file_example/zip_file_example.py index 3211ac0d2..3768f7d9b 100644 --- a/zip_file_example/zip_file_example.py +++ b/zip_file_example/zip_file_example.py @@ -1,10 +1,12 @@ +__author__ = "ipetrash" + + import os import sys import zipfile -__author__ = 'ipetrash' -if __name__ == '__main__': +if __name__ == "__main__": print("Commands: read, readfile and write") command = input("Command: ") @@ -20,7 +22,6 @@ # zf.printdir() - elif command == "readfile": path = input("Archive: ") if not os.path.exists(path): @@ -51,4 +52,4 @@ else: print("Unknown command!") - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/zlib_example/zlib_example.py b/zlib_example/zlib_example.py index 1ad6b5b05..ffd8b0ade 100644 --- a/zlib_example/zlib_example.py +++ b/zlib_example/zlib_example.py @@ -1,22 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Data Compression (модуль zlib_example) import zlib -text = b'witch which has which witches wrist watch' -print('[{}]: {} (crc32: {})'.format(len(text), text, zlib.crc32(text))) + +text = b"witch which has which witches wrist watch" +print(f"[{len(text)}]: {text} (crc32: {zlib.crc32(text)})") print() -print('Compress...') +print("Compress...") compress_text = zlib.compress(text) -print('[{}]: {} (crc32: {})'.format(len(compress_text), compress_text, zlib.crc32(compress_text))) +print(f"[{len(compress_text)}]: {compress_text} (crc32: {zlib.crc32(compress_text)})") print() -print('Decompress...') +print("Decompress...") decompress_text = zlib.decompress(compress_text) -print('[{}]: {} (crc32: {})'.format(len(decompress_text), decompress_text, zlib.crc32(decompress_text))) +print( + f"[{len(decompress_text)}]: {decompress_text} (crc32: {zlib.crc32(decompress_text)})" +) diff --git a/zones_list_dns/README.md b/zones_list_dns/README.md deleted file mode 100644 index 6a81dacb8..000000000 --- a/zones_list_dns/README.md +++ /dev/null @@ -1,4 +0,0 @@ -zones_list_dns -=========== - -####RU: Скрипт возвращает список доменных зон. \ No newline at end of file diff --git a/zones_list_dns/zones_list_dns.py b/zones_list_dns/zones_list_dns.py deleted file mode 100644 index 3be84c73d..000000000 --- a/zones_list_dns/zones_list_dns.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'ipetrash' - - -"""Скрипт возвращает список доменных зон""" - - -if __name__ == '__main__': - from grab import Grab - g = Grab() - g.go('http://snowgate.info/zones_list_dns.html') - print(', '.join(d.text() for d in g.doc.select('//td/strong'))) \ No newline at end of file diff --git "a/\320\224\320\220+\320\224\320\220=\320\235\320\225\320\242.py" "b/\320\224\320\220+\320\224\320\220=\320\235\320\225\320\242.py" new file mode 100644 index 000000000..27139d054 --- /dev/null +++ "b/\320\224\320\220+\320\224\320\220=\320\235\320\225\320\242.py" @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stepik.org/lesson/360560/step/10?unit=345000 + + +""" +Дан ребус ДА + ДА = НЕТ. Необходимо заменить буквы цифрами так, чтобы получилось верное равенство. +Разным буквам соответствуют разные цифры. Число не может начинаться с нуля. +Надо написать программу, которая находит все решения (если есть несколько). +""" + +for d in range(1, 10): # Число не может начинаться с 0 + for a in range(10): + for n in range(1, 10): # Число не может начинаться с 0 + for e in range(10): + for t in range(10): + # Разным буквам соответствуют разные цифры + if len({d, a, n, e, t}) != 5: + continue + + da = d * 10 + a + net = n * 100 + e * 10 + t + + if da + da == net: + print(f"{da}+{da}={net}") + +""" +52+52=104 +53+53=106 +54+54=108 +64+64=128 +65+65=130 +67+67=134 +69+69=138 +73+73=146 +76+76=152 +78+78=156 +79+79=158 +82+82=164 +85+85=170 +86+86=172 +92+92=184 +93+93=186 +""" diff --git "a/\320\226\320\243\320\232 x \320\225\320\226=\320\226\320\226\320\226\320\226\320\226.py" "b/\320\226\320\243\320\232 x \320\225\320\226=\320\226\320\226\320\226\320\226\320\226.py" new file mode 100644 index 000000000..a4cad8dd2 --- /dev/null +++ "b/\320\226\320\243\320\232 x \320\225\320\226=\320\226\320\226\320\226\320\226\320\226.py" @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stepik.org/lesson/360560/step/12?unit=345000 + + +""" +Напишите программу, которая выводит решение ребуса ЖУК*ЕЖ=ЖЖЖЖЖ. +Одинаковым буквам соответствуют одинаковые цифры. +Разным буквам соответствуют разные цифры. Числа не могут начинаться с нуля. + +В выводе между цифрами и знаками должны отсутствовать пробелы. +""" + +for ж in range(10): + for у in range(10): + for к in range(10): + for е in range(10): + # Разным буквам соответствуют разные цифры + if len({ж, у, к, е}) != 4: + continue + + жук = int(f"{ж}{у}{к}") + еж = int(f"{е}{ж}") + жжжжж = int(f"{ж}" * 5) + if ( + (жук < 100 or жук > 999) + or (еж < 10 or еж > 99) + or (жжжжж < 10000 or жжжжж > 99999) + ): + continue + + if жук * еж == жжжжж: + print(f"{жук}*{еж}={жжжжж}") + +""" +271*82=22222 +""" diff --git "a/\320\230\320\232\320\241+\320\230\320\241\320\232=\320\232\320\241\320\230.py" "b/\320\230\320\232\320\241+\320\230\320\241\320\232=\320\232\320\241\320\230.py" new file mode 100644 index 000000000..f7aed68eb --- /dev/null +++ "b/\320\230\320\232\320\241+\320\230\320\241\320\232=\320\232\320\241\320\230.py" @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stepik.org/lesson/360560/step/11?unit=345000 + + +""" +Напишите программу, которая выводит решение ребуса ИКС+ИСК=КСИ. +Одинаковым буквам соответствуют одинаковые цифры. +Разным буквам соответствуют разные цифры. ИКС, ИСК, КСИ -- трехзначные числа. +Числа не могут начинаться с нуля. +""" + +for i in range(10): + for k in range(10): + for s in range(10): + # Разным буквам соответствуют разные цифры + if len({i, k, s}) != 3: + continue + + iks = i * 100 + k * 10 + s + isk = i * 100 + s * 10 + k + ksi = k * 100 + s * 10 + i + + # Проверка, что числа трехзначные + if any(n < 100 or n > 999 for n in [iks, isk, ksi]): + continue + + if iks + isk == ksi: + print(f"{iks}+{isk}={ksi}") + +""" +495+459=954 +""" diff --git "a/\320\240\320\276\321\201\320\272\320\276\320\274\320\275\320\260\320\264\320\267\320\276\321\200 \320\267\320\260\320\277\321\200\320\265\321\202\320\270\320\273 \320\261\321\203\320\272\320\262\321\203 \320\220.py" "b/\320\240\320\276\321\201\320\272\320\276\320\274\320\275\320\260\320\264\320\267\320\276\321\200 \320\267\320\260\320\277\321\200\320\265\321\202\320\270\320\273 \320\261\321\203\320\272\320\262\321\203 \320\220.py" index 28a4617d2..1c58c1f0a 100644 --- "a/\320\240\320\276\321\201\320\272\320\276\320\274\320\275\320\260\320\264\320\267\320\276\321\200 \320\267\320\260\320\277\321\200\320\265\321\202\320\270\320\273 \320\261\321\203\320\272\320\262\321\203 \320\220.py" +++ "b/\320\240\320\276\321\201\320\272\320\276\320\274\320\275\320\260\320\264\320\267\320\276\321\200 \320\267\320\260\320\277\321\200\320\265\321\202\320\270\320\273 \320\261\321\203\320\272\320\262\321\203 \320\220.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" alp = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя" @@ -15,5 +15,5 @@ print(result + c.upper()) # Удаляем букву из надписи - result = result.replace(c, '') - result = result.replace(c.upper(), '') + result = result.replace(c, "") + result = result.replace(c.upper(), "")