Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров8K

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

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

Спасибо за внимание.

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

Теги:
Хабы:
Всего голосов 18: ↑14 и ↓4+11
Комментарии10

Публикации

Истории

Работа

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань