# Tube Archivist. Настройка и кастомизация

**URL:** <https://forum.samohosting.ru/t/tube-archivist-nastrojka-i-kastomizacziya/1926>\
**Category:** Фото\\Медиа сервисы\
**Tags:** youtube\
**Created:** [23.Февраль.2026 12:30:06 UTC](https://forum.samohosting.ru/t/tube-archivist-nastrojka-i-kastomizacziya/1926 "2026-02-23T12:30:06Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Aleksei](https://forum.samohosting.ru/user_avatar/forum.samohosting.ru/aleksei/32/1578_2.png) [@Aleksei](https://forum.samohosting.ru/u/Aleksei)\
**Post date:** [23.Февраль.2026 12:30:07 UTC](https://forum.samohosting.ru/t/tube-archivist-nastrojka-i-kastomizacziya/1926/1 "2026-02-23T12:30:07Z")

</div>

Всем привет.  
Публикую эту заметку для тех, кому интересен вопрос самохостинга собственного экземпляра youtube.  
Вопрос сохранения доступа к важным и ценным youtube-видео в нынешних реалиях стоит особенно остро.  
В ходе поиска подходящего для этих целей сервиса остановился на opensource проекте TubeArchivist. Понравилось в этом проекте следующие:

1. Возможность скачивать видео через свой webui
2. Возможность скачивать видео через браузер посредством расширения (FireFox, Chrome)
3. Возможность скачивать комментарии и субтитры к видео
4. Предусмотрена каталогизация видео (в разрезе каналов)
5. Можно формировать свои плейлисты
6. Гибкая настройка и защита от блокировки со стороны youtube

📋 Будет многобукв поэтому разделю материал на 2 части:

1. Рекомендации по установке и настройке
2. Кастомизация подхода по хранению файлов

# Часть-1: Рекомендации по установке и настройке

Общая схема того, что будем реализовывать  
 ![](https://forum.samohosting.ru/uploads/default/original/2X/4/4adf9f1a2416432d25b145c40ba0d6bcf2948572.gif)  
🛠 Версии ПО:  
PVE: 9.1.5  
OS: Debian 13.1 (trixie)  
Docker Engine: 28.5.1, build e180ab8  
TubeArchivist: 0.5.9

🔗 Ссылки:

- [Официальная документация](https://docs.tubearchivist.com/installation/docker-compose/)
- [Репозиторий Tube Archivist на GitHub](https://github.com/tubearchivist/tubearchivist/blob/master/docker-compose.yml)
- [Расширения для браузеров](https://github.com/tubearchivist/browser-extension)
- [Видео от ProHomeLab](https://www.youtube.com/watch?v=7P55RGvtVKY)
- [Docker-compose от ProHomeLab](https://github.com/stilicho2011/ubuntu_rep/blob/main/tubearchivist/docker-compose.yaml)

Изначально начинал настраивать сервис по видео и репозиторию товарища stilicho2011 (ProHomelab), но на сегодняшний день мануал немного устарел, поэтому будьте внимательны и сверяйте часы с официальной документацией.

### ⚙ Настройка директорий в LXC:

При настройке volumes исходил из того, что все настройки и кэши будут храниться в LXC с Tube Archivist, а медиа данные в NAS, в связи с чем структура директорий выглядит следющим образом:

```auto
Proxmox хост
└── /mnt/tubearchivist
    └── youtube <-- NFS-директория с медиа-контентом на хосте PVE
        (смонтирована в LXC)

LXC контейнер
├── /mnt/tubearchivist/youtube <-- смонтировано из Proxmox
│ └── (в Docker контейнер /youtube)
└── /opt/tubearchivist
    ├── cache <-- локальная, смонтирована в Docker /cache
    ├── redisdata <-- локальная, смонтирована в Docker /data
    ├── elasticsearch <-- локальная, смонтирована в Docker /usr/share/elasticsearch/data
    └── script <-- служебная, не монтируется в Docker (опционально, см. 2-ю статьи)

```

⚠ Важно!  
Для директори elasticsearch (сервис archivist-es) необходимо изменить владельца:  
`chown 1000:0 /opt/tubearchivist/elasticsearch`

📝 Пример настройки прав:

```auto
root@TubeArchivist:/opt/tubearchivist# ls -lnh
total 16K
drwxr-xr-x 9 0 0 4.0K Feb 21 23:07 cache
drwxr-xr-x 6 1000 0 4.0K Feb 21 23:12 elasticsearch
drwxr-xr-x 2 0 0 4.0K Feb 21 22:46 redisdata
drwxr-xr-x 2 0 0 4.0K Feb 21 20:08 script

```

### 🐳 Подготовка docker-compose

👍 Рекомендованным способом установки является использование docker.  
Ниже привожу пример своего docker-compose файла с комментариями. Ключевые изменения:

1. Для сервиса tubearchivist задана опция запрета повышения привилегий

```auto
    security_opt:
      - no-new-privileges:true

```

1. Для установки в непривилегированный LXC были закоментированы дефолтные лимиты для сервиса archivist-es:

```auto
# ulimits:
# memlock: ## позволяет контейнеру держать всю память в RAM без свопа (в unrivileged LXC не работает. Обходится отключение swap для LXC и выделение достаточного объёма RAM)
# soft: -1 ## в unrivileged LXC не работает!
# hard: -1 ## в unrivileged LXC не работает!

```

1. Добавлен сервис bgutil-provider, который автоматически генеррирует PO Token Provider URL для обхода защиты “подтвердите, что вы не робот”. Youtube может определить загрузчик yt‑dlp как бота и заблокировать загрузку, для решения этой проблемы необходимо интегрировать в Tube Archivist данный сервис (ниже опишу, как настроить)
2. Настройки с комментарием “`## !!!Задай своё значение!!!`” необходимо настроить индивидуально под себя

🐳 Docker-compose:

```auto
services:
  tubearchivist:
    container_name: tubearchivist
    restart: unless-stopped
    image: bbilly1/tubearchivist:latest
    ports:
      - 8000:8000
    volumes:
      - /mnt/tubearchivist/youtube:/youtube ## !!!Задай своё значение!!!
      - /opt/tubearchivist/cache:/cache ## !!!Задай своё значение!!!
    environment:
      - ES_URL=http://archivist-es:9200 # needs protocol e.g. http and port
      - REDIS_CON=redis://archivist-redis:6379
      - HOST_UID=1000
      - HOST_GID=1000
      - TA_HOST=http://192.168.1.232:8000 # set your host name ## !!!Задай своё значение!!!
      - TA_USERNAME=tubearchivist # your initial TA credentials
      - TA_PASSWORD=verysecret # your initial TA credentials
      - ELASTIC_PASSWORD=verysecret # set password for Elasticsearch
      - TZ=Europe/Moscow ## !!!Задай своё значение!!!
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/api/health/"]
      interval: 2m
      timeout: 10s
      retries: 3
      start_period: 30s
    depends_on:
      - archivist-es
      - archivist-redis
    security_opt:
      - no-new-privileges:true

  archivist-redis:
    image: redis/redis-stack-server
    container_name: archivist-redis
    restart: unless-stopped
    expose:
      - "6379"
    volumes:
      - /opt/tubearchivist/redisdata:/data ## !!!Задай своё значение!!!
    depends_on:
      - archivist-es  

  archivist-es:
    image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.11.0
    container_name: archivist-es
    restart: unless-stopped
    environment:
      - "ELASTIC_PASSWORD=verysecret" # matching Elasticsearch password
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "xpack.security.enabled=true"
      - "discovery.type=single-node"
      - "path.repo=/usr/share/elasticsearch/data/snapshot"
# ulimits: ## memlock: -1 позволяет контейнеру держать всю память в RAM, без свопа (в unrivileged LXC не работает!)
# memlock: ##в unrivileged LXC не работает!
# soft: -1 ##в unrivileged LXC не работает!
# hard: -1 ##в unrivileged LXC не работает!
    volumes:
      - /opt/tubearchivist/elasticsearch:/usr/share/elasticsearch/data # check for permission error when using bind mount, see readme ## !!!Задай своё значение!!!
    expose:
      - "9200"

  bgutil-provider: ## сервис для автоматической генерации PO Token Provider URL, позволяет обойти блокировку со стороны youtube.
    image: brainicism/bgutil-ytdlp-pot-provider
    container_name: bgutil-provider
    restart: unless-stopped
    init: true
    expose:
      - "4416"

```

⚠ Важное уточнение по настройке сервиса tubearchivist.  
Для сервиса возможно задать две опции

```auto
      - HOST_UID=1000
      - HOST_GID=1000

```

Которые позволяют сервису сохранять файлы в примонтированную директорию с соответствующим uid:gid.  
Здесь есть важное уточнение, что эти опции не имеют ничего общего с опцией user, которая позволяет **запустить сервис внутри docker-контейнера от нужного вам uid:gid** от которых будет производиться дальнейшая запись файлов.  
Что происходит на самом деле. Сервис tubearchivist **сначала от root:root** сохраняет файлы в директорию, после чего выполняет chown и меняет uid:gid на указанные в docker-compose. Поэтому, если вы на своём NAS установите любые отличные от 0:0 (root:root) uid:gid для директории, куда tubearchivist будет складывать медиа, то получите ошибку `Permission denied`.

### ⚙ Настройка webui

Здесь опишу те настройки, которые мне показались наиболее полезными (полное описание смотрите официальной доке).

1. Авторизация  
1.1 Сервис доступен по адресу, который вы задали в docker-compose. Пример:

```auto
      - TA_HOST=http://192.168.1.232:8000 # set your host name ## !!!Задай своё значение!!!

```

1.2 Логин и пароль так же смотрите в docker-compose:

```auto
      - TA_USERNAME=tubearchivist # your initial TA credentials
      - TA_PASSWORD=verysecret # your initial TA credentials

```

1. Settings → Application (_это пример моих настроек, не претендую на истину!_)  
**🔹Секция Downloads**  
2.1 Download Speed limit → `2048` (KB/s, ограничение скорости минимизирует риск блокировки со стороны youtube)  
2.2 Throttled rate limit → `100` (перезапуск загрузки, если скорость упала ниже заданного порога KB/s)  
2.3 Sleep interval → `10` (sec, интервал между повторными запросами минимизирует риск блокировки со стороны youtube)  
**🔹Секция Download Format**  
2.4 Select download format for yt-dlp → если оставить пустым, то yt-dlp будет загружать наилучшее доступное качество, как правило, это эквивалентно использованию `-f bestvideo*+bestaudio/best`  
2.5 Embed metadata → включить `On` (встраивает метаданные непосредственно в медиа-файл в виде меток, чуть больше времени на обработку при скачивании, но файл становится самодостаточным и метаданные могут быть прочитаны сторонними плеерами)  
**🔹Секция Subtitles**  
2.6 Choose subtitle language → `en,ru` (указать язык субтитров для скачивания)  
2.7 Enable auto generated subtitles → включить `On` (добавляет сгенерированные сабы в качестве запасного варианта к ручным сабам с youtube)  
2.8 Enable subtitle index → если `включить`, то будет индексация субтитров в elasticsearch для последующего поиска по ним, требовательно к железу, включать на своё усмотрение  
**🔹Секция Comments**  
2.9 Index comments → `all,100,all,30,all` (получить максимум 100 родительских сообщений и максимум 30 ответов на них на любой глубине. Это невероятно крутая опция, т.к. в комментариях бывает очень ценная информация)  
2.10 Comment sort method → `sort comments by top`  
**🔹🔥Секция Cookie (ВАЖНО!!!)**  
2.11 PO Token Provider URL → `http://bgutil-provider:4416` (предварительно должен быть поднят docker-конейтейнер с сервисом автогенерации PO Token Provider `bgutil-provider`. Адрес и порт могут быть другими, если вы меняли соответствующие поля в docker-compose)  
Проверка:  
Чтобы убедиться в корректной интеграции между Tube Archivist и bgutil-provider необходимо поставить на загрузку одно видео, после чего открыть лог docker-контейнера bgutil-provider либо в вашем docker-менеджере либо выполнить команду в LXC:  
`docker logs -f bgutil-provider`  
Ключевое, что примерно должно быть в логе:

```auto
1. Started POT server (v1.2.2) on on address [::]:4416 -> ✅ Сервер запущен
2. Using challenge from /att/get -> ✅ TubeArchivist обратился к bgutil-provider, который в свою очередь запросил challenge у YouTube. Значит интеграция работает.
3. Generated IntegrityToken: {...} -> ✅ YouTube принял запрос, IntegrityToken успешно получен, TTL 43200 секунд (≈12 часов)
4. Generating POT for ... -> ✅ TubeArchivist передал ID видео, bgutil-provider сгенерировал валидный PO Token, yt-dlp использует его для скачивания.

```

1. Settings → Scheduler  
Обратите внимание на дефолтные настройки. Рекомендую сдвинуть все запланированные задания на период наименьшей нагрузки сервиса, к примеру:  
3.1 Refresh Metadata → `0 1 *` (ежедневно в 1:00 ночи)  
3.2 Thumbnail Check → `30 1 *` (ежедневно в 1:30 ночи)

### ⚙ Интеграция с браузером (на примере FireFox)

1. Установить расширение Tubearchivist: [ссылка](https://addons.mozilla.org/ru/firefox/addon/tubearchivist-companion/)
2. Открыть расширение в браузере  
2.1 Tube Archivist URL: → `http://192.168.1.232:8000` (указать адрес TA-сервиса, см. раздел Авторизация)  
2.2 Tube Archivist API Key: → указать `API token` (Tubearchivist → Settings → Application → Integrations → API token → Show → скопировать ключ)  
2.3 Save (если всё в порядке, то будет гореть зелёная галочка)  
2.4 Download: → установить чек бокс (после добавления видео в TA скачивание начнётся автоматически)  
 ![001. Browser_1](https://forum.samohosting.ru/uploads/default/original/2X/6/690ff1cd52d9ea62dcd5e4c2bc6ab0a02cb9aae9.png)

### 📥 Процесс скачивания

Скачать видео можно двумя способами - непосредственно через интерфейс Tube Archivist или через расширение для браузера.

Рассмотрим оба способа.  
🔸Самый простой и универсальный способ - это ручное добавление ссылки для скачивания в интерфейсе Tube Archivist.  
Добавляем ссылку для скачивания на вкладке Downloads → Add to download queue (если нужно, чтобы после добавления видео в очередь началась автоматическая загрузка, то включаем настройку “Auto Download”)

 ![](https://forum.samohosting.ru/uploads/default/original/2X/2/2688345c391f9c175069b5311311a5bf7e2757b7.png)  
и жмём Add to queue, после чего процесс скачивания делится на 2 этапа:

1. Подготовка очереди для скачивания

- Extracting URL
- Adding new videos to the queue completed
- Task completed

> **Спойлер**
>
> ![](https://forum.samohosting.ru/uploads/default/original/2X/0/0df625d7f9d3cb719621147cba956a2ec68e8422.png)  
> ![](https://forum.samohosting.ru/uploads/default/original/2X/3/3fd7ef87fe1948708a64c98adaf58d1d83128315.png)  
> ![](https://forum.samohosting.ru/uploads/default/original/2X/8/8a56fb68b392592ec2c11135c797c5b1b6b3ec25.png)

1. Скачивание видео.  
Жмём Start download или скачивание начинается автоматически, если на предыдщем шаге выставили соответствующую настройку:

- Downloading (скачивание видео-файла)
- Processing Video (добавление метаданных в индекс Elasticsearch)
- Add comments for new videos (актуально, если эта опция включена в настройках, см. п.2.9-2.10)
- Task completed

> **Спойлер**
>
> ![](https://forum.samohosting.ru/uploads/default/original/2X/8/8a56fb68b392592ec2c11135c797c5b1b6b3ec25.png)  
> ![](https://forum.samohosting.ru/uploads/default/original/2X/1/1cb3d17f5e85c11e88ddbeb8c8934f7d0c59f607.png)  
> ![](https://forum.samohosting.ru/uploads/default/original/2X/3/38141b8a3f7897c56039cf958ae791f45144c1dc.png)  
> ![](https://forum.samohosting.ru/uploads/default/original/2X/f/f6ee5fc988d6847eed7b10f50f77b3f6e66c8568.png)

🔸Скачивание через расширение для браузера FireFox или Chrome.  
После установки расширения под видео на youtube появятся 2 активные кнопки Subscribe и Download.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/9/94ca8e45424a9bf6842d9c6f8a2a5f778f2f81cc.png)  
Здесь ничего хитрого - нажимаем, задание через API уходит в Tube Archivist после чего выполняется и в yotube меняется статус кнопок.  
 ![](https://forum.samohosting.ru/uploads/default/original/2X/7/7b4b56095ac922d8e41bed6f2d848a703d742dbc.png)

### 🖥 Особенности интерфейса

Расскажу про некоторые неочевидные моменты в интерфейсе Tube Archivist, а именно про удаление видео, удаление каналов целиком со всеми видео, про корректную очистку очереди скачивания.

**🔹Удаление видео**  
Я не с первого раза разобрался, как удалить какое-нибудь видео. Если перейти на вкладку channels и открыть канал, то ниже будет список видео, которые сразу же можно воспроизвести, но меню для удаления отсутвует, как на странице канала, так и в плеере. Для этого необходимо открыть само видео (нажать по названию видео), после чего станут доступны различныые действия для видео, в т.ч. и удаление.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/0/084cb2bd87de6482adabaacce1dc6ba1af136447.jpeg)  
 ![](https://forum.samohosting.ru/uploads/default/original/2X/d/da2820079260e9bc2af53821239a8b9081ea9ad7.jpeg)

**🔹Удаление канала**  
Переходим на вкладку channels, открываем канал, который нужно удалить, переходим во вкладку About, находим кнопку Delete Channel → профит.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/6/6c689c52146e683544c4437546c70a1f296f5d3f.jpeg)

**🔹Очистка очереди скачивания**  
После добавления видео в очередь для скачивания (при условии, что отключено автоматическое скачивание) его можно убрать из очереди, для этого необходимо нажать кнопку Ignore напротив видео. Если решите повторно добавить это видео в очерь для скачивания, то ничего не получится. По какой-то причине разработчик решил усложить такую простую функциональность.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/a/ad4ee191d825e305035eac9c8038fd78bd493df0.png)

Дело в том, что после нажатия кнопки Ignore видео добавилось в список Ignore и дальнейшее добавление в очередь загрузки будет невозможно до того момента, пока мы не очистим список. Для этого необходимо на этой же странице найти и включить переключатель Show only ignored videos, после чего отобразится список видео в Ignore-листе. Для исключения из Ignore необходимо нажать на кнопку Forget напротив видео и вернуть переключатель Show only ignored videos в исходное значение.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/6/657cf7232763865f0b7f8c90012457e7db79a525.png)

### 👥 Про многопользовательский режим

В данный момент такая функциональность не реализована. Можно создать отдельного пользователя для ограничения доступа к настройкам приложения, но библиотека контетнта для всех пользователей будет общая.  
В качестве обходного решения можно использовать плейлисты. К примеру создать плейлисты для User1 и User2 (вкладка playlists → значок `+` → Or create custom playlist → Create) и в настройках видео указать плейлист для нужного пользователя.

 ![](https://forum.samohosting.ru/uploads/default/original/2X/d/dc2a5d8090e17725ffeb8bcb5cbd9a094fd50a8c.jpeg)  
 ![](https://forum.samohosting.ru/uploads/default/original/2X/b/b8fa17d68232dc74303a287820f43ca07caadc1d.png)

### 📊 Потребление ресурсов

Я выставил следующие параметры для LXC при которых сервис показывает стабильную работу:  
Cores: 2  
SWAP: 0  
RAM: 4096 MB  
Disk: 8Gb

Для полноты картины привожу значения основных метрик LXC после установки и запуска Tube Archivist:  
CPU: 1%  
RAM: 1,7GiB  
Disk: 4,3GiB

Для примера сотояние LXC в момент скачивания и индексации видео:

 ![](https://forum.samohosting.ru/uploads/default/original/2X/9/939bd7e339008a72b2ebbc7e7fd3bb4dd18e02b6.png)

### Утилизация дискового пространства

🔹Объём индексов и кэша сразу после установки Tube Archivist:

```auto
--- /opt/tubearchivist ---
    1.2 MiB [######################] /elasticsearch
  232.0 KiB [####] /cache
    8.0 KiB [] /redisdata

```

🔹Скачиваем 10 видео:

```auto
root@TubeArchivist:~# ls -lnh /mnt/tubearchivist/youtube/UCxxxxxxxxxxxxxxxxxxxxxx/
total 637M
-rw-r--r-- 1 1000 1000 98M Feb 22 18:27 Ab3kLm9QwEr.mp4
-rw-r--r-- 1 1000 1000 47K Feb 22 17:59 Ab3kLm9QwEr.ru.vtt
-rw-r--r-- 1 1000 1000 38M Feb 22 18:27 Zx8CvBn2TyU.mp4
-rw-r--r-- 1 1000 1000 48K Feb 22 18:15 Zx8CvBn2TyU.ru.vtt
-rw-r--r-- 1 1000 1000 57M Feb 22 18:27 Qw7ErTy5UiO.mp4
-rw-r--r-- 1 1000 1000 45K Feb 22 18:06 Qw7ErTy5UiO.ru.vtt
-rw-r--r-- 1 1000 1000 94M Feb 22 18:27 Mn4BxZp9LsK.mp4
-rw-r--r-- 1 1000 1000 58K Feb 22 18:14 Mn4BxZp9LsK.ru.vtt
-rw-r--r-- 1 1000 1000 32M Feb 22 18:27 Rt6YuIo3PaS.mp4
-rw-r--r-- 1 1000 1000 47K Feb 22 18:24 Rt6YuIo3PaS.ru.vtt
-rw-r--r-- 1 1000 1000 49M Feb 22 18:27 Lk9JhGt2FdA.mp4
-rw-r--r-- 1 1000 1000 59K Feb 22 18:16 Lk9JhGt2FdA.ru.vtt
-rw-r--r-- 1 1000 1000 62M Feb 22 18:27 Vb5NmQw8ErT.mp4
-rw-r--r-- 1 1000 1000 54K Feb 22 18:07 Vb5NmQw8ErT.ru.vtt
-rw-r--r-- 1 1000 1000 73M Feb 22 18:27 Ht2YpLq7WsX.mp4
-rw-r--r-- 1 1000 1000 56K Feb 22 18:23 Ht2YpLq7WsX.ru.vtt
-rw-r--r-- 1 1000 1000 54M Feb 22 18:27 Kd8FsGh4JkL.mp4
-rw-r--r-- 1 1000 1000 63K Feb 22 17:57 Kd8FsGh4JkL.ru.vtt
-rw-r--r-- 1 1000 1000 85M Feb 22 18:27 Px3CvBn7MmN.mp4
-rw-r--r-- 1 1000 1000 51K Feb 22 18:02 Px3CvBn7MmN.ru.vtt

```

🔹Объём индексов и кэша после скачивания 10 видео:

```auto
--- /opt/tubearchivist ---
    1.8 MiB [######################] /elasticsearch
    1.3 MiB [################] /cache
   20.0 KiB [] /redisdata

```

⚠ Получилось 1,7 MiB служебных данных на 650 MiB медиа файлов, что примерно соответствует 2,7 GiB служебных данных на 1Tb медиа файлов. Учитывайте это при планировании объёма диска, выделяемого под LXC.

# Часть-2: Кастомизация подхода по хранению файлов

Tube Archivist при скачивании видео создаёт директорию (канал) и складывает в неё файлы с видео и метаданными. Все последющие видео с этого канала будут помещаться в эту же папку, такая же логика будет и для других каналов.  
Директории присваивается не имя канала, а его ID, аналогично обстоит ситуация и для видео файлов. Пример структуры директорий и файлов:

```auto
/mnt/tubearchivist/youtube
├── UCx9KpL4mN7qRsT2vW8yZaBcD/ # ID канала
│ ├── Qw7Er9TyUi3.mp4 # Видео (ID видео)
│ ├── Qw7Er9TyUi3.ru.vtt # Метаданные видео
│ ├── Ab3Cd4Ef5Gh.mp4
│ └── Ab3Cd4Ef5Gh.ru.vtt
│
├── UCz1A2b3C4d5E6f7G8h9IjKl/
│ ├── Xy9Za8Bc7De.mp4
│ ├── Xy9Za8Bc7De.ru.vtt
│ └── ...
│
└── UCmNoPqRsTuVwXyZaBcDeFgH/
    ├── Lk8Ji7Hg6F.mp4
    ├── Lk8Ji7Hg6F.ru.vtt
    └── ...

```

Это нужно для того, чтобы Tube Archivist остлеживал скаченные видео и не создавал дубли, а так же при подписке на канал отслеживал недостающие видео и скачивал их. ID у каналов и видео неизменны, поэтому решение разработчика вполне логично.

Мне захотелось иметь человекочитаемые названия директорий и файлов, для чего был написан скрипт с помощью ChatGPT, который создаёт рядом с директорией для хранения исходных youtube видео дополнительную директорию, в которую в виде hardlink-ов дублирует весь контент в человекочитаемом виде:

```auto
/mnt/tubearchivist/readable/ # целевая папка (hardlink-и)
├── Название Канала A/
│ ├── Как настроить Docker.mp4
│ ├── Как настроить Docker.ru.vtt
│ ├── Обзор LXC.mp4
│ └── Обзор LXC.ru.vtt
│
├── Название Канала B/
│ ├── Elasticsearch для начинающих.mp4
│ └── Elasticsearch для начинающих.ru.vtt
│
└── Название Канала C/
    ├── Введение в Redis.mp4
    └── Введение в Redis.ru.vtt

```

🔍Что делает скрипт:

1. Задаёт пути исходной (автоматически берёт volume для youtube из docker) и целевой (для hardlink-ов) папок, подключается к Elasticsearch, создаёт лог-файл
2. Пишет лог с датой/временем в файл, держит последние N-записей
3. Берёт список каналов и видео из Elasticsearch
4. Создаёт целевые папки с читаемыми именами каналов
5. Удаляет лишние папки и файлы, которых нет в источнике или Elasticsearch (при удалении видео или канала в Tube Archivist удаляется и hardlink)
6. Создаёт hardlink файлов из исходной папки в целевую с «читаемыми» названиями
7. Логирует окончание работы

🔄 Схема алгоритма:

```auto
[tubearchivist-check.timer] ──every 5s──▶ [tubearchivist-check.service]
                                            │
                                            │ checks 📂 /mnt/tubearchivist/youtube
                                            │ for changes
                                            ▼
                                ┌─ if changes detected ──┐
                                │ │
                  [tubearchivist-debounce.timer] ──10s──▶ [tubearchivist.service]
                                │ │
                                │ │ runs make_readable_names.py
                                │ │ creates hard links in the
                                └────────────────────────┘ 📂 /mnt/tubearchivist/readable

```

❔Как работает:

1. Таймер `[tubearchivist-check.timer]` каждые 5 секунд запускает сервис проверки `[tubearchivist-check.service]`
2. Сервис проверки запускает небольшорй bash скрипт `check_changes.sh`, который проверяет изменения в исходной директории youtube
3. Если изменения не обнаружены, то ожидает следюушей инициации таймером через 5 секунд
4. Если изменения обнаружены, то запускает таймер задержки `[tubearchivist-debounce.timer]`, который отсчитывает 10 секунд. Если в течение 10 секунд произошли ещё изменения в исходной директории youtube, то таймер задержки сбрасывается и отсчёт начинается сначала. Это нужно, чтобы исключить множественные запуски дальнейшей логики при массовых загрузках видео в Tube Archivist. После чего запускается сервис синхронизации `[tubearchivist.service]`
5. Сервис синхронизации `[tubearchivist.service]` запускает python скрипт `make_readable_names.py`, который анализирует изменения и создаёт/удаляет в целевой директории с hardlink-ами директории файлы, выравнивая структуру с исходной директорией youtube

❔Как запустить:

1. Создать файл в любой удобной для вас директории в LXC c Tube Archivist, например  
`nano /home/install.sh`
2. Скопировать в него следующий код:

```auto
#!/bin/bash
set -e

STEP=1
SUMMARY=()

# -------------------------------
# 1. Checking Python3
# -------------------------------
if command -v python3 &>/dev/null; then
    echo "✅ $STEP. Python3 is installed"
else
    echo "🔎 $STEP. Python3 not found. Installing..."
    apt update
    apt install -y python3
    echo "✅ $STEP. Python3 installation completed"
fi

SUMMARY+=("Step $STEP - Python check")
STEP=$((STEP+1))

# -------------------------------
# 2. Creating directory /opt/tubearchivist/script/
# -------------------------------
SCRIPT_DIR="/opt/tubearchivist/script"
mkdir -p "$SCRIPT_DIR"
echo "📁✅ $STEP. Directory $SCRIPT_DIR created (or already exists)"

SUMMARY+=("Step $STEP - Script directory ready")
STEP=$((STEP+1))

# -------------------------------
# 3. Creating file make_readable_names.py
# -------------------------------
PYTHON_FILE="$SCRIPT_DIR/make_readable_names.py"
echo "📝 $STEP. Creating file $PYTHON_FILE"

SUMMARY+=("Step $STEP - Preparing sync script")
STEP=$((STEP+1))

# -------------------------------
# 4. Filling in required variables
# -------------------------------
echo "⚙️ $STEP. Filling configuration variables..."
read -p "4.1 Enter the directory for hardlinks with readable names (optional, default name: readable). TARGET_ROOT: " TARGET_DIR_NAME
TARGET_DIR_NAME=${TARGET_DIR_NAME:-readable}
read -p "4.2 Enter the path for the log file (optional, default path: /opt/tubearchivist/script/). LOG_FILE: " LOG_FILE
LOG_FILE=${LOG_FILE:-/opt/tubearchivist/script/}
read -p "4.3 Enter the maximum number of lines in the log file (optional, default=100). LOG_MAX_LINES: " LOG_MAX_LINES
LOG_MAX_LINES=${LOG_MAX_LINES:-100}
echo "✅ Configuration variables set"

# =========================
# Auto-detect SOURCE_ROOT from archivist-es container
SOURCE_ROOT=$(docker inspect tubearchivist --format '{{range .Mounts}}{{if eq .Destination "/youtube"}}{{.Source}}{{end}}{{end}}')

if [-z "$SOURCE_ROOT"]; then
    echo "❌ Failed to detect SOURCE_ROOT from Docker"
    exit 1
fi

# =========================
# Calculate TARGET_ROOT (same logic as Python)
TARGET_ROOT="$(dirname "$SOURCE_ROOT")/$TARGET_DIR_NAME"

SUMMARY+=("Step $STEP - Configuration variables set")
STEP=$((STEP+1))

# -------------------------------
# 5. Populating make_readable_names.py
# -------------------------------
cat <<EOF | tee "$PYTHON_FILE" >/dev/null
####################
## The script synchronizes the folder and file structure of YouTube channels, converting “unreadable” file names into human-readable ones using data from Elasticsearch. Script structure:
## 1. Configuration: sets the target directory path for readable names and the log file path.
## 2. Logging: writes timestamped messages to the console and the log file, keeping only the last LOG_MAX_LINES entries.
## 3. Data retrieval: retrieves the list of channels and videos from Elasticsearch.
## 4. Folder preparation: creates target folders with readable channel names.
## 5. Cleanup: removes unnecessary folders and files that do not exist in the source or in Elasticsearch.
## 6. Synchronization: creates hard links from the source folder to the target folder using readable file names.
## 7. Completion: logs the end of execution.
####################

#!/usr/bin/env python3
import os
import re
import requests
import shutil
import subprocess
from pathlib import Path
from datetime import datetime

# =========================
# SETTINGS
# =========================

ES_USER = "elastic"

LOG_FILE = Path("$LOG_FILE")
LOG_MAX_LINES = $LOG_MAX_LINES

# If no file suffix → treat as directory
if LOG_FILE.suffix == "":
    LOG_FILE = LOG_FILE / "tubearchivist_sync.log"

# Ensure parent directory exists
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)

# =========================
# Auto-detect archivist-es container IP address
try:
    ip = subprocess.check_output(
        "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' archivist-es",
        shell=True,
        text=True
    ).strip()
    ES_URL = f"http://{ip}:9200"
except subprocess.CalledProcessError:
    print("Failed to retrieve the IP address of the archivist-es container")
    ES_URL = "http://localhost:9200" # fallback

# =========================
# Auto-detect archivist-es container password
ES_PASSWORD = subprocess.check_output(
    "docker inspect archivist-es --format '{{range .Config.Env}}{{println .}}{{end}}' | grep ELASTIC_PASSWORD | cut -d= -f2",
    shell=True,
    text=True
).strip()

# =========================
# Auto-detect archivist-es container bind volumes
SOURCE_ROOT = Path(subprocess.check_output(
    "docker inspect tubearchivist --format '{{range .Mounts}}{{if eq .Destination \"/youtube\"}}{{.Source}}{{end}}{{end}}'",
    shell=True,
    text=True
).strip())

TARGET_DIR_NAME = "$TARGET_DIR_NAME"
TARGET_ROOT = SOURCE_ROOT.parent / TARGET_DIR_NAME

# =========================
# Helper function for logging
# =========================
def log(msg):
    """Logging with date/time and writing to a file, keeping only the last LOG_MAX_LINES lines"""
    timestamp = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
    line = f"{timestamp} - {msg}"
    print(line) # Console output

    # If the log file does not exist, simply create it
    if not LOG_FILE.exists():
        try:
            with LOG_FILE.open("w", encoding="utf-8") as f:
                f.write(line + "\n")
        except Exception as e:
            print(f"Failed to write to the log: {e}")
        return

    # If the log file exists, read the current lines and append the new message
    try:
        with LOG_FILE.open("r", encoding="utf-8") as f:
            lines = f.readlines()
    except Exception as e:
        print(f"Failed to read the log: {e}")
        lines = []

    lines.append(line + "\n")

    # Keep only the last LOG_MAX_LINES
    if len(lines) > LOG_MAX_LINES:
        lines = lines[-LOG_MAX_LINES:]

    try:
        with LOG_FILE.open("w", encoding="utf-8") as f:
            f.writelines(lines)
    except Exception as e:
        print(f"Failed to write to the log: {e}")
# =========================

def safe_name(name):
    """Remove forbidden characters"""
    return re.sub(r'[<>:"/\\|?*]', '', name).strip()

def es_search(index, query):
    r = requests.get(
        f"{ES_URL}/{index}/_search",
        auth=(ES_USER, ES_PASSWORD),
        json=query,
    )
    r.raise_for_status()
    return r.json()["hits"]["hits"]

def get_channels():
    query = {"size": 10000, "query": {"match_all": {}}}
    return es_search("ta_channel", query)

def get_videos(channel_id):
    query = {
        "size": 10000,
        "query": {
            "match": {"channel.channel_id": channel_id}
        }
    }
    return es_search("ta_video", query)

def main():

    TARGET_ROOT.mkdir(parents=True, exist_ok=True)

    log("Scanning Elasticsearch...")
    channels = get_channels()

    # ==============================
    # 1. Build a map channel_id → readable_name
    # ==============================

    channel_name_map = {}

    for ch in channels:
        ch_id = ch["_source"]["channel_id"]
        ch_name = safe_name(ch["_source"]["channel_name"])
        channel_name_map[ch_id] = ch_name

    # ==============================
    # 2. Removing unnecessary folders in TARGET
    # ==============================

    source_channel_ids = {
        d.name for d in SOURCE_ROOT.iterdir() if d.is_dir()
    }

    for target_dir in TARGET_ROOT.iterdir():
        if not target_dir.is_dir():
            continue

        # Checking if such a channel_id exists
        found = False
        for ch_id, ch_name in channel_name_map.items():
            if target_dir.name == ch_name and ch_id in source_channel_ids:
                found = True
                break

        if not found:
            log(f"Removing the unnecessary directory: {target_dir}")
            shutil.rmtree(target_dir)

    # ==============================
    # 3. Synchronizing each channel
    # ==============================
    any_changes = False

    for ch_id in source_channel_ids:

        source_channel_dir = SOURCE_ROOT / ch_id

        # Channel name from ES or fallback to channel_id
        ch_name = channel_name_map.get(ch_id, ch_id)
        target_channel_dir = TARGET_ROOT / ch_name

        if not target_channel_dir.exists():
            log(f"Creating the channel directory: {target_channel_dir}")
            target_channel_dir.mkdir(parents=True)
            any_changes = True

        videos = get_videos(ch_id)

        video_map = {}
        for video in videos:
            vid_id = video["_source"]["youtube_id"]
            vid_title = safe_name(video["_source"]["title"])
            video_map[vid_id] = vid_title

        valid_files = set()
        new_files_added = False

        # ==============================
        # 4. Creating a hard link
        # ==============================

        for file in source_channel_dir.iterdir():
            if not file.is_file():
                continue

            vid_id = file.name[:11]
            suffix = file.suffix

            readable_title = video_map.get(vid_id)

            if readable_title:
                target_file = target_channel_dir / f"{readable_title}{suffix}"
            else:
                # Fallback if the video is not in ES
                target_file = target_channel_dir / file.name

            valid_files.add(target_file.name)

            if not target_file.exists():
                os.link(file, target_file)
                log(f"File added: {target_file}")
                new_files_added = True

        # ==============================
        # 5. Removing unnecessary files
        # ==============================
        removed_any_file = False
        for target_file in target_channel_dir.iterdir():
            if target_file.is_file() and target_file.name not in valid_files:
                target_file.unlink()
                removed_any_file = True
                log(f"Removing the unnecessary file: {target_file}")

        if new_files_added or removed_any_file:
            any_changes = True # Marking that changes occurred

    if not any_changes:
        log(f"No changes occurred in the {TARGET_ROOT} directory")

    log("Done ✔")

if __name__ == " __main__":
    main()
EOF

echo "🔄✅ $STEP. Directory synchronization script created"

SUMMARY+=("Step $STEP - Sync script created")
STEP=$((STEP+1))

# -------------------------------
# 6. Creating a file check_changes.sh
# -------------------------------
CHECK_FILE="$SCRIPT_DIR/check_changes.sh"
cat > "$CHECK_FILE" <<EOF
#!/bin/bash

WATCH_DIR="$SOURCE_ROOT"
STATE_FILE="/run/tubearchivist.state"

# Collecting a list of files and directories
CURRENT_STATE=\$(find "\$WATCH_DIR" -printf "%P %s %T@\\n" 2>/dev/null | sort)

# First run (initialization)
if [! -f "\$STATE_FILE"]; then
    echo "\$CURRENT_STATE" > "\$STATE_FILE"
    
    # Immediately trigger first sync
    systemctl restart tubearchivist-debounce.timer
    
    exit 0  
fi

PREV_STATE=\$(cat "\$STATE_FILE")

if ["\$CURRENT_STATE" != "\$PREV_STATE"]; then
    echo "\$CURRENT_STATE" > "\$STATE_FILE"
    
    # Trigger debounce timer
    systemctl restart tubearchivist-debounce.timer
fi
EOF
echo "📄✅ $STEP. Check script created"

SUMMARY+=("Step $STEP - Check script created")
STEP=$((STEP+1))

# -------------------------------
# 8. Make executable
# -------------------------------
chmod 700 "$CHECK_FILE"
echo "🔐✅ $STEP. Made the check script executable = 700"

SUMMARY+=("Step $STEP - Permissions set")
STEP=$((STEP+1))

# -------------------------------
# 9. Creating systemd service tubearchivist-check.service
# -------------------------------
SERVICE_FILE="/etc/systemd/system/tubearchivist-check.service"
cat <<EOF | tee "$SERVICE_FILE" >/dev/null
[Unit]
Description=Check NFS changes for Tubearchivist

[Service]
Type=oneshot
ExecStart=$CHECK_FILE
EOF
echo "⚙️✅ $STEP. Check service created"

SUMMARY+=("Step $STEP - Check service created")
STEP=$((STEP+1))

# -------------------------------
# 10. Creating timer tubearchivist-check.timer
# -------------------------------
TIMER_FILE="/etc/systemd/system/tubearchivist-check.timer"
cat <<EOF | tee "$TIMER_FILE" >/dev/null
[Unit]
Description=Poll NFS for Tubearchivist changes

[Timer]
OnBootSec=5s
OnUnitActiveSec=5s
AccuracySec=1s
Unit=tubearchivist-check.service

[Install]
WantedBy=timers.target
EOF
echo "⏱️✅ $STEP. Check timer (every 5 seconds) created"

SUMMARY+=("Step $STEP - Check timer created")
STEP=$((STEP+1))

# -------------------------------
# 11. Creating debounced timer
# -------------------------------
DEBOUNCE_FILE="/etc/systemd/system/tubearchivist-debounce.timer"
cat <<EOF | tee "$DEBOUNCE_FILE" >/dev/null
[Unit]
Description=Debounced Tubearchivist execution

[Timer]
OnActiveSec=10s
AccuracySec=1s
Unit=tubearchivist.service

[Install]
WantedBy=timers.target
EOF
echo "⏳✅ $STEP. Debounce timer created"

SUMMARY+=("Step $STEP - Debounce timer created")
STEP=$((STEP+1))

# -------------------------------
# 12. Creating synchronization service
# -------------------------------
SYNC_SERVICE_FILE="/etc/systemd/system/tubearchivist.service"
cat <<EOF | tee "$SYNC_SERVICE_FILE" >/dev/null
[Unit]
Description=Run Tubearchivist sync script

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 $PYTHON_FILE
EOF
echo "🔄✅ $STEP. Synchronization service created"

SUMMARY+=("Step $STEP - Sync service created")
STEP=$((STEP+1))

# -------------------------------
# 13. Executing systemctl commands
# -------------------------------
echo "🚀 $STEP. Activating systemd services..."
systemctl daemon-reload
systemctl enable --now tubearchivist-check.timer
systemctl enable tubearchivist-debounce.timer
systemctl status tubearchivist-check.timer || true
systemctl status tubearchivist-debounce.timer || true
echo "✅ Services successfully enabled"
SUMMARY+=("Step $STEP - Services activated")

# -------------------------------
# 14. Displaying final message
# -------------------------------
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎉 SYNCHRONIZATION SETUP COMPLETED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 Summary:"
echo "--------------------------------------"

for item in "${SUMMARY[@]}"; do # NEW: выводим таблицу
    printf " %-45s %s\n" "$item" "✅"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Total steps executed: $STEP"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🆕 Created objects:"
echo "--------------------------------------"
printf " %-2s %-29s %s\n" "1." "Python sync script" "$PYTHON_FILE"
printf " %-2s %-29s %s\n" "2." "Bash change check script" "$CHECK_FILE"
printf " %-2s %-29s %s\n" "3." "systemd check service" "$SERVICE_FILE"
printf " %-2s %-29s %s\n" "4." "systemd check timer" "$TIMER_FILE"
printf " %-2s %-29s %s\n" "5." "systemd debounce timer" "$DEBOUNCE_FILE"
printf " %-2s %-29s %s\n" "6." "systemd sync service" "$SYNC_SERVICE_FILE"
echo "--------------------------------------"
echo "📂 SOURCE_ROOT (for TubeArchivist): $SOURCE_ROOT"
echo "📂 TARGET_ROOT (for hardlink) : $TARGET_ROOT"
echo "📝 LOG_FILE : ${LOG_FILE%/}/tubearchivist_sync.log"
echo "📈 LOG_LINES : $LOG_MAX_LINES"
echo ""
echo "🧭 Service diagram: 
[tubearchivist-check.timer] ──every 5s──▶ [tubearchivist-check.service]
                                            │
                                            │ checks 📂 $SOURCE_ROOT
                                            │ for changes
                                            ▼
                                ┌─ if changes detected ──┐
                                │ │
                  [tubearchivist-debounce.timer] ──10s──▶ [tubearchivist.service]
                                │ (delayed start) │
                                │ │ runs make_readable_names.py
                                │ │ creates hard links in the
                                └────────────────────────┘ 📂 $TARGET_ROOT
"

```

1. Сделать файл исполняемым  
`chmod 700 /home/install.sh`
2. Запустить скрипт  
`bash /home/install.sh`

❔Как настроить:  
После запуска скрипта в диалоговом режиме будет предложено задать три необязательных параметра:

1. `Enter the directory for hardlinks with readable names (optional, default name: readable). TARGET_ROOT:`  
🔹Название директории с hardlink-ами (человекочитаемыми именами). Если пропустить эту настройку, то директории будет присвоено значение по умолчанию: `readable`
2. `Enter the path for the log file (optional, default path: /opt/tubearchivist/script/). LOG_FILE:`  
🔹Путь до директории с лог-файлом. Если пропустить эту настройку, то будет присвоено значение по умолчанию: `/opt/tubearchivist/script/`
3. `Enter the maximum number of lines in the log file (optional, default=100). LOG_MAX_LINES:`  
🔹Максимальное количество строк в лог-файле. Если пропустить эту настройку, то будет присвоено значение по умолчанию: `100`

❔Какие объекты будут созданы:

1. Python скрипт синхронизации директорий `/opt/tubearchivist/script/make_readable_names.py`
2. Bash скрипт для проверки изменений в исходной директории youtube `/opt/tubearchivist/script/check_changes.sh`
3. systemd таймер запуска сервиса проверки `/etc/systemd/system/tubearchivist-check.service`
4. systemd сервис проверки `/etc/systemd/system/tubearchivist-check.service`
5. systemd таймер задержки запуска сервиса синхронизации `/etc/systemd/system/tubearchivist-debounce.timer`
6. systemd сервис синхронизации `/etc/systemd/system/tubearchivist.service`

📝 Пример содержимого лог-файла:

```auto
22.02.2026 23:15:22 - Scanning Elasticsearch...
22.02.2026 23:15:22 - Creating the channel directory: /mnt/tubearchivist/readable/Kанал Альфа
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Альфа/Введение в Системы.mp4
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Альфа/Введение в Системы.ru.vtt
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Альфа/Настройка Сети.mp4
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Альфа/Настройка Сети.ru.vtt
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Бета/Основы Базы Данных.mp4
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Бета/Основы Базы Данных.ru.vtt
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Гамма/Установка Виртуальной Машины.mp4
22.02.2026 23:15:23 - File added: /mnt/tubearchivist/readable/Канал Гамма/Установка Виртуальной Машины.ru.vtt
22.02.2026 23:15:23 - Done ✔

```

📝 Пример отчёта после выполнения скрипта:

```auto
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎉 SYNCHRONIZATION SETUP COMPLETED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📊 Summary:
--------------------------------------
 Step 1 - Python check ✅
 Step 2 - Script directory ready ✅
 Step 3 - Preparing sync script ✅
 Step 4 - Configuration variables set ✅
 Step 5 - Sync script created ✅
 Step 6 - Check script created ✅
 Step 7 - Permissions set ✅
 Step 8 - Check service created ✅
 Step 9 - Check timer created ✅
 Step 10 - Debounce timer created ✅
 Step 11 - Sync service created ✅
 Step 12 - Services activated ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total steps executed: 12
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🆕 Created objects:
--------------------------------------
 1. Python sync script /opt/tubearchivist/script/make_readable_names.py
 2. Bash change check script /opt/tubearchivist/script/check_changes.sh
 3. systemd check service /etc/systemd/system/tubearchivist-check.service
 4. systemd check timer /etc/systemd/system/tubearchivist-check.timer
 5. systemd debounce timer /etc/systemd/system/tubearchivist-debounce.timer
 6. systemd sync service /etc/systemd/system/tubearchivist.service
--------------------------------------
📂 SOURCE_ROOT (for TubeArchivist): /mnt/tubearchivist/youtube
📂 TARGET_ROOT (for hardlink) : /mnt/tubearchivist/readable
📝 LOG_FILE : /opt/tubearchivist/script/tubearchivist_sync.log
📈 LOG_LINES : 100

🧭 Service diagram: 
[tubearchivist-check.timer] ──every 5s──▶ [tubearchivist-check.service]
                                            │
                                            │ checks 📂 /mnt/tubearchivist/youtube
                                            │ for changes
                                            ▼
                                ┌─ if changes detected ──┐
                                │ │
                  [tubearchivist-debounce.timer] ──10s──▶ [tubearchivist.service]
                                │ (delayed start) │
                                │ │ runs make_readable_names.py
                                │ │ creates hard links in the
                                └────────────────────────┘ 📂 /mnt/tubearchivist/readable

```

Если у кого есть опыт использования другого аналогичного сервиса, то обязательно делитесь в коментах.
