From 3dbe5bb64c002d455867e152675f5403d813ed38 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Fri, 5 Jun 2026 14:21:12 +0200 Subject: [PATCH 1/6] ci(issues): detect likely-duplicate issues on open (#1645) --- .github/workflows/detect-duplicate.yml | 38 ++++ scripts/detect-duplicate-issue.mjs | 236 +++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 .github/workflows/detect-duplicate.yml create mode 100644 scripts/detect-duplicate-issue.mjs diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml new file mode 100644 index 000000000..09aa23565 --- /dev/null +++ b/.github/workflows/detect-duplicate.yml @@ -0,0 +1,38 @@ +name: 🔁 Detect Duplicate Issues + +on: + issues: + types: [opened] + +permissions: + contents: read + +concurrency: + group: detect-duplicate-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + detect: + name: 🔍 Find similar issues + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + permissions: + issues: write + contents: read + steps: + - name: 📥 Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: 🔍 Detect duplicate issues + run: bun scripts/detect-duplicate-issue.mjs + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} diff --git a/scripts/detect-duplicate-issue.mjs b/scripts/detect-duplicate-issue.mjs new file mode 100644 index 000000000..26886b265 --- /dev/null +++ b/scripts/detect-duplicate-issue.mjs @@ -0,0 +1,236 @@ +#!/usr/bin/env bun +/** + * Flags likely-duplicate issues when a new issue is opened, using lexical similarity + * (Jaccard over word sets of the title and body) — no API key, no embeddings. + * + * On a match it posts ONE comment listing the closest open issues and adds the + * "possible duplicate" label. If nothing is similar enough, it does nothing. + * + * Env: + * GITHUB_REPOSITORY owner/repo + * ISSUE_NUMBER the new issue number + * ISSUE_TITLE the new issue title + * ISSUE_BODY the new issue body + * GH_TOKEN/GITHUB_TOKEN for gh (provided in CI) + * DUP_THRESHOLD similarity threshold 0..1 (default 0.3) + * DUP_MAX max matches to report (default 5) + * DUP_FIXTURE optional path to a JSON array of {number,title,body} (local testing) + * DRY_RUN if set, print results instead of commenting/labelling + */ + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +// Parse a numeric env var, falling back to `def` only when unset/empty/NaN so an explicit 0 is honoured. +const numEnv = (name, def) => { + const raw = process.env[name]; + if (raw === undefined || raw === "") return def; + const n = Number(raw); + return Number.isNaN(n) ? def : n; +}; + +const REPO = process.env.GITHUB_REPOSITORY || "streamyfin/streamyfin"; +const NUMBER = numEnv("ISSUE_NUMBER", Number.NaN); +const TITLE = process.env.ISSUE_TITLE || ""; +const BODY = process.env.ISSUE_BODY || ""; +const THRESHOLD = numEnv("DUP_THRESHOLD", 0.3); +const MAX = numEnv("DUP_MAX", 5); +const DRY = !!process.env.DRY_RUN; +const LABEL = "possible duplicate"; +const MARKER = ""; + +// Generic stop words only — keep domain/feature/platform words (android, downloads, +// subtitles…) since those are exactly what makes two reports the same or different. +const STOP = new Set( + ( + "a an the and or but if then of to in on at by for with from as is are was were be been being do does did " + + "it its this that these those i you we they me my your our their he she him her " + + "when while where what which who how why so just then than too very can could would should will " + + "not no nor only own same s t don dont im ive please thanks hi hello also still get got use used using " + + "app application streamyfin issue bug" + ).split(/\s+/), +); + +const stem = (w) => w.replace(/(ing|ed|es|s)$/, ""); + +const tokens = (s) => + (s || "") + .toLowerCase() + .replace(/```[\s\S]*?```/g, " ") // drop code blocks + .replace(//g, " ") // drop html comments + .replace(/https?:\/\/\S+/g, " ") // drop urls + .replace(/[^a-z0-9\s]/g, " ") + .split(/\s+/) + .filter((w) => w.length > 2 && !STOP.has(w)) + .map(stem) + .filter((w) => w.length > 2); + +const jaccard = (a, b) => { + const A = new Set(a); + const B = new Set(b); + if (!A.size || !B.size) return 0; + let inter = 0; + for (const x of A) if (B.has(x)) inter++; + return inter / (A.size + B.size - inter); +}; + +const newTitle = tokens(TITLE); +const newBody = tokens(BODY); +const score = (o) => + 0.6 * jaccard(newTitle, tokens(o.title)) + + 0.4 * jaccard(newBody, tokens(o.body)); + +// fetch open issues (excluding PRs and the new issue itself) +let issues; +if (process.env.DUP_FIXTURE) { + issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8")); +} else { + const raw = execFileSync( + "gh", + [ + "api", + `repos/${REPO}/issues`, + "--paginate", + "-X", + "GET", + "-f", + "state=open", + "-f", + "per_page=100", + "--jq", + ".[] | select(.pull_request | not) | {number, title, body}", + ], + { encoding: "utf8", maxBuffer: 1e8 }, + ); + issues = raw + .split("\n") + .filter(Boolean) + .map((l) => JSON.parse(l)); +} + +const matches = issues + .filter((o) => o.number !== NUMBER) + .map((o) => ({ ...o, s: score(o) })) + .filter((o) => o.s >= THRESHOLD) + .sort((a, b) => b.s - a.s) + .slice(0, MAX); + +if (!matches.length) { + console.log("No likely duplicates found."); + process.exit(0); +} + +// Neutralise other issues' titles before echoing them back: break @mentions and +// strip markdown/HTML control chars so a maliciously-named issue can't ping people +// or inject formatting into our comment. GitHub linkifies "#123" on its own. +const safeTitle = (t) => + (t || "") + .replace(/@/g, "@​") + .replace(/[`<>|*_~[\]]/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 140); +const list = matches + .map( + (m) => + `- #${m.number} — ${safeTitle(m.title)} (≈ ${Math.round(m.s * 100)}% similar)`, + ) + .join("\n"); +const comment = [ + MARKER, + "🔍 **This looks like it might be a duplicate.** Possibly related open issues:", + "", + list, + "", + "If yours is different, ignore this — a maintainer will confirm. Otherwise, please 👍 the existing issue and add any extra details there.", +].join("\n"); + +console.log(`Found ${matches.length} possible duplicate(s):\n${list}`); + +if (DRY) { + console.log("\nDRY_RUN: not commenting/labelling."); + process.exit(0); +} + +// Live mode needs a real issue number; refuse rather than POST to /issues/NaN/... +if (!Number.isInteger(NUMBER) || NUMBER <= 0) { + console.error( + `Invalid ISSUE_NUMBER ${JSON.stringify(process.env.ISSUE_NUMBER)} — refusing to comment.`, + ); + process.exit(1); +} + +// Idempotency: skip if we've already flagged this issue (guards re-runs / future triggers). +const priorComments = execFileSync( + "gh", + [ + "api", + `repos/${REPO}/issues/${NUMBER}/comments`, + "--paginate", + "--jq", + ".[].body", + ], + { encoding: "utf8", maxBuffer: 1e8 }, +); +if (priorComments.includes(MARKER)) { + console.log("Already flagged (marker present); skipping."); + process.exit(0); +} + +execFileSync( + "gh", + [ + "api", + "-X", + "POST", + `repos/${REPO}/issues/${NUMBER}/comments`, + "-f", + `body=${comment}`, + ], + { stdio: "ignore" }, +); +try { + execFileSync( + "gh", + [ + "api", + "-X", + "POST", + `repos/${REPO}/issues/${NUMBER}/labels`, + "-f", + `labels[]=${LABEL}`, + ], + { stdio: "ignore" }, + ); +} catch { + // label may not exist yet — create then add + execFileSync( + "gh", + [ + "api", + "-X", + "POST", + `repos/${REPO}/labels`, + "-f", + `name=${LABEL}`, + "-f", + "color=fbca04", + "-f", + "description=Automatically flagged as a possible duplicate", + ], + { stdio: "ignore" }, + ); + execFileSync( + "gh", + [ + "api", + "-X", + "POST", + `repos/${REPO}/issues/${NUMBER}/labels`, + "-f", + `labels[]=${LABEL}`, + ], + { stdio: "ignore" }, + ); +} +console.log("Commented and labelled."); From 394262af5a317e7e034e4b039bfd1437084a6561 Mon Sep 17 00:00:00 2001 From: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:37:35 +0200 Subject: [PATCH 2/6] Update Streamyfin version options in issue template Add new version options for Streamyfin in issue report template. --- .github/ISSUE_TEMPLATE/issue_report.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index ccdb0d0ed..fb67b57f5 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -77,9 +77,11 @@ body: label: Streamyfin Version description: What version of Streamyfin are you running? options: + - 0.54.1 + - 0.51.0 - 0.47.1 - 0.30.2 - - older + - Older - TestFlight/Development build validations: required: true From 89190b5054c2cc4c3513b77ab62bebe7ca0dab4b Mon Sep 17 00:00:00 2001 From: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:51:10 +0200 Subject: [PATCH 3/6] Revise issue report template for clarity and accuracy Updated placeholders and descriptions for device and server information in issue report template. --- .github/ISSUE_TEMPLATE/issue_report.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index fb67b57f5..9848cbce1 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -67,7 +67,7 @@ body: attributes: label: Which device and operating system are you using? description: Please provide your device model and OS version - placeholder: e.g. iPhone 15 Pro, iOS 18.1.1 or Samsung Galaxy S24, Android 14 + placeholder: e.g. iPhone 17 Pro, iOS 26.5.1 or Samsung Galaxy S25, Android 16 validations: required: true @@ -75,12 +75,10 @@ body: id: version attributes: label: Streamyfin Version - description: What version of Streamyfin are you running? + description: What version of Streamyfin are you using? options: - 0.54.1 - 0.51.0 - - 0.47.1 - - 0.30.2 - Older - TestFlight/Development build validations: @@ -92,9 +90,9 @@ body: label: Jellyfin Server Information description: Please provide details about your Jellyfin server placeholder: | - - Jellyfin Server Version: e.g. 10.10.7 - - Server OS: e.g. Ubuntu 22.04, Windows 11, Docker - - Connection: e.g. Local network, Remote via domain, VPN + - Jellyfin Server Version: e.g. 10.11.10 + - Server OS: e.g. Ubuntu 26.04, Windows 11, Docker, Proxmox + - Connection: e.g. Local network, remote via domain, VPN - type: textarea id: screenshots @@ -106,7 +104,7 @@ body: id: logs attributes: label: Relevant logs (if available) - description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URLs or usernames.** + description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URL, API keys or usernames.** render: shell - type: textarea From b0c9dc114ce88db318d33603f155c34689b55347 Mon Sep 17 00:00:00 2001 From: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:54:07 +0200 Subject: [PATCH 4/6] Revise bug report template for clarity and specificity Updated issue report template to specify improvements for Streamyfin and enhanced placeholder text for clarity. --- .github/ISSUE_TEMPLATE/issue_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 9848cbce1..af86644dd 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -1,5 +1,5 @@ name: "🐛 Bug Report" -description: Create a report to help us improve +description: Create a report to help Streamyfin improve title: "[Bug]: " labels: - "🐛 bug" @@ -36,7 +36,7 @@ body: attributes: label: What happened? description: A clear and concise description of what the bug is. - placeholder: Describe what happened in detail. + placeholder: Describe what happened in detail, the more precise the better. validations: required: true @@ -67,7 +67,7 @@ body: attributes: label: Which device and operating system are you using? description: Please provide your device model and OS version - placeholder: e.g. iPhone 17 Pro, iOS 26.5.1 or Samsung Galaxy S25, Android 16 + placeholder: e.g. iPhone 17 Pro / iOS 26.5.1, Samsung Galaxy S25 / Android 16, Apple TV / tvOS 26.5 validations: required: true From a242ff69fd1d84aaa057f124cc3dab199c072c56 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Mon, 8 Jun 2026 12:10:43 +0200 Subject: [PATCH 5/6] chore(i18n): sentence-case and polish the en.json source (#1663) --- app/(auth)/(tabs)/(favorites)/see-all.tsx | 6 +- app/(auth)/(tabs)/(home)/downloads/index.tsx | 8 +- app/(auth)/(tabs)/(home)/settings.tv.tsx | 18 +- components/TrackSheet.tsx | 2 +- translations/ar.json | 8 +- translations/ca.json | 8 +- translations/cs.json | 8 +- translations/da.json | 8 +- translations/de.json | 8 +- translations/el.json | 8 +- translations/en.json | 744 +++++++++---------- translations/eo.json | 8 +- translations/es.json | 8 +- translations/fi.json | 8 +- translations/fr.json | 8 +- translations/he.json | 8 +- translations/hu.json | 8 +- translations/it.json | 8 +- translations/ja.json | 8 +- translations/ko.json | 8 +- translations/nb.json | 8 +- translations/nl.json | 8 +- translations/nn.json | 8 +- translations/no.json | 8 +- translations/pl.json | 8 +- translations/pt-BR.json | 8 +- translations/pt.json | 8 +- translations/ro.json | 8 +- translations/ru.json | 8 +- translations/sq.json | 8 +- translations/sv.json | 8 +- translations/th.json | 8 +- translations/tlh.json | 8 +- translations/tr.json | 8 +- translations/uk.json | 8 +- translations/vi.json | 8 +- translations/zh-CN.json | 8 +- translations/zh-TW.json | 8 +- translations/zh.json | 8 +- 39 files changed, 521 insertions(+), 529 deletions(-) diff --git a/app/(auth)/(tabs)/(favorites)/see-all.tsx b/app/(auth)/(tabs)/(favorites)/see-all.tsx index e3b0198ab..c885afdc2 100644 --- a/app/(auth)/(tabs)/(favorites)/see-all.tsx +++ b/app/(auth)/(tabs)/(favorites)/see-all.tsx @@ -161,9 +161,7 @@ export default function FavoritesSeeAllScreen() { /> {!itemType ? ( - - {t("favorites.noData", { defaultValue: "No items found." })} - + {t("favorites.noData")} ) : isLoading ? ( @@ -194,7 +192,7 @@ export default function FavoritesSeeAllScreen() { ListEmptyComponent={ - {t("home.no_items", { defaultValue: "No items" })} + {t("home.no_items")} } diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index da4a8272c..68340e6bf 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -137,12 +137,12 @@ export default function DownloadsPage() { deleteFileByType("Episode") .then(() => toast.success( - t("home.downloads.toasts.deleted_all_tvseries_successfully"), + t("home.downloads.toasts.deleted_all_series_successfully"), ), ) .catch((reason) => { writeToLog("ERROR", reason); - toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); + toast.error(t("home.downloads.toasts.failed_to_delete_all_series")); }); const deleteOtherMedia = () => Promise.all( @@ -207,7 +207,7 @@ export default function DownloadsPage() { - {t("home.downloads.tvseries")} + {t("home.downloads.series")} @@ -288,7 +288,7 @@ export default function DownloadsPage() { {t("home.downloads.delete_all_movies_button")} {otherMedia.length > 0 && (