Commit Graph

28 Commits

Author SHA1 Message Date
gitea 6a6b821a0d AI: add a self-hosted Ollama backend, and let the picker choose the model
The bot could talk to OpenAI or Anthropic; this adds Ollama as a third
provider so it can run against models hosted on our own box, and extends
the switch command to pick WHICH model - not just which backend.

Provider: Ollama exposes an OpenAI-compatible /v1 surface, so the client is
just openai.AsyncOpenAI(base_url=OLLAMA_URL + "/v1"). That reuses the
existing message format and the whole _map_openai_error mapping instead of
forking a second error taxonomy. There is no API key - the endpoint IS the
configuration, so the backend stays dormant (and refuses to be selected,
with a message naming the variable) until CONJURER_OLLAMA_URL is set, the
same way the Conan bridge behaves.

Model selection:
* list_provider_models() asks the SERVER for Ollama (/v1/models), so the
  picker shows what is actually pulled on the box rather than a hardcoded
  list. Hosted providers just report what they are wired to.
* set_active_model() pins the config's latest_model and persists it;
  cheap_model is left alone so the MUSIC path keeps its cheaper backend.
* $gadaj_teraz now takes "<config> [model]", and a new read-only $modele_ai
  lists what is available. Pinning an id Ollama does not have is rejected up
  front with the real list - otherwise the typo only surfaces later as a
  failed reply.

Two fixes this exposed:
* AI_CONFIGS now merges built-in defaults with the settings-file block
  instead of letting the file win outright. Every provider switch persists a
  "configs" block, so a file written by an older build would have
  permanently hidden ollama from the picker after an upgrade.
* _persist_active_ai_config assigns "configs" instead of setdefault, so a
  pinned model actually survives a restart.
* the hardcoded 120s response timeout is now CONJURER_AI_TIMEOUT_SECONDS - a
  self-hosted model on a modest GPU can legitimately need longer.

Tests cover: ollama appears in the picker, select_model maps the legacy
gpt-4o default instead of leaking it, model listing (server-queried, sorted,
de-duplicated, failure -> AIError, unconfigured -> auth), pinning (latest
only, blank/unknown rejected), and that provider_generate routes to the new
path. Suite: 68 unit + 70 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 15:41:28 +00:00
gitea 26b6ab636e Librarian: answer each query back to the bot that sent it
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Successful in 26s
CI / integration (pull_request) Successful in 26s
So one librarian can serve several bots (test + deploy) instead of firing
every result/pong at a single static CONJURER_MAIN_BOT.

* The bot includes its own callback address (CONJURER_SELF_CALLBACK) in
  every /query and /ping.
* The librarian stores that callback with the query (persisted with the
  request, so a replay after restart still answers the right bot) and, for
  results, in the OUTBOX entry ({target, payload}) so the resender delivers
  to the origin bot even across a librarian restart.
* Pongs go back to the pinging bot too - otherwise a second bot's health
  check would be ponged to the first and always time out, so it could
  never enable its librarian cog.
* Empty callback falls back to MAIN_BOT_ADDRESS, and a legacy OUTBOX entry
  (raw payload, pre-callback) is still delivered to the default bot, so the
  upgrade is seamless.

Tests: per-origin result delivery + legacy-shape fallback (outbox),
busy/idle pong routed to the callback bot vs default (lifecycle). Suite:
58 unit + 52 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 18:36:49 +02:00
gitea 44b7298a15 Durable result delivery: OUTBOX + idempotent INBOX so results never die
build / build (push) Successful in 46s
CI / compile (push) Successful in 10s
CI / unit (push) Successful in 20s
CI / integration (push) Successful in 27s
An 8h search result must survive a transient bot outage, an api/address
misroute, or a restart of either side. Make the librarian->bot result
path durably at-least-once with idempotent rendering:

Shared: durable_queue.DiskQueue - a dependency-free, atomically-written,
one-file-per-key disk queue (unit-tested), shared by both images
(added to Dockerfile.librarian; the bot already COPYs *.py).

Librarian (sender): finished results go to a persistent OUTBOX before
sending; delivery retries with backoff; an entry is removed only on a
positive ACK; a resender thread keeps flushing the OUTBOX, so a result
survives a bot outage AND a librarian restart (OUTBOX is on the state
volume) - it simply keeps trying until acked.

Bot (receiver): /conjurer is now idempotent and durable - each result is
persisted to an INBOX before acking and only queued if its uuid was not
already delivered (dropped as a duplicate) or already pending. Once the
cog actually renders it, mark_delivered() records the uuid and clears the
inbox, so the librarian's resends become no-ops. On startup the bot
replays any accepted-but-unrendered result from the INBOX, so a bot crash
mid-flight doesn't lose it. Pongs stay ephemeral.

Together: the librarian keeps a result until the bot confirms it; the bot
keeps it until it is on screen; duplicates never double-render. Combined
with the deploy return-path fix, an expensive result no longer vanishes.

Tests: unit test_durable_queue; integration test_librarian_outbox
(retry/backoff, resend survives outage) and test_result_durable_delivery
(persist, dedup pending, dedup delivered, replay, pong not persisted).
Suite: 55 unit + 39 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 15:31:37 +00:00
gitea 5d321f2f5b Librarian: busy-aware ping + per-query lost-result watchdog
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 20s
CI / integration (pull_request) Successful in 20s
CI / compile (push) Successful in 10s
CI / unit (push) Successful in 21s
CI / integration (push) Successful in 20s
build / build (push) Successful in 57s
Two refinements to the librarian health/delivery story, matching how it
actually behaves under load:

1. Busy-aware ping (case b - broken return path). A ping arriving while
   the worker is grinding a search no longer queues behind it (which made
   a healthy-but-busy librarian time out and look dead). The librarian
   tracks worker_busy and, when set, pongs back IMMEDIATELY without
   touching the queue. Being busy is fine - you can keep piling searches
   on. The ping still travels the librarian->bot return path, so it keeps
   catching the one thing it must: a disrupted/incompatible return path
   where queries vanish. Idle pings still go through the internal queue.

2. Per-query watchdog (case a - finished but result lost). The librarian
   now tracks every search uuid's lifecycle (queued -> processing ->
   gone) in active_queries, exposed via a new POST /query_status. After
   dispatching a search the bot records it in self.pending; watch_pending
   polls /query_status for each. While the librarian still knows the uuid
   the search is progressing - left alone. The moment a uuid VANISHES
   there while still pending on the bot, its result was computed but never
   delivered: after a grace window (to rule out an in-flight result) the
   bot posts a notice to the channel - but ONLY then. A normally delivered
   result is popped from self.pending by check_data_q and never flagged.

Hardening: the worker's search body is now wrapped in try/except/finally
so a crashing search can't kill the worker thread (which would freeze the
queue), and worker_busy / active_queries are always cleared. The grace
logic lives in a dependency-free librarian_watchdog.pending_verdict so it
is unit-testable without discord/pdf libs. /ping and /query_status are
plain (sync) views so they run without flask[async].

Tests: unit test_librarian_watchdog (verdict transitions); integration
test_librarian_query_lifecycle (query_status known/unknown + auth,
idle-ping-queues, busy-ping-pongs-directly). Suite: 28 integration + 48
unit green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 18:58:03 +02:00
gitea ed8b271b4e Gate librarian cog on a full ping round-trip, not a bare GET
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 18s
CI / integration (pull_request) Successful in 18s
The librarian health check was a plain GET to '/', which only proved
Flask was listening - not that the service could actually take a query,
run it through its internal queue+worker, and answer back. So the cog
could load against a librarian whose worker was wedged or that couldn't
reach the bot on the return leg.

Replace it with a ping that travels the SAME path a real search does, on
both sides:
  bot: QueryControl -> OUT_COMM_Q -> scan_queue -> awaiting_q
  librarian: POST /ping -> librarian_queue -> worker pulls it off
             (no Crossref/DOI search) -> pongs back with the same uuid
  bot: /conjurer -> incoming_q -> scan_incoming matches uuid, wakes waiter
The cog enables only when that whole loop closes within 3s. This also
proves the librarian->bot return path, which a GET never did.

Safety: uuid is random per ping; the wait and POST are both bounded so
startup can't stall; a pong that finds no waiter is dropped (never
orphaned into IN_COMM_Q, which would make the cog post a bogus 'no
results' message); and a ping whose pong never returns is swept out of
awaiting_q after PING_TTL_SECONDS so nothing leaks. All awaiting_q writes
stay within scan_queue (append) and scan_incoming (remove) - no locks,
no cross-thread mutation.

Integration tests cover: OK round-trip, timeout when accepted-but-no-pong,
unreachable, non-200, orphan-pong-dropped, and that real results still
reach IN_COMM_Q. Suite: 24 integration + 41 unit green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 15:00:04 +02:00
gitea 3f4a1d5083 lore: bound pamiec.json by summarising old memory into "Legendy Baru"
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Failing after 10s
CI / integration (pull_request) Successful in 12s
build / build (push) Successful in 37s
CI / compile (push) Successful in 38s
CI / unit (push) Failing after 1m19s
CI / integration (push) Successful in 9s
The conversation memory file grows forever (every chat appends a user+assistant
pair), so startup load gets slower and the disk fills. New always-loaded cog
lore_commands turns that growth into content: a background task summarises the
oldest slice into one in-character "legend" via the ACTIVE AI backend, replaces
those old messages with the summary (bounding the file, keeping continuity for
the next startup's context), archives it to legendy.json, and announces it on

Safety: the compaction transforms (build_transcript, apply_compaction) are pure
and unit-tested. The file rewrite is re-read -> back up -> atomic write with no
await in between, so a handle_response append that lands while the summary is
being generated can neither be lost (it's in the preserved tail) nor corrupt
the file (single-threaded, no interleave). A .bak is kept. Scope note: this
bounds the on-disk file (startup/disk); the in-RAM MESSAGE_TABLE is a separate
concern left untouched to avoid yanking context from a live conversation.

Commands: $zapisz_legende (Vykidailo) forces a compaction now; $legendy recalls
a random past legend. All thresholds env-overridable (CONJURER_MEMORY_COMPACT_*,
CONJURER_LEGENDS_CHANNEL). constants gains LEGENDS_FILE + config + seed; bot.py
registers the cog.

Verified: tests/unit/test_lore_commands.py covers prefix-replace/tail-keep,
preservation of appends made during summarisation, and transcript formatting +
head/tail truncation. Unit job 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 20:12:59 +02:00
gitea e9e731e2bd bar: $nalej invents cocktails, $menu keeps the bar's growing lore
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 16s
CI / integration (pull_request) Successful in 11s
build / build (push) Successful in 42s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 14s
CI / integration (push) Successful in 11s
The most in-character capability the bot has: the persona is literally a 200kg
bartender who mixes strong drinks with intriguing names. New always-loaded cog
bar_commands:

* $nalej [motyw] asks the ACTIVE AI backend (whatever $gadaj_teraz selects) to
  invent one themed cocktail in Conjurer's voice - persona reused from
  GPT_SETTINGS[0] as a system message, instructions as the user turn, via
  handle_response request_type NONE so it never pollutes the bar's conversation
  memory. Empty motyw = a surprise; "radio"/"pod muzykę" themes the drink on the
  track currently playing (PREPPED_TRACKS["now_playing"]).
* every drink is appended to menu.json (new seeded state file, CONJURER_MENU_FILE
  overridable) with name/theme/author/timestamp/full text - emergent bar lore.
* $menu lists the invented drinks and pours one at random from the archive.

Text-only for now; a DALL-E drink image is an easy follow-up (the render path
already exists in ai_commands, OpenAI-only).

constants gains MENU_FILE (next to pamiec.json by default) + its seed; bot.py
registers bar_commands as a core cog. Verified: tests/unit/test_bar_commands.py
covers name extraction (markers/markdown/fallback) and the menu round-trip
incl. corrupt-file tolerance; unit job 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 16:55:20 +02:00
Michal Tuszowski f3f07bc921 file share: route the bot's client through config + shared auth
file_search_functions was the only bot-side service client that hardcoded the
musician's LAN address and sent no auth header. Both musician endpoints it
calls (/get_share_list, /get_share_links) run _authorize_request(), so the
whole file-share feature 401'd on any deployment that set CONJURER_API_KEY -
which the deployment guides instruct you to do - and the hardcoded IP made a
musician on another host (the Proxmox/docker split) unreachable regardless.

Use FILE_SERVICE_ADDRESS + service_headers() like music_functions does, add
the two endpoint paths to constants alongside the existing ones, and set an
explicit timeout so a wedged musician can't hang the calling cog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:02:27 +02:00
Michal Tuszowski 3d9d47aa90 ai: single-switch GPT/Claude backend for the chat cog
Wire the bot's AI chat pipeline (ai_functions.handle_response) to talk to
either OpenAI or the Anthropic Messages API, chosen by one active-config
switch. Behaviour on the default "gpt" config is unchanged.

constants.py:
* guarded `import anthropic` + CLAUDECLIENT (mirrors OPENAICLIENT), netrc
  machine 'anthropic' / ANTHROPIC_API_KEY;
* CLAUDE_LATEST_MODEL / CLAUDE_CHEAP_MODEL (opus-4-8 / haiku-4-5);
* AI_CONFIGS + DEFAULT_AI_CONFIG loaded from an optional 3rd element of
  system_gpt_settings.json (backward compatible - a 2-element file falls
  back to built-in defaults, active "gpt"). Single switch: CONJURER_AI_CONFIG
  env > settings "active" > "gpt".

ai_functions.py:
* provider_generate() dispatches to OpenAI (unchanged openai_call) or the new
  _anthropic_call() (splits system out, alternating messages, max_tokens,
  temperature omitted - Opus 4.8 rejects sampling params);
* AIError normalises both SDKs' exceptions into one category set so
  handle_response keeps its single set of in-character error replies;
* select_model() reads the active config; legacy "gpt-4o" default auto-maps
  to the active provider's model so the switch actually changes the backend;
* set_active_ai_config()/list_ai_configs() with best-effort persistence back
  into system_gpt_settings.json index 2.

ai_commands.py:
* $gadaj_teraz <config> hybrid command (Vykidailo-gated) switches backend at
  runtime;
* graceful guards when OPENAICLIENT is None: personal assistants (OpenAI
  Assistants API) and DALL-E image gen degrade instead of crashing, so a
  Claude-only deployment boots.

system_gpt_settings.json: add the configs block (gpt/claude/_template) as the
collection point for future backends. requirements_bot.txt: add anthropic.
bot.env.example: ANTHROPIC_API_KEY + CONJURER_AI_CONFIG. Unit tests cover the
message splitter, model selection, config listing, and error mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:34:59 +02:00
Michal Tuszowski 2a21fc9e8c split: betoniarka (radio operator) colocated with liquidsoap; musician goes Discord-only
Permissions post-mortem that motivated this: the musician wrote radio
playlists AS ROOT onto a ROOT-OWNED network share which liquidsoap then
read AS USER 'radio' - chown fails on such shares by design (root squash /
uid mapping), so the radio came up and died on the playlists. The fix is
structural: the playlist WRITER now lives in the same container as the
READER, as the same user, on a local volume. No shared partition, no
chown, no uid mapping.

New: conjurer_betoniarka/betoniarka.py - runs inside the radio container
(started by the entrypoint as user 'radio', port 5005):
- library scan -> all_playlist/hit playlists with LOCAL container paths
  (start + every 24h + authenticated GET /rescan)
- bot-facing radio API moved from the musician: /add_to_priority,
  /create_priority_playlist, /request_radio_file, /clear_pr_pls,
  plus GET /ping (health) and /stream (web page)
- radio_log/persistence tailer forwarding play events to the bot's
  /prepped_tracks with the shared API key (bot-unreachable = logged, not fatal)

Musician: pure Discord music player now - keeps /mp3, /update_mp3,
/get_music and the file-share endpoints; all radio playlist writing, radio
paths/env and the tailer removed.

Bot: new CONJURER_RADIO_SERVICE (defaults to CONJURER_FILE_SERVICE so
un-split deployments keep working); radio_commands targets it; separate
'radio' health-gate group on betoniarka /ping (musician group now covers
music_commands + file_search_commands only).

Docker: betoniarka baked into the radio image (python3 + flask/waitress/
requests from Debian debs), port 5005 exposed, entrypoint starts it via
setpriv as 'radio'; data-volume chown is now best-effort with a loud
warning (keep the volume local); docs get the post-mortem + wiring.

Verified: py_compile everything; functional stub tests - rescan writes
local-path playlists, wyszukaj scores and appends to priority, auth
401/ok, tailer forward carries the API key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:42:41 +02:00
Michal Tuszowski 8ba06dbc4f fix: voice_recognition uses constants for credentials and paths
The cog did its own netrc read from a hardcoded /home/pi/.netrc at import
time, crashing on any other host. Now:

- constants.py: ASSEMBLYAI_API_KEY resolved like every other token
  (env ASSEMBLYAI_API_KEY -> netrc machine 'assemblyai' at
  CONJURER_NETRC_FILE); new TRANSCRIPTS_PATH (env
  CONJURER_TRANSCRIPTS_PATH, defaults next to the log file, created by the
  runtime layout)
- voice_recognition_commands.py: drop the hardcoded netrc read and
  transcript dir; when the key is missing raise a clear RuntimeError so
  the guarded loader disables ONLY this cog with a readable reason
- bot.env.example: document ASSEMBLYAI_API_KEY

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:07:18 +02:00
Michal Tuszowski 020a6b114a fix: make bot startup resilient (self-healing state, gated cogs, loud failures)
Container/native startup died silently on any missing file/service. Now:

constants.py - self-healing runtime layout at import:
- create missing dirs (log dir, LOGSTORE, GRAPHICS_PATH, MUSIC_FOLDER)
- seed missing state files from the repo templates shipped next to
  constants.py (settings/system_gpt/pamiec/pamiec_muzyki/accident_log),
  falling back to safe empty JSON; existing files are NEVER overwritten

bot.py:
- log to stdout too, so 'docker logs' finally shows the crash reason
- missing Discord token = loud sys.exit with mount/env instructions
  (was: silent return -> container crash-loop with empty logs)
- every cog loads independently (one broken cog = skipped with traceback,
  bot continues)
- musician/librarian cogs are health-gated: enabled only when the service
  answers HTTP; a watchdog re-checks every 5 min and enables them the
  moment the service comes alive (no restart needed); tree re-synced
- on_ready reconnects no longer re-load extensions

requirements_conan.txt + Dockerfile.bot: aiomcrcon (Python <=3.11 only)
moved to best-effort extras so the 3.13 image builds clean and the
conanjurer cog stays dormant without it.

DOCKER_PROXMOX.md: startup model (core vs gated cogs) + crash-loop
troubleshooting incl. the 'disappearing files' checklist (nothing in the
stack deletes host files; bind mount = live state).

Verified: fresh-volume seeding creates dirs+templates, existing files
untouched, missing-token exits with FATAL message, health-gating logic
(stub-based runpy tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:07:18 +02:00
root 4ff0427b64 Docker and installation issues 2026-07-07 23:22:12 +02:00
Michal Tuszowski 597bc004fc docker: Proxmox deployment for bot/librarian/musician + code fixes
Containerises the three services (each intended for its own Proxmox VM)
and adds the code changes needed to run cleanly on Linux/Docker.

Code fixes:
- constants.py: CONJURER_DATA_DIR roots all writable bot state under one
  mounted volume (per-variable overrides still win; native Pi unaffected)
- conjurer_librarian/search_bot.py + scrape_bot.py: the hardcoded Windows
  DOI database path (C:\Database\chunks\) is now CONJURER_LIBRARIAN_DB_PATH,
  with CONJURER_LIBRARIAN_MAXTHREADS / _CHUNK also env-overridable

Docker:
- docker/Dockerfile.{bot,librarian,musician} + compose.{bot,librarian,musician}.yaml
- docker/env/*.env.example (force-added; real *.env stays gitignored)
- docker/entrypoint.bot.sh seeds default JSON state into /data only when
  absent, so preserved history is never overwritten
- .dockerignore
- docs/deployment/DOCKER_PROXMOX.md: step-by-step runbook incl. preserving
  the existing command/conversation history and cross-VM auth

The bot image uses the vendored yt_dlp/spotify_dl forks (they win on
sys.path over the pip packages), dropping the old sed patching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:16:56 +02:00
Michal Tuszowski 5adeb1b384 Land prototype on main (fix stacked-PR retarget gap)
PRs #10 and #11 were merged into their intermediate base branches
(restructure/working-copy-root and proto-improvements) rather than main,
because the stacked PRs' bases were not auto-retargeted (the branches were
not deleted on merge). As a result main only received the #9 restructure
and is still the plain working-copy bot.

This brings the full prototype onto main as a clean delta on top of the
current main tree (identical content to proto-improvements, but with main
ancestry so it merges without the squash-induced rename/delete conflicts):

- constants.py: env-var config, safe JSON loading, dependency guards,
  env->netrc tokens, API_SHARED_KEY + service_headers(), CONAN_* config
- communication_subroutine.py: queue timeout/Empty, daemon threads,
  cooperative stop_event, inbound _authorize_request()
- bot.py: asyncio event loop + load conanjurer_commands
- music_functions / radio_commands / librarian_commands: X-Conjurer-Api-Key
- conanjurer_commands/_functions: fixed + integrated bridge with RCON
  player-join notifications
- requirements_bot.txt: aiomcrcon, asyncssh
- conjurer_musician/.gitignore: keep runtime playlists/mp3 out of the repo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:28:14 +02:00
Michal Tuszowski a64fb2da57 Restructure: promote working_copy to repo root
Make the stable 'working copy' bot the canonical code at the repository
root so the install/deploy scripts operate against it again.

- Move working_copy/* to root (bot entrypoint is bot.py)
- Restore root-level install/ops scripts from c4fa88e (deploy.sh,
  install_main_bot.sh, status_report.*, conjurer.service, etc.)
- Fix deploy.sh: copy bot.py (was thin_client.py) and add the
  conanjurer_* modules; bump command count
- Remove side-by-side variant dirs (backup_old_docker, prototype_one,
  prototype_musician_one, musician_old, working_copy) and docker cruft
- Keep components as subdirs: conjurer_librarian, conjurer_musician,
  spotify_dl, yt_dlp, fonts, utils, docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:50:55 +02:00
Michal Tuszowski 8e5e4ce530 Backup old. Preparation for forking 2026-06-16 14:09:27 +02:00
gitea f9ad679833 Dockerization + ai review recomendations. 2025-10-29 14:57:43 +01:00
gitea 7ef1502249 upgrade to latest OpenAI API usage and logging improvements 2025-08-20 21:20:00 +02:00
gitea 5204c257cf tectonic 2025-08-16 18:49:51 +02:00
gitea dd869cef36 var fx 2025-08-16 18:10:07 +02:00
gitea b9e0f73534 Fix in default config 2025-04-18 13:45:33 +02:00
gitea b4fe6270a9 Fixes after reinstall 2025-04-17 20:44:39 +02:00
gitea 0e860fe6a9 Add auth to ytdlp 2025-04-09 18:55:52 +02:00
gitea 4a6afa14cc bold commiting at the end of the day 2024-11-15 00:37:14 +01:00
gitea 9a0f869839 Main refactoring 2024-11-10 15:03:44 +01:00
gitea 1b4746aa9d fx 2024-11-07 11:02:58 +01:00
gitea c4e0dea7da Refactoring step one 2024-11-07 09:54:52 +01:00