Мы вырастили монстра, который больше не нужен (с)
Рассмотрим возможности применения модного фреймворка HTMX в связке с Ktor на примере написания загрузчика видео.
Ещё давным-давно я мечтал ворваться в написание сайтов, но меня всегда останавливал огромный зоопарк JS-фреймворков. Ну в принципе сам JavaScript
К счастью, появился инструмент, который возвращает сайтостроение назад к истокам.
К бэкендерам!
Финальный результат можно оценить на https://flowtube.ru/
Что такое HTMX
Что же из себя представляет HTMX?
Это легковесная библиотека, которая добавляет новые атрибуты для html.
Нам не нужно больше парсить JSON.
Делаем запрос к серверу и вставляем кусок html кода на страницу.
HTMX занял второе место в рейтинге самых популярных фронтенд фрейморков по версии JavaScript Rising Stars 2023.
Очевидно, что у такого подхода существуют свои ограничения и пока не всё получится сделать, но потенциал огромен. Новое золото!
Переходим к коду
Осознаю, что в рамках данной статьи рассмотреть все нюансы своего проекта не получится, я выложил его в открытый доступ на GitFlic.
Для написания загрузчика нам понадобится:
1. Ktor
2. HTMX
3. Tailwind для стилей
4. Kotlinx.html как DSL для написания HTML разметки прямо в Kotlin коде.
5. Прокси (для обхода ограничений YouTube, если решите запускать сервис на облаке.
Об этом в конце статьи)
Для работы с YouTube API мы будем использовать InnerTube.
Более подробно про этот подход я написал в другой статье на Хабр - Как написать свой клиент для YouTube, не привлекая внимания санитаров
Подключаем HTMX и Tailwind
Огромным преимуществом Kotlin является то, что мы можем использовать DSL для HTML и писать разметку прямо в коде.
Мне кажется, это гораздо интереснее, чем всякие шаблонизаторы типа Thymeleaf, Freemaker и т.п
import kotlinx.html.*
fun HTML.head(title : String, description : String) = head {
meta {
charset = "utf-8"
}
meta {
name = "description"
content = description
}
title {
+title
}
//Подключаем библиотеки
script { src = "https://cdn.tailwindcss.com" }
script { src = "https://unpkg.com/htmx.org@2.0.0" }
}
Таким образом мы можем выстраивать древовидную структуру, как в Compose.
Стили Tailwind задаются в classes.
get("/") {
call.respondHtml(HttpStatusCode.OK) {
lang = "ru-RU"
head(
title = "Загрузчик видео и аудио с YouTube",
description = "FlowTube - удобный сервис для быстрого скачивания видео и аудио из YouTube по ссылке."
)
body {
classes = setOf("bg-white dark:bg-white")
navbar()
searchForm()
rustoreBanner()
}
}
}
Теперь непосредственно про использование HTMX.
У загрузчика есть форма с текстовым полем для ввода ссылки.
По нажатию на кнопку происходит запрос к серверу, получаем данные из YouTube и выдаем пользователю в виде готового html куска.
import kotlinx.html.*
fun FlowContent.searchForm() = div {
id = "content"
form {
attributes["hx-post"] = "/searchvideo"
attributes["hx-target"] = "#search-content"
label {
+"Search"
}
div {
div {
unsafe {
raw(
"""<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>""".trimIndent()
)
}
}
input {
type = InputType.search
id = "url"
name = "url"
placeholder = "Введите ссылку на видео"
required = true
}
button {
type = ButtonType.submit
+"Найти"
}
}
}
//Потом мы вставим html cюда
div {
id = "search-content"
}
}
Атрибут hx-post означает, что мы делаем POST-запрос к /searchvideo, передаем параметр url из input.
hx-target обозначает то место, куда мы вставим наш html кусок.
post("/searchvideo") {
val videoUrl = call.receiveParameters()["url"].toString()
val videoInfo = InnerTubeRepository().getVideoInfo(videoId = exactYouTubeId(videoUrl))
call.respondHtml {
body {
///Генерируем html
}
}
}
Работает. Но давайте добавим ещё индикатор загрузки.
Для этого нам воспользуемся атрибутом htmx-indicator
form {
attributes["hx-indicator"] = "#spinner-indicator"
//Остальной код
}
//Наша крутилка
div {
role = "status"
id= "spinner-indicator"
classes = setOf("htmx-indicator", "py-2")
attributes["hx-trigger"] = "load"
unsafe {
raw("""
<svg aria-hidden="true" class="mx-auto w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
<span class="sr-only">Loading...</span>
""".trimIndent())
}
}
Как обойти ограничение YouTube через прокси
Ну напоследок.
Вполне вероятно, что вы захотите запустить сервис где-то на облаке.
Я столкнулся с тем, что при любом запросе YouTube выдает ошибку “Get the best YouTube experience. Sign in to save videos, subscribe, and more” или другое похожее сообщение "если вы не бот, то авторизуйтесь"
Простейшее решение:
fun create() : InnerTubeApiImpl {
return InnerTubeApiImpl(client = HttpClient(CIO) {
engine {
// Меняем на свой адрес прокси
proxy = ProxyBuilder.http("http://0.0.0.0:8000/")
}
defaultRequest {
//Авторируемся через Username и password
val credentials = Base64.getEncoder().encodeToString("USERNAME:PASSWORD".toByteArray())
header(HttpHeaders.ProxyAuthorization, "Basic $credentials")
}
})
Спасибо за внимание.
Надеюсь, что эта статья была для вас полезной!
Комментарии (10)
TequilaOne
01.07.2024 06:26Есть ли возможность загружать в качестве 1080р 1440р ? Или возможности строго ограничены , и только в 360р загружать?
zhirnoov Автор
01.07.2024 06:26К сожалению, сейчас YouTube ввел ограничение.
Все популярные веб-загрузчики отдают максимум лишь 360p.
1080p тоже можно, но без звука. Требуется склейка с аудиодорожкой.
В моем Android приложении это реализовано на клиентской стороне.
Для веб версии пока нет, возможно в будущем.Pubert
01.07.2024 06:26Для python есть pytube, отдающий список ссылок на ресурсы. Для высоких расширений отдаются ссылки на видео и аудио отдельно, можно клеить ffmpeg с python-адаптером. Уверен, для котлина тоже есть такая возможность
zhirnoov Автор
01.07.2024 06:26Можно. В моем Android приложении ровно так и сделано через FFmpeg, но на стороне клиента. Можно почитать в статье на Хабре.
С вебом же я мало знаком. Получается придется хранить исходники на сервере и готовый файл, чтобы его потом отдавать. У этого есть подводные камни.
Меня уже как-то протроллили выкачкой 12 часов журчания ниагарского водопада в 4К 60FPS и весом в 50 гигабайт:)
yumupdate
Количество рекламы будто на крипто кран зашёл.
Я без претензий, просто улыбнуло)
Видео https://youtu.be/Ko_Ne4wSUjk?si=yiw9t06DtiLyL1-U так и не открылось(
zhirnoov Автор
Фикшу, что-то сломалось
zhirnoov Автор
Реакт победил. Шучу, перезапустил сервис и всё заработало само.
Вечером плотно сяду за рефакторинг и разбор.
yumupdate
Я не разраб, но используя htmx действительно можно реализовывать довольно крутые фичи без особых навыков js.
Click to edit
Scroll to load
Live search
Progress update (status bar)
Мне htmx нравится!