feature/sermon-audio-import #119

Open
EgorEast wants to merge 21 commits from feature/sermon-audio-import into main
Owner

feat: импорт проповеди из YouTube / Invidious (аудио + метаданные)

Что
В админ-форме у поля «YouTube (URL)» — блок импорта. Источники: YouTube (youtubei.js, клиент ANDROID_VR — без PO-токенов) и Invidious (пресеты + свой адрес, хранится в AsyncStorage). Кнопка «Импортировать» (активна при непустом URL): скачивает аудио на устройстве → грузит на сервер → заполняет название, описание, закрепляет аудиофайл.
Веб: работает Invidious-источник (fetch-стрим + FormData); YouTube на вебе отключён чипом с подсказкой (PO-токены в браузере — debt).
Формат m4a без конвертации (осознанно: ffmpeg-kit ретирнут, AAC играет нативно и в вебе; бэкенд 0.12.0+ принимает — проверено end-to-end). Ручной пикер теперь тоже принимает m4a.
Прогресс двухфазный, отмена при размонтировании, temp-файл чистится всегда; ошибки — типизированные тосты («Сервис недоступен», «Видео недоступно»…).

Коммиты

  • chore: add youtubei.js and expo-crypto dependencies (+prebuild)
  • feat: import sermon audio and metadata from YouTube or Invidious
  • docs: document sermon audio import, storage keys and decisions ×2
  • fix: harden sermon audio import after review (abort-контракт, чистый updater, нормализация URL инстанса, youtubeFormat-хелперы + тесты)
  • feat: accept m4a in admin audio picker + feat(web): import sermons from invidious instances

Debt / вне скоупа
YouTube-импорт на вебе (нужны PO-токены в браузере); надёжность Invidious зависит от инстанса; youtubei.js чинится обновлением библиотеки. Детали — docs/debt.md, решения — docs/decisions.md.

feat: импорт проповеди из YouTube / Invidious (аудио + метаданные) Что В админ-форме у поля «YouTube (URL)» — блок импорта. Источники: YouTube (youtubei.js, клиент ANDROID_VR — без PO-токенов) и Invidious (пресеты + свой адрес, хранится в AsyncStorage). Кнопка «Импортировать» (активна при непустом URL): скачивает аудио на устройстве → грузит на сервер → заполняет название, описание, закрепляет аудиофайл. Веб: работает Invidious-источник (fetch-стрим + FormData); YouTube на вебе отключён чипом с подсказкой (PO-токены в браузере — debt). Формат m4a без конвертации (осознанно: ffmpeg-kit ретирнут, AAC играет нативно и в вебе; бэкенд 0.12.0+ принимает — проверено end-to-end). Ручной пикер теперь тоже принимает m4a. Прогресс двухфазный, отмена при размонтировании, temp-файл чистится всегда; ошибки — типизированные тосты («Сервис недоступен», «Видео недоступно»…). Коммиты - chore: add youtubei.js and expo-crypto dependencies (+prebuild) - feat: import sermon audio and metadata from YouTube or Invidious - docs: document sermon audio import, storage keys and decisions ×2 - fix: harden sermon audio import after review (abort-контракт, чистый updater, нормализация URL инстанса, youtubeFormat-хелперы + тесты) - feat: accept m4a in admin audio picker + feat(web): import sermons from invidious instances Debt / вне скоупа YouTube-импорт на вебе (нужны PO-токены в браузере); надёжность Invidious зависит от инстанса; youtubei.js чинится обновлением библиотеки. Детали — docs/debt.md, решения — docs/decisions.md.
Signed-off-by: egoreast <egor.east@yandex.ru>
Adds an import block under the sermon form's YouTube field: it resolves
metadata plus the best mp4/AAC audio track (itag 140, no transcode),
downloads it to the device cache with progress, uploads it via POST /files
and fills audioUrl, title and description.

Sources are switchable (YouTube via youtubei.js InnerTube with the
ANDROID_VR client, Invidious via its public API) with two verified instance
presets plus a custom base URL; the choice is stored in AsyncStorage and
validated with zod on read. The library is imported lazily, so regular
users never download it, and its RN shims (expo-crypto, own eval) are
installed after the library, not before.

downloadFileWithTimeout moves to shared/lib/fs and becomes reusable: the
timeout budget is a caller argument, an external AbortSignal lets the form
cancel a download on unmount, and its message is no longer prefixed with
[updateService] (the update error classifier matches a substring instead).

Signed-off-by: egoreast <egor.east@yandex.ru>
Signed-off-by: egoreast <egor.east@yandex.ru>
Verified against the live API with moderator credentials on 2026-10-05:
POST /files returns 400 "Недопустимый тип файла. Разрешены только: JPEG,
PNG, WebP, MP3, PDF, FB2." for an .m4a name. Both a synthetic ffmpeg
AAC/m4a file and arbitrary bytes are rejected, and the mime type is
irrelevant (audio/mpeg with an .m4a name fails too), so the server
filters by file extension rather than by payload.

Consequence: the upload step of the sermon audio import fails, so the
import does not work end to end until the backend whitelist gains .m4a
or the pipeline converts to mp3 (which needs native ffmpeg). No code
changed - the admin audio picker stays mp3-only, which is correct.

Replaced the "unverified" debt entry with the verified result and the
two options, and corrected the pipeline description in admin.md which
claimed the server returns the m4a URL as is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: harden sermon audio import after review
Some checks failed
CI / dco (pull_request) Successful in 12s
CI / checks (pull_request) Failing after 4m27s
830aadb668
Signed-off-by: egoreast <egor.east@yandex.ru>
EgorEast force-pushed feature/sermon-audio-import from 830aadb668
Some checks failed
CI / dco (pull_request) Successful in 12s
CI / checks (pull_request) Failing after 4m27s
to d627f5347d
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 7m13s
2026-10-05 11:15:51 +00:00
Compare
EgorEast force-pushed feature/sermon-audio-import from d627f5347d
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 7m13s
to 04a8b7eafc
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 5m18s
2026-10-05 11:27:16 +00:00
Compare
feat(web): import sermons from invidious instances
All checks were successful
CI / dco (pull_request) Successful in 12s
CI / checks (pull_request) Successful in 5m14s
42ffc1dcce
Invidious-only on web, YouTube stays native-only, fetch-stream download + browser FormData upload

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: surface upload errors and add import progress bar
Some checks failed
CI / dco (pull_request) Successful in 10s
CI / checks (pull_request) Failing after 12m42s
74f3b07914
Propagate the root cause into ImportSourceError.rootCause and the toast for
upload-failed/service-unavailable, log upload failures, and guard a missing
native download file. Strip the boundary-less multipart Content-Type in the
shared axios request interceptor: the browser xhr adapter sends it as-is, so
the server cannot parse the body (fetch adapter and native OkHttp add the
boundary themselves). Add a themed progress bar under the import button and a
test querying it by role.

Signed-off-by: egoreast <egor.east@yandex.ru>
Import failures now go to the global error dialog (reportError) with
copyable details instead of a toast; success still toasts and aborted
imports stay silent.

YouTube source: create the Innertube session with an explicit ANDROID_VR
user_agent and, on a 403, retry once with WEB_EMBEDDED (PO-token-free).
If both clients fail, the error carries the combined detail with client
and HTTP status (ANDROID_VR: 403; WEB_EMBEDDED: 403).

Invidious source: classify instance failures with the host - 401 means
authorization required, 403 or an HTML body (Anubis anti-bot, incl.
200/5xx) means closed by anti-bot, network/timeout and other non-JSON
stay service-unavailable, JSON error stays video-unavailable. Added a
muted hint under the instance field recommending a self-hosted instance.

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: route import errors to dialog only for diagnostics, add invidious basic auth
All checks were successful
CI / dco (pull_request) Successful in 10s
CI / checks (pull_request) Successful in 5m33s
02f0b7000a
Modal (reportError) now shows only upload-failed and unexpected errors; known
actionable kinds (parse, video-unavailable, login-required, live, no-audio,
service-unavailable incl. anti-bot/auth variants) go to a short toast.

Support Invidious instances behind nginx basic auth: creds@host is normalized
and stored, credentials are sent as an Authorization: Basic header while the
request URL and error text stay credential-free.

Signed-off-by: egoreast <egor.east@yandex.ru>
Metadata (title, description) fills the form right after resolve; audio
downloads and uploads only when the form has no audio file yet.

- importAudio gains withAudio and onMetadata; the metadata-only path returns
  audioUrl: null without download/upload phases.
- useAudioImport splits callbacks (onMetadata / onAudioImported) and picks the
  success toast by whether audio was uploaded.
- ImportFromYoutube/SermonMediaFields pass hasAudio and the split callbacks.
- Upload retries up to 3 attempts with 1.5s/3s backoff for network-level
  failures only; HTTP 4xx/5xx fail fast. Final failure carries file size,
  attempts and elapsed time plus a connection hint.
- POST /files requests use explicit timeout: 0 for large uploads.

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: fix file uploads on web and native pickers, images-only library
Some checks failed
CI / dco (pull_request) Successful in 14s
CI / checks (pull_request) Has been cancelled
3069d4c1ce
Web manual picker reads the document-picker blob: URI via fetch and wraps it in a
browser File (uploadSermonFile no longer constructs expo-file-system File on web,
fixing "this.validatePath is not a function"). Native picker builds the classic RN
multipart part { uri, name, type } and posts it directly via axiosInstance instead
of appending an expo File. Split shared upload primitives into uploadFilePart.ts.
FileUploadField shows the media-library button only for the image kind
(supportsLibrary flag).

Signed-off-by: egoreast <egor.east@yandex.ru>
Merge branch 'main' into feature/sermon-audio-import
All checks were successful
CI / dco (pull_request) Successful in 5s
CI / checks (pull_request) Successful in 6m24s
2217154e9b
Signed-off-by: egoreast <egor.east@yandex.ru>
RN FormData append overload in ambient types removes the cast;
100% upload now shows «Обработка на сервере…» while awaiting response.

Signed-off-by: egoreast <egor.east@yandex.ru>
Merge branch 'main' into feature/sermon-audio-import
All checks were successful
CI / dco (pull_request) Successful in 11s
CI / checks (pull_request) Successful in 5m23s
72dbc05a47
Signed-off-by: egoreast <egor.east@yandex.ru>
Merge branch 'main' into feature/sermon-audio-import
All checks were successful
CI / dco (pull_request) Successful in 4s
CI / checks (pull_request) Successful in 5m23s
8c1e8d0a12
Signed-off-by: egoreast <egor.east@yandex.ru>
feat: backend-driven invidious instance presets and admin management
All checks were successful
CI / dco (pull_request) Successful in 5s
CI / checks (pull_request) Successful in 5m20s
84ae072bfb
Presets are fetched from GET /invidious-instances with a bundled https://inv.phobos.observer fallback on empty/error, so the import form keeps working until the backend is deployed. Adds the admin screen «Источники импорта» (list/add https-validated/delete + PUT full replace, 409/error dialog handling) reachable from AdminQuickActions behind isAdminUser. Regenerated the API client for spec 0.18.3.

Signed-off-by: egoreast <egor.east@yandex.ru>
feat: validate invidious api on add, restore url marquee, fix row layout
All checks were successful
CI / dco (pull_request) Successful in 10s
CI / checks (pull_request) Successful in 5m27s
2f7a4d62dc
client-side probe mirrors the device download path; known failures toast,
unknown open the copyable error dialog; marquee restored for long urls;
row centering/layout fixed

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: treat browser fetch-block as known invidious failure
All checks were successful
CI / dco (pull_request) Successful in 9s
CI / checks (pull_request) Successful in 5m28s
d8de45d672
On web CORS-blocked/unreachable instances throw TypeError with no status — classify
as a known failure with an actionable toast instead of the diagnostic dialog;
native keeps the dialog for unknown device-network errors.

Signed-off-by: egoreast <egor.east@yandex.ru>
Cache removal -> cloud-crossed glyph; history removal -> history/time glyph.
Consistent across row, playlist and header menus.

Signed-off-by: egoreast <egor.east@yandex.ru>
fix: rebrand bulk clear actions icons
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 5m48s
a5c688877f
Consistency with 62cbe1f menu icon rebrand: offline clear uses
cloud-offline-outline, history clear uses time-outline.

Signed-off-by: egoreast <egor.east@yandex.ru>
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 5m48s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/sermon-audio-import:feature/sermon-audio-import
git switch feature/sermon-audio-import

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feature/sermon-audio-import
git switch feature/sermon-audio-import
git rebase main
git switch main
git merge --ff-only feature/sermon-audio-import
git switch feature/sermon-audio-import
git rebase main
git switch main
git merge --no-ff feature/sermon-audio-import
git switch main
git merge --squash feature/sermon-audio-import
git switch main
git merge --ff-only feature/sermon-audio-import
git switch main
git merge feature/sermon-audio-import
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Slovo_Propovedi/slovo-propovedi-mobile!119
No description provided.