Хакер - Про Pyto. Делаем веб-сервер на iOS и качаем видео с youtube-dl

Хакер - Про Pyto. Делаем веб-сервер на iOS и качаем видео с youtube-dl

hacker_frei

https://t.me/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




Report Page