From ca584e16927ad120c2f83d4f9c8abb5c902370f7 Mon Sep 17 00:00:00 2001 From: Florian <52180080+florianjs@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:02:01 +0100 Subject: [PATCH 1/3] feat(upload): upload assistant in the browser and as a cli One file or folder in, a published torrent out: private .torrent with the personal announce URL (info hash kept by the tracker, so the uploader seeds the local file), MediaInfo, screenshots hosted on Pixhost (adult flag), and a BBCode description in one of 8 languages. - shared core: isomorphic torrent builder, MediaInfo summary, release name parser, BBCode description builder - web page /upload/assistant: mediainfo.js, screenshots with the browser decoder and an ffmpeg.wasm fallback (WORKERFS, PNG then JPEG), WebCrypto hashing, edit, preview, publish - CLI tools/upload-cli bundled by esbuild at build time and served at /tools/trackarr-upload.mjs (Node 20+, ffmpeg, mediainfo) - descriptions render BBCode (escaped, whitelisted tags, http(s) only) or Markdown; limit raised to 50,000 characters - server: API key auth (Bearer) for uploads, assistant config endpoint, screenshot relay to Pixhost; CSP allows wasm and blob: media/images Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + README.md | 1 + app/components/EditTorrentModal.vue | 2 +- app/components/StepLine.vue | 46 ++ app/components/UploadTorrentModal.vue | 9 +- app/pages/index.vue | 3 + app/pages/torrents/[hash].vue | 33 +- app/pages/upload/assistant.vue | 575 ++++++++++++++++++ app/utils/markdown.ts | 13 + app/utils/uploadAssistant/files.ts | 86 +++ app/utils/uploadAssistant/hash.ts | 29 + app/utils/uploadAssistant/media.ts | 202 ++++++ doc/.vitepress/config.ts | 1 + doc/guide/getting-started.md | 1 + doc/guide/upload-assistant.md | 56 ++ doc/reference/env.md | 8 + i18n/locales/de/assistant.json | 59 ++ i18n/locales/en/assistant.json | 59 ++ i18n/locales/es/assistant.json | 59 ++ i18n/locales/fr/assistant.json | 59 ++ i18n/locales/it/assistant.json | 59 ++ i18n/locales/ja/assistant.json | 59 ++ i18n/locales/ru/assistant.json | 59 ++ i18n/locales/zh/assistant.json | 59 ++ nuxt.config.ts | 8 +- package-lock.json | 59 ++ package.json | 7 + scripts/build-cli.mjs | 18 + server/api/torrents/[hash]/index.patch.ts | 2 +- server/api/torrents/index.post.ts | 8 +- server/api/upload-assistant/config.get.ts | 36 ++ .../api/upload-assistant/screenshot.post.ts | 43 ++ server/utils/csp.ts | 10 +- server/utils/pixhost.ts | 48 ++ server/utils/schemas.ts | 2 +- server/utils/uploaderAuth.ts | 57 ++ shared/utils/bbcode.ts | 220 +++++++ shared/utils/release/description.ts | 190 ++++++ shared/utils/release/labels.ts | 97 +++ shared/utils/release/mediainfo.ts | 167 +++++ shared/utils/release/releaseName.ts | 63 ++ shared/utils/release/torrent.ts | 150 +++++ tests/csp.test.ts | 5 +- tests/upload-assistant.test.ts | 270 ++++++++ tools/upload-cli/main.ts | 359 +++++++++++ 45 files changed, 3345 insertions(+), 14 deletions(-) create mode 100644 app/components/StepLine.vue create mode 100644 app/pages/upload/assistant.vue create mode 100644 app/utils/uploadAssistant/files.ts create mode 100644 app/utils/uploadAssistant/hash.ts create mode 100644 app/utils/uploadAssistant/media.ts create mode 100644 doc/guide/upload-assistant.md create mode 100644 i18n/locales/de/assistant.json create mode 100644 i18n/locales/en/assistant.json create mode 100644 i18n/locales/es/assistant.json create mode 100644 i18n/locales/fr/assistant.json create mode 100644 i18n/locales/it/assistant.json create mode 100644 i18n/locales/ja/assistant.json create mode 100644 i18n/locales/ru/assistant.json create mode 100644 i18n/locales/zh/assistant.json create mode 100644 scripts/build-cli.mjs create mode 100644 server/api/upload-assistant/config.get.ts create mode 100644 server/api/upload-assistant/screenshot.post.ts create mode 100644 server/utils/pixhost.ts create mode 100644 server/utils/uploaderAuth.ts create mode 100644 shared/utils/bbcode.ts create mode 100644 shared/utils/release/description.ts create mode 100644 shared/utils/release/labels.ts create mode 100644 shared/utils/release/mediainfo.ts create mode 100644 shared/utils/release/releaseName.ts create mode 100644 shared/utils/release/torrent.ts create mode 100644 tests/upload-assistant.test.ts create mode 100644 tools/upload-cli/main.ts diff --git a/.gitignore b/.gitignore index 3618abae..fb865be0 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ documentation/ # Test output coverage/ .vitest/ + +# Upload CLI, bundled at build (scripts/build-cli.mjs) +public/tools/trackarr-upload.mjs diff --git a/README.md b/README.md index 39a9052a..21c6bf24 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Built with Nuxt 4 • Rust • PostgreSQL • Redis | Forum, comments, reports, invitations, H&R | UI in 8 languages (EN, FR, ES, DE, IT, ZH, JA, RU) | | Private messages between members, with blocks | Light & dark themes, ⌘K command palette | | Paid sign-up in Bitcoin, Monero or Solana | On-chain payment checks, no processor, no spend key | +| Upload assistant: .torrent, MediaInfo, screens, BBCode | Web or CLI, one file or folder in, published torrent out | | **Security** | **Emergency** | | -------------------------------- | -------------------------------------------- | diff --git a/app/components/EditTorrentModal.vue b/app/components/EditTorrentModal.vue index b0d8dbaa..e4ec9af9 100644 --- a/app/components/EditTorrentModal.vue +++ b/app/components/EditTorrentModal.vue @@ -267,7 +267,7 @@ const error = ref(null); const { data: categories } = await useFetch('/api/categories'); const renderedDescription = computed(() => { - return renderMarkdown(description.value); + return renderDescription(description.value); }); // Initialize form data when torrent prop changes diff --git a/app/components/StepLine.vue b/app/components/StepLine.vue new file mode 100644 index 00000000..ee5a857a --- /dev/null +++ b/app/components/StepLine.vue @@ -0,0 +1,46 @@ + + + diff --git a/app/components/UploadTorrentModal.vue b/app/components/UploadTorrentModal.vue index fd2e6861..23390c3e 100644 --- a/app/components/UploadTorrentModal.vue +++ b/app/components/UploadTorrentModal.vue @@ -20,6 +20,13 @@ {{ t('upload.title') }} + + {{ t('assistant.entry') }} + diff --git a/app/pages/torrents/[hash].vue b/app/pages/torrents/[hash].vue index 0c8390df..cc3635ef 100644 --- a/app/pages/torrents/[hash].vue +++ b/app/pages/torrents/[hash].vue @@ -307,7 +307,7 @@ const externalLinks = computed(() => { return links; }); -const renderedDescription = computed(() => renderMarkdown(torrent.value?.description)); +const renderedDescription = computed(() => renderDescription(torrent.value?.description)); async function copyHash() { await navigator.clipboard.writeText(torrent.value!.infoHash).catch(() => {}); @@ -406,4 +406,35 @@ async function deleteTorrent() { border-left: 2px solid rgb(var(--line-strong)); padding-left: 0.75rem; } +/* BBCode: screenshots side by side, spoilers, MediaInfo report */ +.description-content :deep(a > img) { + display: inline-block; + margin: 0.25rem; + vertical-align: middle; +} +.description-content :deep(details) { + margin-bottom: 0.75rem; + border: 1px solid rgb(var(--line)); + border-radius: 4px; + padding: 0.5rem 0.75rem; +} +.description-content :deep(summary) { + cursor: pointer; + font-weight: 600; + color: rgb(var(--ink)); +} +.description-content :deep(details[open] > summary) { + margin-bottom: 0.5rem; +} +.description-content :deep(pre) { + overflow-x: auto; + white-space: pre; + font-size: 0.8em; + line-height: 1.4; +} +.description-content :deep(cite) { + display: block; + font-style: normal; + font-weight: 600; +} diff --git a/app/pages/upload/assistant.vue b/app/pages/upload/assistant.vue new file mode 100644 index 00000000..69be9bb1 --- /dev/null +++ b/app/pages/upload/assistant.vue @@ -0,0 +1,575 @@ +