Как я написал загрузчик YouTube видео на Ktor + HTMX. Убийца React?

Мы вырастили монстра, который больше не нужен ©

Рассмотрим возможности применения модного фреймворка HTMX в связке с Ktor на примере написания загрузчика видео.

Ещё давным-давно я мечтал ворваться в написание сайтов, но меня всегда останавливал огромный зоопарк JS-фреймворков. Ну в принципе сам JavaScript

К счастью, появился инструмент, который возвращает сайтостроение назад к истокам.
К бэкендерам!

Финальный результат можно оценить на https://flowtube.ru/

Что такое HTMX

Что же из себя представляет HTMX?
Это легковесный фреймворк, который добавляет новые атрибуты для html.
Нам не нужно больше парсить JSON.
Делаем запрос к серверу и вставляем кусок html кода на страницу.
HTMX занял второе место в рейтинге самых популярных фронтенд фрейморков по версии JavaScript Rising Stars 2023.

Очевидно, что у такого подхода существуют свои ограничения и пока не всё получится сделать, но потенциал огромен. Новое золото!

Зумерки переизобрели AJAX и хвастаются. React навсегда!

Зумерки переизобрели AJAX и хвастаются. React навсегда!

Переходим к коду

Осознаю, что в рамках статьи рассмотреть все нюансы своего проекта не получится, я выложил его в открытый доступ на 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.
У загрузчика есть форма с текстовым полем для ввода ссылки.
После нажатия на кнопку должен происходит запрос к серверу, получаем данные и выдаем пользователю в виде готового 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(
                        """""".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("""
                    
                     Loading...
                """.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")  
     }  
  })

Спасибо за внимание.
Надеюсь, что эта статья была для вас полезной!

Habrahabr.ru прочитано 1574 раза