commit 2cc911fdd23f8a68196486bd7da57dbad3746f42 Author: Profitroll Date: Fri Jan 15 23:01:23 2021 +0200 Версия 1.4 добавлена на GitHub. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..6aa2c78 --- /dev/null +++ b/README.txt @@ -0,0 +1,155 @@ +Добро пожаловать в AutoZoom! + +В этом файле описаны все шаги которые нужно выполнить для работы с программой. + +Содержание +№1. Описание и информация +№2. Инструкция по установке +№3. Инструкция по использованию +№4. Благодарности и помощь + +Обратная связь и предложения: https://t.me/profitroll +Сообщить об ошибке/баге: https://github.com/profitrollgame/autozoom/issues + +-----------------------= №1 =----------------------- +--------- Информация и описание программы ---------- +---------------------------------------------------- + +Программа создана для автоматизации присоединения к +всевозможным Zoom конференциями. С помощью этой утилиты +можно запланировать присоединение к желаемой конференции +в удобное время и удобный день. Также может быть +полезно школьникам/студентам, которые желают побывать +на уроке/паре, но при этом находиться не у компьютера. +Софт может спасти вас в случае если нужно куда-то уходить, +а пропустить конференцию принципиально нельзя. + +Я не писал эту программу чтобы вредить знаниям учеников, +мешать им учиться или что-то подобное. Сделано это лишь +для удобства, интереса ради или даже в шутку. + + +----------------------= №2.1 =---------------------- +------------- Инструкция по установке -------------- +---------------------------------------------------- + +1. Переложите папку +Пришло время найти место для нашей программы. Скопируйте папку из этого +архива в любое удобное место на компьютере (если ещё этого не сделали). + +2. Установите Python3 +Попробуйте запустить ваш start.bat в папке AutoZoom. Возможно, он сам отправит вас на страницу загрузки Python. +Если же этого не произошло - сделать это можно вручную с официального сайта или из магазина приложений +Microsoft Store (https://www.microsoft.com/ru-ru/p/python-37/9nj46sx7x90p?activetab=pivot:overviewtab&source=lp). + +3. Откройте редактор +Дважды нажмите на start.bat и выберите пункт "Редактор" чтобы редактировать ваши уроки на любой +удобный день. Введите все нужные данные. Название конференции, дату, время, ссылку на приглашение. + +4. Настройте вход в Zoom +Теперь самая важная часть. Установите Zoom (https://zoom.us/download) на свой ПК. +Запустите его и зарегистрируйтесь/войдите в аккаунт. При входе ОБЯЗАТЕЛЬНО +нужно нажать на галочку о сохранении аккаунта "Не выполнять выход". + +5. Настройки клиента Zoom +Нажмите на шестерёнку под вашим аватаром (правый верхний угол) и зайдите +в пункт "Видеоизображение". В этом пункте найдите галочку "Выключать мое видео +при входе в конференцию" и активируйте её. Затем уберите галочку с "Всегда показывать диалоговое +окно предварительного просмотра видео при подключении к видеоконференции". Замечательно, +теперь отправляемся во вкладку "Звук" где внизу проверяем чтобы была галочка возле "Автоматически +подключать звук с компьютера" и "Отключить звук моего микрофона при подключении к конференции". Также +можно убрать галочку с "Нажмите и удерживайте клавишу пробел, чтобы временно включить свой звук", если нужно. +Почти закончили. Теперь перейдите в пункт "Сочетания клавиш" и выключите всё вам не нужное дабы случайно +не помешать процессу автоматизации. Некоторые сочетания нужно удалить. Например, Alt+A лучше удалить нажав +сначала на неё, а потом на Backspace. Желательно убрать все ненужные сочетания сразу. + +6. Готово +Вроде как всё настроено, пришло время открыть в папке приложения файл run.bat двойным нажатием и всё готово. +После вопроса про OBS можно перейти к опциональным шагам ниже для записи конференций или же просто нажать +"Н" на клавиатуре и перейти сразу к делу. + + +----------------------= №2.2 =---------------------- + +(Опционально) Запись конференций +С официального сайта (https://obsproject.com/download) скачайте и установите OBS Studio для записи всех конференций. +После стандартного процесса установки откройте только что установленный OBS. В вопросах мастера настойки укажите, +что вас интересует запись. Разрешение укажите нужное вам. + +Пройдя всё банальное и объяснённое в установщике отправляемся в настройки. Сразу же находим пункт "Вывод" в боковой +панели и меняем формат записи на mp4, если нужно будет потом редактировать видео. Если же нет – не трогаем. +Потом двигаемся к пункту "Горячие клавиши" и находим "Начать запись" и "Остановить запись". Тыкам на поле мышкой, а +затем прожимаем необходимую комбинацию клавиш. Рекомендую устанавливать на старт "Shift+F7", а на остановку "Shift+F8", +однако можете поставить всё что вам удобно. + +Чтобы наша запись работала правильно необходимо открыть в боковой панели "Общие" и в подразделе "Системный трей" поставить +галочку в пункте "Скрывать окно в системный трей при запуске". Запомините это, ведь если нужно будеть открыть OBS вручную +для настройки - нужно на панели задач с правой стороны найти стрелочку вверх и там нажать на иконку OBS. Во время записи +там будет показан красный кружок для более удобной индикации. По желанию можно вывести иконку на панель задач просто перетащив +её своей мышкой под окошко трея. + +Класс, теперь тут всё настроено. Тыкаем "Применить" и "ОК" внизу окна. Теперь мы в главном меню. Если ещё нет никаких +источников и в микшере пусто – не беда. Тыкаем + внизу окна источников и нажимаем "Захват экрана". +Выбираем нужный нам экран и жмакаем "ОК". + +Если в микшере таки пусто – тыкаем тот же + и нажимаем "Захват выходного аудиопотока" для записи вашего устройства +воспроизведения (колонки, наушники, не важно). Выбираем нужное устройство из списка и тыкаем "ОК". +Если нужно ещё и записать ваш голос с микрофона – снова жмём + добавляя устройство ВХОДНОГО аудиопотока, +выбираем нужный микрофон и дальше снова "ОК". Замечательно, в OBS всё настроено. Двигаем в AutoZoom. + +В случае если вы ставили свои комбинации клавиш (вместо рекомендуемых), то сейчас нужно открыть run.bat в папке +AutoZoom и выбрать пункт "Настройки". Затем выберите пункт "Начать запись" и введите желаемую комбинацию +клавиш (например, Shift+F7), нажмите Enter. Теперь выберите пункт "Остановить запись" и снова введите комбинацию клавиш. +Желательно, чтобы комбинации были разными, дабы точно избежать сбоев, однако это не по принципиально. Чудесно, жмакаем +последний пункт здесь и в меню редактора. Движемся дальше к run.bat. + +Открывая AutoZoom можно обнаружить, что он спрашивает хотим ли мы использовать OBS. Пишем Y или Д и жмём Enter. +В появившемся окне выбираем .exe файл нашего OBS. Обычно он лежит в "C:/Program Files/obs-studio/bin/64bit/obs64.exe", +но у вас может быть вместо 64bit папка 32bit, а файл obs32.exe. В случае с выбранным вами другим путём при +установке – ищите файл там, куда кинули. + +После выбора .exe файла вам должно в консоль AutoZoom написать пути ядра и приложения OBS. +Они также будут храниться в файлах AutoZoom, если вдруг понадобится их изменить. + + +----------------------= №2.3 =---------------------- + +Если же вам нужно получать уведомления от бота через Telegram - такая опция тоже есть. +Для этого нужно создать бота через BotFather (@BotFather) командой /newbot. +Затем вводим имя нашему боту, по сути любое которое нужно. Затем id бота чтобы оно заканчивалось на "bot" или "_bot". +После этого мы получаем HTTP API (токен бота) который вводим во время запуска AutoZoom или же перейдя в пункт +настроек. После ввода туда токена нужно написать нашему только что созданному боту через его имя (то, что на "bot" кончается) +номер который выдаст AutoZoom. По желанию можно введя команду /setuserpic и выбрав вашего бота ещё и сменить его аватар. +Теперь каждый раз когда AutoZoom будет начинать работу или заходить/покидать конференцию вы сразу же получите +сообщение в вашу личку Telegram. + + +-----------------------= №3 =----------------------- +------------ Инструкция по использованию ----------- +---------------------------------------------------- + +У нас есть рабочая и настроенная программа, но как же этим чудом теперь пользоваться? Всё очень просто. +Для начала открываем наш start.bat и тыкаем в пункт "Редактор". Там жмём "Добавить урок" и следуем шагам в приложении. +Чтобы удалить конференцию в меню редактора можно нажать "Удалить урок" и выбрать индекс (число слева посередине каждого урока). +Также, если вдруг что-то перенеслось, можно изменить конференци. нажав в редакторе "Изменить урок". + +Если нужно записать программу в автозапуск - сделать это легко. +Для этого создайте ярлык для daemon.bat или start.bat, вырежьте его и вставьте по пути автозапуска +(обычно это "C:\Users\ПОЛЬЗОВАТЕЛЬ\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"). +Готово! Теперь при запуске компьютера через некоторое время после входа в пользователя у вас запустится AutoZoom сам. Это +может быть полезно если хочется полностью автоматизировать присоединение к конференциям. + +Также если что-то пошло не так - можно в меню настроек сбросить все параметры до "По умолчанию". + + +-----------------------= №4 =----------------------- +--------------- Заключительные слова --------------- +---------------------------------------------------- + +На этом полная установка подходит к концу. +Если же вы нашли ошибки – не стесняйтесь писать в Telegram мне прямо в личку (https://t.me/profitroll). + +Приятного использования! + +P.S.: Отдельное спасибо Kusyaka за помощь в создании сего творения. +Без тебя, если честно, у меня бы ничего не вышло, дружище <3 \ No newline at end of file diff --git a/daemon.bat b/daemon.bat new file mode 100644 index 0000000..939bf15 --- /dev/null +++ b/daemon.bat @@ -0,0 +1 @@ +python daemon.py \ No newline at end of file diff --git a/daemon.py b/daemon.py new file mode 100644 index 0000000..cfb6410 --- /dev/null +++ b/daemon.py @@ -0,0 +1,1007 @@ +# -*- coding: utf-8 -*- + +import subprocess +import time +import datetime +import os +import pip +import pathlib +import json +import getopt +import sys +import winsound +from random import randint +from pathlib import Path +from datetime import datetime, date, timedelta + +path = Path(__file__).resolve().parent +sounds_folder = str(Path(str(path)+"/sounds/")) + os.sep +files_folder = str(Path(str(path)+"/files/")) + os.sep + +def saveJson(filename, value): + with open(filename, 'w', encoding="utf-8") as f: + json.dump(value, f, indent=4, ensure_ascii=False) + +def getConfig(some_var): + global files_folder + + if os.path.exists(files_folder): + if not os.path.exists(files_folder+'config.json'): + temp_config_list = {} + temp_config_list["debug"] = False + temp_config_list["shutdown_timeout"] = 30 + temp_config_list["shutdown_enabled"] = True + temp_config_list["start"] = "shift+f7" + temp_config_list["stop"] = "shift+f8" + temp_config_list["telegram_enabled"] = False + saveJson(files_folder+'config.json', temp_config_list) + else: + try: + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + return config_list[some_var] + except: + return "Error" + else: + os.mkdir(files_folder) + +def install(package, first_class=None, second_class=None): + try: + from config import debug + except: + debug = False + + try: + exec(f"{package} = __import__('{package}')") + globals()[package] = __import__(package) + if getConfig("debug"): + print(f'[OK] Импортирован модуль "{package}"') + except: + print(f'Trying to import package {package}') + if hasattr(pip, 'main'): + pip.main(['install', package]) + print(f'[OK] Установлен модуль "{package}"') + try: + exec(f"{package} = __import__('{package}')") + globals()[package] = __import__(package) + except ModuleNotFoundError: + none = input('Упс, модуль ещё не готов...') + print('Упс, модуль ещё не готов...') + if getConfig("debug"): + print(f'[OK] Импортирован модуль "{package}"') + else: + pip._internal.main(['install', package]) + print(f'[OK] Установлен модуль "{package}"') + exec(f"{package} = __import__('{package}')") + globals()[package] = __import__(package) + if getConfig("debug"): + print(f'[OK] Импортирован модуль "{package}"') + +install('easygui') +install('tkinter') +install('keyboard') +install('ast') +install('telegram_send') +install('inputimeout') + +#telegram_send.send(messages=[f"I'm alive"], parse_mode="markdown")#, conf=f"{files_folder}telegram.conf") + +menu_choose = None + +try: + from inputimeout import inputimeout, TimeoutOccurred +except: + print(f'[WARN] Не удалось импортировать классы "inputimeout" и "TimeoutOccurred" из модуля "inputimeout"') + +if os.name == 'nt': + clear = lambda: os.system('cls') +else: + clear = lambda: os.system('clear') + +def nowtime(seconds=True, noice=True): + now = datetime.now() + if seconds == True: + justnow = now.strftime("%H:%M:%S") + else: + justnow = now.strftime("%H:%M") + + if noice == True: + beautiful = f'[{justnow}]' + else: + beautiful = justnow + + return beautiful + +def act(x): + return x+10 + +def waitStart(runTime, action): + from datetime import datetime, time + from time import sleep + + startTime = time(*(map(int, runTime.split(':')))) + while startTime > datetime.today().time(): + sleep(2) + return action + +def getPair(line): + key, sep, value = line.strip().partition(" ") + return int(key), value + +def getLessons(): + if not os.path.exists(files_folder+'lessons.json'): + with open(files_folder+'lessons.json', 'w', encoding="utf-8") as f: + f.write("[]") + lessons_list = [] + else: + with open(files_folder+'lessons.json', encoding="utf-8") as json_file: + lessons_list = json.load(json_file) + + return lessons_list + +def getState(): + output = os.popen('wmic process get description, processid').read() + if "CptHost.exe" in output: + return True + else: + return False + +def listLessons(from_where='remove'): + try: + if from_where == 'editor': + print('Полный список запланированных конференций:\n') + + print('================================================') + for les in enumerate(getLessons()): + + if les[1]["repeat"]: + repeat = 'Вкл.' + else: + repeat = 'Выкл.' + + if les[1]["record"]: + record = 'Вкл.' + else: + record = 'Выкл.' + + try: + repeat_day = getDay(les[1]["repeat_day"]) + except: + repeat_day = 'Не повторяется' + + length = len(str(les[0])) + + spacer_all = 6 * ' ' #(4+length) * ' ' + spacer_ind = (5 - length) * ' ' #(len(str(les[0]))-1)*' ' + + # print(5 - length) + # print(f'length = "{length}"') + # print(f'spacer_all = "{spacer_all}"') + # print(f'spacer_ind = "{spacer_ind}"') + + print(f'{spacer_all}Имя: {les[1]["name"]}\n{spacer_all}Дата: {les[1]["date"]}\n{spacer_all}Время: {les[1]["time"]}\n {les[0]}{spacer_ind}Ссылка: {les[1]["link"]}\n{spacer_all}Повтор: {repeat}\n{spacer_all}День: {repeat_day}\n{spacer_all}Запись: {record}\n================================================') + + if from_where == 'editor': + none = input('\n\n > ') + except KeyboardInterrupt: + clear() + return + +def sortLessons(dictionary): + dictionary.sort(key = lambda x: datetime.strptime(x['time'], '%H:%M')) + dictionary.sort(key = lambda x: datetime.strptime(x['date'], '%d.%m.%Y')) + +def getDayNum(day): + output = datetime.strptime(day, "%d.%m.%Y").isoweekday() + return output + +def getDay(number): + if number == 1: + return 'Понедельник' + if number == 2: + return 'Вторник' + if number == 3: + return 'Среда' + if number == 4: + return 'Четверг' + if number == 5: + return 'Пятница' + if number == 6: + return 'Суббота' + if number == 7: + return 'Воскресенье' + +# def repeatLesson(): + +def addLesson(): + try: + local_lessons = {} + lessons_got = getLessons() + + lessname = input('Введите (своё) имя конференции:\n\n > ') + local_lessons.update({"name": lessname}) + + while True: + clear() + today = date.today() + today_1 = date.today() + timedelta(days=1) + today_2 = date.today() + timedelta(days=2) + today_3 = date.today() + timedelta(days=3) + today_4 = date.today() + timedelta(days=4) + today_5 = date.today() + timedelta(days=5) + today_6 = date.today() + timedelta(days=6) + + print(f'Введите дату конференции (дд.мм.гггг)\nили же просто номер для дней ниже:\n') + print(f'1. {today.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'2. {today_1.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_1.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'3. {today_2.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_2.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'4. {today_3.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_3.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'5. {today_4.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_4.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'6. {today_5.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_5.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'7. {today_6.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_6.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + + try: + lessdate = input('\n > ') + if lessdate == '': + finallessdate = lessons_got[edi]["date"] + elif lessdate == '1': + finallessdate = today.strftime("%d.%m.%Y") + elif lessdate == '2': + finallessdate = today_1.strftime("%d.%m.%Y") + elif lessdate == '3': + finallessdate = today_2.strftime("%d.%m.%Y") + elif lessdate == '4': + finallessdate = today_3.strftime("%d.%m.%Y") + elif lessdate == '5': + finallessdate = today_4.strftime("%d.%m.%Y") + elif lessdate == '6': + finallessdate = today_5.strftime("%d.%m.%Y") + elif lessdate == '7': + finallessdate = today_6.strftime("%d.%m.%Y") + else: + try: + test = (datetime.strptime(lessdate, "%d.%m.%Y")) + finallessdate = lessdate + except: + continue + + local_lessons.update({"date": finallessdate}) + + break + except: + continue + + while True: + clear() + try: + lesstime = input('Введите время конференции (чч:мм):\n\n > ') + finallesstime = (datetime.strptime(lesstime, "%H:%M")) + local_lessons.update({"time": lesstime}) + break + except: + continue + + clear() + lesslink = input('Введите ссылку на конференцию:\n\n > ') + local_lessons.update({"link": lesslink}) + + while True: + clear() + repeat = input(f'Повторять эту конференцию ({getDay(getDayNum(finallessdate))})? (Да/Нет)\n\n > ') + + if repeat.lower() in ['y', 'yes', 'д', 'да']: + finalrepeat = True + finalrepeatday = getDayNum(finallessdate) + local_lessons.update({"repeat": finalrepeat}) + local_lessons.update({"repeat_day": finalrepeatday}) + break + elif repeat.lower() in ['n', 'no', 'н', 'нет']: + finalrepeat = False + finalrepeatday = None + local_lessons.update({"repeat": finalrepeat}) + local_lessons.update({"repeat_day": finalrepeatday}) + break + else: + continue + + while True: + clear() + lessrecord = input('Записать эту конференцию? (Да/Нет)\n\n > ') + + if lessrecord.lower() in ['y', 'yes', 'д', 'да']: + finallessrecord = True + local_lessons.update({"record": finallessrecord}) + break + elif lessrecord.lower() in ['n', 'no', 'н', 'нет']: + finallessrecord = False + local_lessons.update({"record": finallessrecord}) + break + else: + continue + + + lessons_got.append(dict(local_lessons)) + sortLessons(lessons_got) + saveJson(files_folder+'lessons.json', lessons_got) + + clear() + print(f'Добавлен урок "{local_lessons["name"]}" за {local_lessons["date"]} на время {local_lessons["time"]}.') + none = input('\n > ') + except KeyboardInterrupt: + clear() + return + + +def editLesson(): + try: + local_lessons = {} + lessons_got = getLessons() + + while True: + print('Выберите номер (индекс) для изменения:\n') + listLessons() + lessons_got = getLessons() + + print('Для отмены операции введите "c" или "cancel"') + + edi = input('\n > ') + + if not isinstance(edi, int): + if edi.lower() == 'c' or edi.lower() == 'cancel': + clear() + return + try: + edi = int(edi) + except: + clear() + continue + + try: + probe = lessons_got[edi]["name"] + break + except: + clear() + print('Выберите правильный индекс (номер) для изменения.') + time.sleep(3) + clear() + continue + + break + + clear() + lessname = input(f'Введите (своё) имя конференции:\n(Оригинальное имя: "{lessons_got[edi]["name"]}")\n\n > ') + if lessname == '': + lessname = lessons_got[edi]["name"] + local_lessons.update({"name": lessname}) + + while True: + clear() + today = date.today() + today_1 = date.today() + timedelta(days=1) + today_2 = date.today() + timedelta(days=2) + today_3 = date.today() + timedelta(days=3) + today_4 = date.today() + timedelta(days=4) + today_5 = date.today() + timedelta(days=5) + today_6 = date.today() + timedelta(days=6) + + print(f'Введите дату конференции (дд.мм.гггг)\nили же просто номер для дней ниже:\n(Оригинальная дата: "{lessons_got[edi]["date"]}")\n') + print(f'1. {today.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'2. {today_1.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_1.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'3. {today_2.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_2.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'4. {today_3.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_3.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'5. {today_4.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_4.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'6. {today_5.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_5.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + print(f'7. {today_6.strftime("%d.%m.%Y")} ({getDay(datetime.strptime(today_6.strftime("%d.%m.%Y"), "%d.%m.%Y").isoweekday())})') + + try: + lessdate = input('\n > ') + if lessdate == '': + finallessdate = lessons_got[edi]["date"] + elif lessdate == '1': + finallessdate = today.strftime("%d.%m.%Y") + elif lessdate == '2': + finallessdate = today_1.strftime("%d.%m.%Y") + elif lessdate == '3': + finallessdate = today_2.strftime("%d.%m.%Y") + elif lessdate == '4': + finallessdate = today_3.strftime("%d.%m.%Y") + elif lessdate == '5': + finallessdate = today_4.strftime("%d.%m.%Y") + elif lessdate == '6': + finallessdate = today_5.strftime("%d.%m.%Y") + elif lessdate == '7': + finallessdate = today_6.strftime("%d.%m.%Y") + else: + try: + test = (datetime.strptime(lessdate, "%d.%m.%Y")) + finallessdate = lessdate + except: + continue + + local_lessons.update({"date": finallessdate}) + + break + except: + continue + + while True: + clear() + try: + lesstime = input(f'Введите время конференции (чч:мм):\n(Оригинальное время: "{lessons_got[edi]["time"]}")\n\n > ') + + if lesstime == '': + finallesstime = lessons_got[edi]["time"] + lesstime = lessons_got[edi]["time"] + else: + try: + finallesstime = (datetime.strptime(lesstime, "%H:%M")) + except: + continue + + local_lessons.update({"time": lesstime}) + break + except: + continue + + clear() + lesslink = input(f'Введите ссылку на конференцию\n(Оригинальная ссылка: "{lessons_got[edi]["link"]}")\n\n > ') + + if lesslink == '': + lesslink = lessons_got[edi]["link"] + local_lessons.update({"link": lesslink}) + + while True: + clear() + repeat = input(f'Повторять эту конференцию ({getDay(getDayNum(finallessdate))})? (Да/Нет)\n(Оригинальное значение: "{getDay(lessons_got[edi]["repeat_day"])}")\n\n > ') + + if repeat.lower() in ['y', 'yes', 'д', 'да']: + finalrepeat = True + finalrepeatday = getDayNum(finallessdate) + local_lessons.update({"repeat": finalrepeat}) + local_lessons.update({"repeat_day": finalrepeatday}) + break + elif repeat.lower() in ['n', 'no', 'н', 'нет']: + finalrepeat = False + local_lessons.update({"repeat": finalrepeat}) + break + elif repeat == '': + finalrepeat = lessons_got[edi]["repeat"] + local_lessons.update({"repeat": finalrepeat}) + try: + finalrepeatday = lessons_got[edi]["repeat_day"] + local_lessons.update({"repeat_day": finalrepeatday}) + except: + pass + break + else: + continue + + while True: + clear() + lessrecord = input(f'Записать эту конференцию? (Да/Нет)\n(Оригинальное значение: "{lessons_got[edi]["record"]}")\n\n > ') + + if lessrecord.lower() in ['y', 'yes', 'д', 'да']: + finallessrecord = True + local_lessons.update({"record": finallessrecord}) + break + elif lessrecord.lower() in ['n', 'no', 'н', 'нет']: + finallessrecord = False + local_lessons.update({"record": finallessrecord}) + break + elif lessrecord == '': + finallessrecord = lessons_got[edi]["record"] + local_lessons.update({"record": finallessrecord}) + break + else: + continue + + del lessons_got[edi] + lessons_got.append(dict(local_lessons)) + sortLessons(lessons_got) + saveJson(files_folder+'lessons.json', lessons_got) + clear() + print(f'Изменён урок "{lessname}" за {finallessdate} на время {finallesstime}.') + none = input('\n > ') + except KeyboardInterrupt: + clear() + return + + +def removeLesson(): + try: + while True: + print('Выберите номер (индекс) для удаления:\n') + listLessons() + lessons_local = getLessons() + print('Для отмены операции введите "c" или "cancel"') + + rem = input('\n > ') + + if rem.lower() == 'c' or rem.lower() == 'cancel': + clear() + break + else: + try: + rem = int(rem) + except: + clear() + continue + + try: + del_name = lessons_local[rem]["name"] + del_date = lessons_local[rem]["date"] + del_time = lessons_local[rem]["time"] + del lessons_local[rem] + except: + clear() + print('Выберите правильный индекс (номер) для удаления.') + time.sleep(3) + clear() + continue + + sortLessons(lessons_local) + saveJson(files_folder+'lessons.json', lessons_local) + clear() + print(f'Удалён урок "{del_name}" за {del_date} на время {del_time}.') + none = input('\n > ') + break + except KeyboardInterrupt: + clear() + return + +def removeAllLessons(): + try: + while True: + clear() + removeall = input(f'Вы уверены что хотите удалить все конференции? (Да/Нет)\nВнимание! Это действие нельзя обратить!\nВаши настройки затронуты НЕ будут.\n\n > ') + + if removeall.lower() in ['y', 'yes', 'д', 'да']: + with open(files_folder+'lessons.json', 'w', encoding="utf-8") as f: + f.write("[]") + clear() + none = input('Все уроки были удалены.\n\n > ') + clear() + break + elif removeall.lower() in ['n', 'no', 'н', 'нет']: + clear() + break + else: + continue + except KeyboardInterrupt: + clear() + return + +def editor(): + try: + from main import mainMenu + while True: + clear() + editor_choose = input('» Меню редактора\n\n1. Добавить урок\n2. Изменить урок\n3. Удалить урок\n4. Посмотреть уроки\n5. Удалить все уроки\n6. В главное меню\n\n > ') + if editor_choose == '1': + clear() + addLesson() + elif editor_choose == '2': + clear() + editLesson() + elif editor_choose == '3': + clear() + removeLesson() + elif editor_choose == '4': + clear() + listLessons(from_where = 'editor') + elif editor_choose == '5': + clear() + removeAllLessons() + elif editor_choose == '6': + clear() + mainMenu() + else: + continue + except KeyboardInterrupt: + clear() + return + +def tgsend(enabled, message): + if enabled: + telegram_send.send(messages=[f"{message}"], parse_mode="markdown", conf=files_folder+"telegram.conf") + +def settings(): + try: + while True: + clear() + + if getConfig("debug"): + debug_val = 'Вкл.' + elif not getConfig("debug"): + debug_val = 'Выкл.' + else: + debug_val = 'ERROR' + + if getConfig("shutdown_enabled"): + shutdown_en_val = 'Вкл.' + elif not getConfig("shutdown_enabled"): + shutdown_en_val = 'Выкл.' + else: + shutdown_en_val = 'ERROR' + + if os.path.exists(files_folder+'telegram.conf'): + tg_var = 'Настроен' + else: + tg_var = 'Не настроен' + + if getConfig("telegram_enabled"): + telegram_en_val = 'Вкл.' + elif not getConfig("debug"): + telegram_en_val = 'Выкл.' + else: + telegram_en_val = 'ERROR' + + shutdown_time_val = getConfig("shutdown_timeout") + start_val = getConfig("start") + stop_val = getConfig("stop") + settings_choose = input(f'» Настройки\n\n1. Режим отладки ({debug_val})\n2. Авто выключение ПК ({shutdown_en_val})\n3. Таймаут выключения ПК ({shutdown_time_val} мин.)\n4. Комбинация начала записи OBS ({start_val})\n5. Комбинация остановки записи OBS ({stop_val})\n6. Telegram бот ({telegram_en_val})\n7. Настроить Telegram бота ({tg_var})\n8. Сбросить все настройки\n9. В главное меню\n\n > ') + + if settings_choose == '1': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + config_list["debug"] = not getConfig("debug") + saveJson(files_folder+'config.json', config_list) + clear() + continue + elif settings_choose == '2': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + config_list["shutdown_enabled"] = not getConfig("shutdown_enabled") + saveJson(files_folder+'config.json', config_list) + clear() + continue + elif settings_choose == '3': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + try: + clear() + config_list["shutdown_timeout"] = int(input('Введите через сколько минут после конференции выключать ПК:\n\n > ')) + saveJson(files_folder+'config.json', config_list) + continue + except: + clear() + print('Нужно использовать целое число.') + time.sleep(2) + continue + continue + elif settings_choose == '4': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + try: + clear() + config_list["start"] = input('Введите комбинацию клавиш для начала записи OBS:\nЭта комбинация должна быть идентична оной в самом OBS!\n\n > ') + saveJson(files_folder+'config.json', config_list) + continue + except: + clear() + print('Нужно использовать комбинацию клавиш в виде текста.') + time.sleep(2) + continue + continue + elif settings_choose == '5': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + try: + clear() + config_list["stop"] = input('Введите комбинацию клавиш для остановки записи OBS:\nЭта комбинация должна быть идентична оной в самом OBS!\n\n > ') + saveJson(files_folder+'config.json', config_list) + continue + except: + clear() + print('Нужно использовать комбинацию клавиш в виде текста.') + time.sleep(2) + continue + continue + elif settings_choose == '6': + with open(f"{files_folder}config.json", encoding="utf-8") as json_file: + config_list = json.load(json_file) + + config_list["telegram_enabled"] = not getConfig("telegram_enabled") + saveJson(files_folder+'config.json', config_list) + clear() + continue + elif settings_choose == '7': + clear() + none = input('Пожалуйста, прочтите инструкцию по установке Telegram бота в README.TXT\nчтобы хорошо понимать что сейчас от вас нужно.\n\n > ') + + while True: + clear() + try: + telegram_send.configure(files_folder+'telegram.conf', channel=False, group=False, fm_integration=False) + break + except: + clear() + continue + telegram_send.send(messages=[f"🎊 Конфигурация правильна, всё работает!"], parse_mode="markdown", conf=f"{files_folder}telegram.conf") + clear() + continue + elif settings_choose == '8': + while True: + clear() + reset_decision = input('Вы уверены что хотите сбросить настройки? (Да/Нет)\n\nВнимание! Это действие нельзя обратить!\nВаш список конференций затронут НЕ будет.\n\n > ') + if reset_decision.lower() in ['y', 'yes', 'д', 'да']: + temp_config_list = {} + temp_config_list["debug"] = False + temp_config_list["shutdown_timeout"] = 30 + temp_config_list["shutdown_enabled"] = True + temp_config_list["start"] = "shift+f7" + temp_config_list["stop"] = "shift+f8" + temp_config_list["telegram_enabled"] = False + saveJson(files_folder+'config.json', temp_config_list) + if os.path.exists(files_folder+"obscorepath.txt"): + os.remove(files_folder+"obscorepath.txt") + if os.path.exists(files_folder+"obspath.txt"): + os.remove(files_folder+"obspath.txt") + if os.path.exists(files_folder+"telegram.conf"): + os.remove(files_folder+"telegram.conf") + clear() + none = input('Все настройки были сброшены до стандартных.\n\n > ') + clear() + break + elif reset_decision.lower() in ['n', 'no', 'н', 'нет']: + clear() + break + else: + clear() + continue + continue + clear() + continue + elif settings_choose == '9': + clear() + return + except KeyboardInterrupt: + clear() + return + +def main(source='deamon'): + try: + from main import mainMenu + clear() + + import webbrowser + #lessons_list = open('lessons.json', 'r') + + try: + with open(files_folder+'obspath.txt', 'r', encoding="utf-8") as f: + current_obs_path = f.read() + except: + current_obs_path = '' + + if not os.path.exists(files_folder+'obspath.txt') or current_obs_path == '': + obs_choice = input('Хотите использовать запись через OBS? (Д/Н): ') + if obs_choice.lower() == 'д' or obs_choice.lower() == 'y': + with open(files_folder+'obspath.txt', 'w', encoding="utf-8") as f: + while True: + try: + filename = easygui.fileopenbox('Выберите путь до obs32.exe или obs64.exe') + if filename.find("obs64.exe") != -1: + f.write(filename) + with open(files_folder+'obscorepath.txt', 'w', encoding="utf-8") as f: + f.write(filename[:-9]) + print(f'Сохранены пути для OBS:\nПриложение: {filename}\nКорневая папка: {filename[:-9]}') + time.sleep(3) + break + elif filename.find("obs32.exe") != -1: + f.write(filename) + with open(files_folder+'obscorepath.txt', 'w', encoding="utf-8") as f: + f.write(filename[:-9]) + print(f'Сохранены пути для OBS:\nПриложение: {filename}\nКорневая папка: {filename[:-9]}') + time.sleep(3) + break + elif filename.find("obs.exe") != -1: + f.write(filename) + with open(files_folder+'obscorepath.txt', 'w', encoding="utf-8") as f: + f.write(filename[:-7]) + print(f'Сохранены пути для OBS:\nПриложение: {filename}\nКорневая папка: {filename[:-7]}') + time.sleep(3) + break + else: + easygui.msgbox("Неверный путь") + break + except: + none = input('Вы не выбрали верный путь для OBS.\n\n > ') + if os.path.exists(files_folder+"obscorepath.txt"): + os.remove(files_folder+"obscorepath.txt") + if os.path.exists(files_folder+"obspath.txt"): + os.remove(files_folder+"obspath.txt") + clear() + break + + if not os.path.exists(files_folder+'telegram.conf'): + tg_choice = input('Хотите использовать Telegram бота? (Д/Н): ') + if tg_choice.lower() == 'д' or tg_choice.lower() == 'y': + # with open(files_folder+'telegram.conf', 'w', encoding="utf-8") as f: + clear() + none = input('Пожалуйста, прочтите инструкцию по установке Telegram бота в README.TXT\nчтобы хорошо понимать что сейчас от вас нужно.\n') + clear() + + telegram_send.configure(files_folder+'telegram.conf', channel=False, group=False, fm_integration=False) + telegram_send.send(messages=[f"🎊 Конфигурация правильна, всё работает!"], parse_mode="markdown", conf=f"{files_folder}telegram.conf") + clear() + + lessons_count = 0 + + try: + if getConfig("debug"): + print(f'{nowtime()} Конфигурация импортирована') + except: + print(f'{nowtime()} Конфигурация отсутсвует, выключаем отладку') + + + for les in getLessons(): + lessons_list = getLessons() + + lesson_name = les["name"] + lesson_date = les["date"] + lesson_time = les["time"] + lesson_url = les["link"] + lesson_obs = les["record"] + lesson_repeat = les["repeat"] + lesson_repeat_day = les["repeat_day"] + + today = date.today().strftime("%d.%m.%Y") + + if getDayNum(today) == lesson_repeat_day: #lesson_date == today: # or getDayNum(today) == lesson_repeat_day: + print('================================================\n') + + print(f'{nowtime()} Найден урок "{lesson_name}" в {lesson_time}. Ждём начала...') + waitStart(lesson_time, lambda: act(100)) + webbrowser.open(lesson_url) + easteregg_number = randint(1, 100000) + if easteregg_number == 69420: + webbrowser.open('https://www.pornhub.com/view_video.php?viewkey=ph5f3eb1e206aa8') + print(f'{nowtime()} Ждём 10 секунд до отслеживания Zoom...') + time.sleep(10) + + while not getState(): + if getConfig("debug"): + print(f'{nowtime()} Урок задерживается, ждём...') + time.sleep(5) + continue + + record_now = False + lesson_duration = 0 + firstshow = True + + if lesson_obs: + try: + if getConfig("debug"): + print(f'{nowtime()} Импортированы клавиши старта и остановки записи ({getConfig("start")} и {getConfig("stop")}).') + + start = getConfig("start") + stop = getConfig("stop") + except: + start = 'shift+f7' + stop = 'shift+f8' + if getConfig("debug"): + print(f'{nowtime()} Используем стандартные клавиши старта и остановки записи ({start} и {stop}).') + + while True: + if getState(): + if firstshow: + print(f'{nowtime()} Захвачен текущий урок в Zoom.') + winsound.PlaySound(sounds_folder+"started.wav", winsound.SND_FILENAME) + tgsend(getConfig("telegram_enabled"), f"▶ Зашёл на урок *{lesson_name}* в *{nowtime(False, False)}*") + if lesson_obs: + try: + obs_path_file = open(files_folder+'obspath.txt', 'r', encoding="utf-8") + obs_path_file_text = obs_path_file.read() + + obs_core_path_file = open(files_folder+'obscorepath.txt', 'r', encoding="utf-8") + obs_core_path_file_text = obs_core_path_file.read() + + obs_process = subprocess.Popen(obs_path_file_text, cwd=obs_core_path_file_text) + time.sleep(5) + except: + print(f'{nowtime()} Не удалось открыть OBS для записи.') + else: + if getConfig("debug"): + print(f'{nowtime()} Не включаем OBS для записи.') + firstshow = False + + if lesson_obs: + if not record_now: + keyboard.press(start) + time.sleep(.25) + keyboard.release(start) + record_now = True + print(f'{nowtime()} Сигнал записи OBS отправлен.') + # ({start})') + + lesson_duration = lesson_duration + 10 + + if getConfig("debug"): + print(f'{nowtime()} Zoom подключён. Урок идёт уже {str(lesson_duration)} сек. ({str(round(lesson_duration/60, 2))} мин.)') + + time.sleep(10) + continue + else: + if getConfig("debug"): + print(f'{nowtime()} Zoom отключился. Процесс CptHost.exe более не существует.') + + tgsend(getConfig("telegram_enabled"), f"◀ Урок *{lesson_name}* длился *{str(round(lesson_duration/60, 2))}* мин.") + print(f'{nowtime()} Урок длился {str(lesson_duration)} сек. ({str(round(lesson_duration/60, 2))} мин.)') + winsound.PlaySound(sounds_folder+"ended.wav", winsound.SND_FILENAME) + + if lesson_obs: + keyboard.press(stop) + time.sleep(.25) + keyboard.release(stop) + print(f'{nowtime()} Сигнал остановки записи через OBS отправлен.') + # ({stop})') + record_now = False + time.sleep(3) + try: + obs_process.terminate() + except: + if getConfig("debug"): + print(f'{nowtime()} Не удалось остановить процесс OBS.') + + if not lesson_repeat: + del lessons_list[i] + + saveJson(files_folder+'lessons.json', lessons_list) + + print(f'{nowtime()} Урок "{lesson_name}" в {lesson_time} удалён.') + + print('\n================================================\n\n') + + firstshow = True + + lessons_count = lessons_count+1 + + break + record_now = False + lessons_list = getLessons() + + + time.sleep(3) + print(f'{nowtime()} Уроков нет или же все в списке закончились.') + + if lessons_count > 0: + if getConfig("shutdown_enabled"): + try: + print(f'{nowtime()} Ваш ПК автоматически выключится через {str(getConfig("shutdown_timeout"))} мин.') + winsound.PlaySound(sounds_folder+"shutdown.wav", winsound.SND_FILENAME) + shutdown = inputimeout(prompt=f'{nowtime()} Нажмите Enter чтобы предотвратить выключение ПК...', timeout=getConfig("shutdown_timeout")*60) + except TimeoutOccurred: + print(f'{nowtime()} Время вышло, выключаем ваш ПК...') + time.sleep(3) + tgsend(getConfig("telegram_enabled"), f"⚠ Уроков больше нет, выключаем ваш ПК...") + time.sleep(5) + os.system("shutdown /s /t 1") + + if source == 'deamon': + exit = input(f'{nowtime()} Программа завершена! Нажмите Enter чтобы выйти...') + clear() + sys.exit() + elif source == 'menu': + exit = input(f'{nowtime()} Программа завершена! Нажмите Enter чтобы вернуться в меню...') + clear() + return + except KeyboardInterrupt: + if source == 'deamon': + exit = input(f'{nowtime()} Программа остановлена! Нажмите Enter чтобы выйти...') + clear() + sys.exit() + elif source == 'menu': + exit = input(f'{nowtime()} Программа остановлена! Нажмите Enter чтобы вернуться в меню...') + clear() + return + +if __name__ == '__main__': + import sys + clear() + + main() \ No newline at end of file diff --git a/files/config.json b/files/config.json new file mode 100644 index 0000000..1c19333 --- /dev/null +++ b/files/config.json @@ -0,0 +1,8 @@ +{ + "debug": false, + "shutdown_timeout": 30, + "shutdown_enabled": true, + "start": "shift+f7", + "stop": "shift+f8", + "telegram_enabled": false +} \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..03d894d Binary files /dev/null and b/icon.ico differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..e258e27 --- /dev/null +++ b/main.py @@ -0,0 +1,185 @@ +import sys +import webbrowser +import os +import platform +import subprocess +from daemon import install +from pathlib import Path + +install('wget') +install('zipfile') +install('requests') +import wget +import requests +from zipfile import ZipFile +from daemon import main, editor, settings, clear + +version = 1.4 +path = Path(__file__).resolve().parent + +def mainMenu(): + try: + global version + global path + + while True: + serv_ver = requests.get("https://www.end-play.xyz/AutoZoomVersion.txt").text + if float(serv_ver) > float(version): + show_version = ' (!)' + else: + show_version = '' + + #clear() + menu_choose = input(f'» Главное меню\n\n1. Запуск\n2. Редактор\n3. Настройки\n4. Обновление{show_version}\n5. Помощь и связь\n6. Закрыть приложение\n\n > ') + + if menu_choose == '1': + main('menu') + elif menu_choose == '2': + editor() + elif menu_choose == '3': + settings() + elif menu_choose == '4': + updater(serv_ver, version) + elif menu_choose == '5': + helpMenu() + elif menu_choose == '6': + clear() + sys.exit() + else: + clear() + continue + except: + clear() + +def os_arch(): + is_64bits = sys.maxsize > 2**32 + + if is_64bits: + return '64bit' + else: + return '32bit' + +def helpMenu(): + try: + while True: + clear() + global version + global path + help_choose = input(f'» Меню помощи\n\n1. Документация\n2. Telegram проекта\n3. Связаться с автором\n4. Сводка информации\n5. В главное меню\n\n > ') + if help_choose == '1': + try: + clear() + webbrowser.open("https://github.com/profitrollgame/autozoom/wiki") + except: + clear() + none = input('Не удалось открыть страницу вашего браузера.\nВы можете открыть адрес самостоятельно: https://github.com/profitrollgame/autozoom/wiki\n\n > ') + clear() + elif help_choose == '2': + try: + clear() + webbrowser.open("https://t.me/auto_zoom") + except: + clear() + none = input('Не удалось открыть страницу вашего браузера.\nВы можете открыть адрес самостоятельно: https://t.me/auto_zoom\n\n > ') + clear() + elif help_choose == '3': + try: + clear() + webbrowser.open("https://t.me/profitroll") + except: + clear() + none = input('Не удалось открыть страницу вашего браузера.\nВы можете открыть адрес самостоятельно: https://t.me/profitroll\n\n > ') + clear() + if help_choose == '4': + clear() + print('» Информация о системе\n') + print('Система:') + print(f'• ОС: {platform.system()}') + print(f'• Релиз: {platform.release()}') + print(f'• Разрядность: {os_arch()}') + print('\nPython:') + print(f'• Версия: {platform.python_version()}') + print(f'• Вариант: {platform.python_implementation()}') + print(f'• Ревизия: {platform.python_revision()}') + print(f'• Расположение: {sys.path[4]}') + print('\nAutoZoom:') + print(f'• Версия: {version}') + print(f'• Расположение: {path}') + none = input('\n > ') + clear() + elif help_choose == '5': + clear() + return + else: + clear() + continue + except KeyboardInterrupt: + clear() + return + +def updater(serv_ver, version): + try: + while True: + clear() + if float(serv_ver) > float(version): + show_version = ' (!)' + serv_ver = serv_ver.rstrip('\n') + show_action = f'Обновить до {serv_ver}' + changelog_text = f'Изменения в версии {serv_ver}:' + changelog_footer = '\nОбновитесь чтобы вышеуказанное работало.' + else: + show_version = '' + show_action = f'Переустановить' + changelog_text = f'Изменения в вашей версии:' + changelog_footer = '' + + + updater_choose = input(f'» Меню обновлений\n\n1. {show_action}\n2. Список изменений\n3. В главное меню\n\n > ') + if updater_choose == '1': + while True: + clear() + updater_decide = input(f'1. Установить\n2. Отменить\n\n > ') + + if updater_decide == '1': + clear() + + wget.download('https://www.end-play.xyz/AutoZoomLatest.zip', out='AutoZoomLatest.zip') + with ZipFile('AutoZoomLatest.zip', 'r') as zipObj: + zipObj.extractall() + print('Все файлы были успешно загружены') + + if os.path.exists("AutoZoomLatest.zip"): + os.remove("AutoZoomLatest.zip") + + clear() + none = input('Обновление завершено, перезапустите AutoZoom.\n\n > ') + sys.exit() + elif updater_decide == '2': + clear() + break + else: + continue + elif updater_choose == '2': + changelog = requests.get("https://www.end-play.xyz/AutoZoomChangelog.txt") + changelog.encoding = None + clear() + print(f'{changelog_text}\n') + print(changelog.text) + print(changelog_footer) + + none = input('\n > ') + continue + elif updater_choose == '3': + clear() + return + else: + continue + except: + clear() + return + +if __name__ == '__main__': + from daemon import clear + clear() + + mainMenu() \ No newline at end of file diff --git a/sounds/ended.wav b/sounds/ended.wav new file mode 100644 index 0000000..898ec6c Binary files /dev/null and b/sounds/ended.wav differ diff --git a/sounds/shutdown.wav b/sounds/shutdown.wav new file mode 100644 index 0000000..e413871 Binary files /dev/null and b/sounds/shutdown.wav differ diff --git a/sounds/started.wav b/sounds/started.wav new file mode 100644 index 0000000..325e1c5 Binary files /dev/null and b/sounds/started.wav differ diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..7a84f97 --- /dev/null +++ b/start.bat @@ -0,0 +1 @@ +python main.py \ No newline at end of file