Хакер - Про Pyto. Делаем веб-сервер на iOS и качаем видео с youtube-dl
hacker_frei
Андрей Письменный
Содержание статьи
- Pythonista
- Встречайте — Pyto!
- Особенности Pyto
- Командная строка
- Использование файловой системы
- Запуск фоновых задач
- Другие фичи и модули
- Пишем веб-сервер
- Задача
- Инструментарий
- Код
- Первый запуск
- Выводы
Говорят, iOS невероятно закрытая система: ни написать свою программу без лицензии разработчика, ни заставить iPhone или iPad делать что‑то, что не одобряют в Apple. Сегодня я познакомлю тебя с Pyto — интерпретатором Python для iOS, который позволяет творить... если не чудеса, то как минимум многие вещи, которые раньше считались невозможными без джейлбрейка.
Сначала посмотрим на сам Pyto, а потом смастерим простенький веб‑интерфейс, который поможет более удобно скачивать видео через youtube-dl. Работать он будет локально на iOS.
PYTHONISTA
Pyto — не первая попытка создать среду разработки на Python для портативных устройств Apple. Уже много лет существует приложение Pythonista. Оно служило мне верой и правдой, позволяя как решать на ходу сиюминутные задачи (что‑то посчитать, расшифровать, сгенерировать последовательность), так и создавать небольшие полезные приложения. В Pythonista есть свой графический интерфейс и некоторые возможности для взаимодействия с системой. К примеру, можно сделать периодически обновляемый виджет для экрана Today.

Однако разработка Pythonista остановилась уже больше пяти лет назад. Автор за это время появился на публике всего один раз и выпустил небольшой апдейт, который позволил приложению запускаться на современных версиях iOS. А затем снова пропал: ни ответов на форуме, ни постов в твиттере. И уж конечно, никаких новых фич в Pythonista.
ВСТРЕЧАЙТЕ — PYTO!
И тут на сцену выходит Pyto — тоже творение единственного разработчика, но зато полного сил и энтузиазма. А также, кажется, желания испытывать терпение модераторов Apple, постоянно идя по краю дозволенного в App Store. Pyto, к примеру, умеет запускать приложения в фоне, позволяет создавать локальные веб‑серверы и даже имеет встроенную поддержку репозитория PyPI — ставить пакеты можно буквально одной кнопкой!
INFO
Установка приложений внутри приложения до сих пор была абсолютным табу в iOS. Любые лазейки в Apple прикрывали, из‑за чего в ту же Pythonista новые пакеты приходилось протаскивать сложной цепочкой действий. Не исключено, что автору Pyto повезло и модераторы просто проморгали нарушения. Или же разрешать программистам некоторые вольности — это новая политика Apple. Поживем — увидим!
У Pyto большие возможности для работы с математическими функциями — к примеру, пакет NumPy, без которого не обходятся любые мало‑мальски сложные вычисления, установлен по умолчанию. Также в Pyto удобно строить графики и даже пользоваться кое‑какими средствами для машинного обучения. К примеру, мне без труда удалось поставить Gensim и поразвлекаться с разбором текстов.
Как и Pythonista, Pyto стоит денег: после трехдневного пробного периода тебя попросят раскошелиться на 750 рублей за базовую версию и 1400 за расширенную, с набором библиотек, требующих компилцяии. Но необходимость раскошеливаться — не главный недостаток Pyto.
Основной проблемой этого приложения остается невероятная сырость. Настолько мощная, что в болото багов можно провалиться по колено, если не с головой. И я не преувеличиваю: один раз мне удалось заклинить Pyto настолько, что даже перезагрузка не помогала.
Однако если держаться проторенных дорожек и пользоваться Pyto бережно и аккуратно, то он сослужит отличную службу.
ОСОБЕННОСТИ PYTO
Командная строка
В Pyto есть командная строка. Но учти, что это не Bash и внутри — не Linux. Это свой командный интерпретатор Pyto. Он поддерживает несколько основных команд Bash — список можно узнать командой help. Отсюда ты запустишь скрипты на Python, но на установку каких‑то других программ рассчитывать не приходится.

Использование файловой системы
Важное отличие iOS от других операционок — в том, насколько сильно здесь контролируется доступ к файловой системе. Без разрешения скрипт в Pyto не может получить доступ ни к одной папке!
Хорошая новость заключается в том, что запрашивать права очень просто. Для этого в Pyto есть модуль file_system.
Пишем простенький скрипт:
import file_system as fs
path = fs.FolderBookmark()
print(path)
После его запуска появится диалоговое окно для выбора папки. Выбрав ее, ты дашь Pyto права на доступ ко всем файлам в ней. Именно Pyto, а не своему скрипту! Это означает, что дальше доступ к этой папке будет открыт всем программам внутри Pyto, в том числе после любых перезапусков и перезагрузок.
Если же по ходу исполнения скрипта тебе понадобится получать доступ к разным файлам или папкам, ты можешь воспользоваться функциями import_file() и open_directory() из того же модуля. Каждый раз при их вызове будет появляться диалоговое окно для выбора файла или папки.
Запуск фоновых задач
Еще одна особенность iOS — неиспользуемые приложения принудительно завершаются и выгружаются из памяти (если встретишь людей, кропотливо чистящих историю задач в iOS, передай им, что это мартышкин труд — ОС сама отлично с этим справится). Но что сделать, чтобы программа работала в фоне непрерывно? Для разных типов задач в iOS существуют API, все же позволяющие фоновое исполнение.
В Pyto используется нехитрый прием: если тебе нужна работа в фоне, то приложение будет делать вид, что проигрывает аудиофайл, тогда как никакого воспроизведения в реальности не происходит. Для активации этого поведения используй модуль background.
Чтобы программа начала лучше держаться на плаву, достаточно написать что‑то в таком духе:
import background as bg
with bg.BackgroundTask() as task:
# Код для исполнения в фоне
Другие фичи и модули
В Pyto есть поддержка еще нескольких интересных возможностей: например, ты можешь активировать свои скрипты вызовами из Shortcuts или URL-схему и передавать им параметры. Другая мощная вещь — мост между Python и Objective-C на основе библиотеки Rubicon-ObjC, он позволяет вызывать функции из нативных фреймворков.
К Pyto прилагается неплохой набор модулей, для работы которых требуется компиляция. Самостоятельно добавлять такие модули, к сожалению, нельзя.

А также несколько собственных модулей для работы с iOS. Мы уже рассмотрели file_system и background, но есть и другие:
- apps — интерфейсы к популярным сторонним приложениям. Позволяют из скрипта активировать другую программу и сказать ей сделать то или иное действие;
- pyto_ui — фреймворк для разработки приложений с графическим интерфейсом. Позволяет рисовать окошки, кнопочки и прочие элементы управления;
- widgets и watch — библиотеки для создания виджетов для экрана «Сегодня» и часов Apple Watch;
- sound, music и speech отвечают за воспроизведение звука, музыки и синтез речи;
- notifications позволяет отправлять оповещения;
- location и motion дают возможность получать геокоординаты устройства и опрашивать датчики движения;
- pasteboard — взаимодействие с буфером обмена;
- multipeer — позволяет обмениваться данными в режиме точка — точка.
Разбор всех этих замечательных возможностей не уместится в одну статью, так что выберем одну небольшую задачу, на которой испытаем Pyto в деле.
ПИШЕМ ВЕБ-СЕРВЕР
Задача
Проглядывая доступные в Pyto модули, я заприметил youtube-dl — широко известную утилиту для скачивания видео с видеохостингов. В том числе и тех, которые пытаются защитить контент от подобных действий. В общем, отличная штука! Но пользоваться модулем Python или утилитой для командной строки на мобильном устройстве не очень‑то удобно.
Есть множество способов приделать с помощью Pyto более удобный интерфейс к youtube-dl, в том числе написать графическое приложение на одном из доступных GUI-фреймворков. Однако мне пришла в голову мысль попробовать использовать Pyto в качестве локального веб‑сервера. Заодно можно будет посмотреть, как тут дела с веб‑разработкой.
Инструментарий
Первым делом нам потребуется веб‑фреймворк или как минимум библиотека, которая будет упрощать работу с HTTP. В документации мне встретилось упоминание Tornado и Django, но для нашей задачи и то и то — оверкилл. Достаточно будет Flask, который без проблем установился командой pip install flask и заработал.
Чтобы еще сильнее упростить себе жизнь, я буду использовать клиентский фреймворк HTMX, который позволяет делать асинхронные запросы без использования JavaScript. С ним достаточно задать тегу несколько HTML-атрибутов с информацией о том, куда слать запрос и куда подставлять полученный ответ. Остальное HTMX сделает сам.
Ну и чтобы внешний вид программы был не колхозным, подключим минималистичный CSS-фреймворк Pico.css.
Код
Первым делом импортируем все необходимое.
import flask
from youtube_dl import YoutubeDL
from threading import Thread
import sys
import urllib.request
import os
import background as bg
import file_system as fs
Инициализируем глобальную переменную path и задаем путь к папке, куда будут скачиваться видео. Подразумевается, что права на нее ты уже дал при помощи fs.FolderBookmark().
path = '/private/var/mobile/Library/Mobile Documents/com~apple~CloudDocs/Pyto/'
В пару к веб‑серверу обычно делают базу данных, но, поскольку мы будем работать исключительно в дев‑окружении (в Pyto продакшен все равно не поднять), можно просто использовать глобальные переменные и хранить данные в памяти во время работы программы.
Инициализируем две переменные: в одной будет храниться имя скачиваемого файла, в другой — его размер.
filename = ''
size = 0
Создаем экземпляр класса Flask и делаем так, чтобы он активировался при запуске скрипта:
app = flask.Flask(__name__)
if __name__ == '__main__':
app.run()
Дальше создаем основу нашей программы — функцию для скачивания файла. Она получает ссылку на файл (link) и имя файла для записи (filename). Внутри мы при помощи urllib.request.urlopen() создаем запрос и при помощи request.getheader() получаем длину файла (заголовок content-length). Это значение сохраняем в глобальную переменную size. После этого активируем фоновую задачу, которая сводится к скачиванию файла методом urllib.request.urlretrieve().
def download_file(link, filename):
with urllib.request.urlopen(link) as request:
global size
size = int(request.getheader('content-length'))
with bg.BackgroundTask() as task:
urllib.request.urlretrieve(link, path + filename)
Дальше — вспомогательная функция для получения текущего прогресса в процентах. Формула проста: нам понадобится поделить текущий размер файла на полный, а затем умножить на сто процентов. Текущий замеряем при помощи os.path.getsize() (путь и имя файла у нас хранятся в глобальных переменных). Если файла еще нет, возвращаем 0.
def get_progress():
filepath = path + filename
if os.path.exists(filepath):
real_size = os.path.getsize(filepath)
progress = int(real_size / size * 100)
return progress
else:
return 0
Создаем обработчик, который будет возвращать главную страницу.
@app.route('/')
def hello():
return '''
# Здесь код страницы
'''
Сам код страницы следующий:
<html>
<head>
<script src="htmx.min.js"></script>
<link rel="stylesheet" href="pico.min.css">
</head>
<body><main class="container"><article>
<form hx-get="/go" hx-target="this" hx-swap="outerHTML">
<div>
<label>Ссылка на видео:</label>
<input type="text" name="video_url" value="" size="100">
<button class="btn">Качаем!</button>
</div>
</form>
</article></main></body>
В секции head мы подключаем файлы с нашими фреймворками — предполагается, что ты их предварительно скачал, но вместо этого можешь получить ссылки на версии в CDN и прописать их вместо названий файлов.
В body создаем теги main и article, чтобы воспользоваться сеточной версткой и вывести аккуратную белую карточку.
Но самое интересное — это тег form:
<form hx-get="/go" hx-target="this" hx-swap="outerHTML">
Здесь мы оставляем указания для HTMX: дергать сервер по адресу /go, а полученным результатом заменить весь тег. Таким образом форма у нас будет заменена на прогресс‑бар.
Теперь давай добавим обработчик для пути /go.
@app.route('/go', methods=["GET"])
def go():
В качестве параметра пользователь пришлет нам ссылку на видео, расположенное на каком‑то видеохостинге. Используя модуль youtube_dl, мы получим прямую ссылку на файл и название ролика. Для этого сначала создадим словарь с опциями (в каком качестве качать), потом экземпляр youtube_dl (назовем ydl), а потом передадим методу ydl.extract_info() наш URL из аргумента запроса. Достаем ссылку для скачивания и заголовок функцией info_dict.get().
youtube_dl_opts = {'format': 'best'}
with YoutubeDL(youtube_dl_opts) as ydl:
info_dict = ydl.extract_info(flask.request.args['video_url'], download=False)
video_url = info_dict.get("url", None)
video_title = info_dict.get('title', None)
Теперь нам нужно задать имя файла и сохранить его в глобальной переменной filename. Можно использовать название ролика, но в нем могут попасться символы, которые не понравятся файловой системе. Отфильтруем их и соберем новую строку.
global filename
filename = "".join(i for i in video_title if i not in ":*?<>|/") + '.mp4'
У нас есть все параметры для функции download_file. Чтобы наш веб‑апп отдал ответ клиенту и продолжил скачивание в фоне, запустим эту функцию как тред.
thread = Thread(target=download_file, args = (video_url, filename))
thread.start()
Ну и наконец, отдадим клиенту ответ. Это заголовок h2 с названием ролика и пустой слой с еще одной порцией магии HTMX. На этот раз мы велим ему запрашивать страницу /status через одну секунду и пришедшим ответом заменять наш слой.
return f'''
<h2>{video_title}</h2>
<div hx-get="/status"
hx-trigger="load delay:1s"
hx-swap="outerHTML">
</div>
'''
Осталось нарисовать прогресс‑бар. Для этого пишем обработчик /status. Запрашиваем прогресс в процентах у нашей функции get_progress() и снова возвращаем слой с аргументами HTMX: послать новый запрос на /status еще через секунду и подменить текущий слой. Сам прогресс‑бар есть в Pico.css: достаточно использовать тег progress и присвоить аргументу value текущее значение.
@app.route('/status')
def status():
progress = get_progress()
return f'''
<div hx-get="/status"
hx-trigger="load delay:1s"
hx-swap="outerHTML">
<h3>{progress}%</h3>
<progress value="{progress}" max="100"></progress>
</div>
'''
Первый запуск
Итак, собираем всё воедино, не забываем дать Pyto права на указанную в самом начале папку и жмем кнопку Run. Запустится локальный сервер и будет отвечать на запросы, сделанные из браузера.
Переходим в Safari и запрашиваем адрес localhost:5000, где у нас работает сервер. Видим нашу форму (окно с Pyto на моих скриншотах открыто для наглядности, делать так же не обязательно).

Вставляем URL, и закачка пошла! Когда она завершится, файл должен оказаться в заданной папке.

К сожалению, несмотря на то что мы использовали модуль background, закрыть Safari и заняться чем‑то другим до окончания скачивания нельзя. Как только клиентская часть веб‑приложения перестанет слать запросы к серверу, iOS таки поставит его процесс на паузу. Однако мои эксперименты показали, что без background процесс с Pyto может затухнуть даже во время того, как ты сидишь и наблюдаешь за скачиванием.
ВЫВОДЫ
Мы написали простенький веб‑сервер, который позволяет скачивать видеофайлы. При необходимости ты можешь придумать какой‑то способ для его вызова, например при помощи Shortcuts.
Возможности Pyto тем временем не ограничиваются разработкой веб‑приложений. С его помощью ты можешь творить самые разные программы и наделять свой айфон или айпад новыми фичами.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei