From 57293efa7301414ae810a13386fa66e14d9e7d62 Mon Sep 17 00:00:00 2001 From: danieldjupvik Date: Tue, 7 Jul 2026 00:13:40 +0200 Subject: [PATCH 1/6] feat: daily auto-build of seerr latest release with PR #1855 applied --- .github/workflows/build.yml | 161 ++++++++++++++++++++++++++++++++++++ .last-built | 1 + README.md | 43 +++++++++- 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 .last-built diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f92b7ed --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,161 @@ +name: build + +on: + schedule: + - cron: '23 5 * * *' + workflow_dispatch: + inputs: + force: + description: Rebuild even if the latest upstream release was already built + type: boolean + default: false + +permissions: + contents: write + packages: write + issues: write + actions: write + +concurrency: + group: build + cancel-in-progress: false + +env: + UPSTREAM_REPO: seerr-team/seerr + PR_NUMBER: '1855' + IMAGE: ghcr.io/${{ github.repository_owner }}/seerr-hide-requested + GH_TOKEN: ${{ github.token }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Stop if PR 1855 was merged or closed upstream + run: | + pr="$(gh api "repos/$UPSTREAM_REPO/pulls/$PR_NUMBER")" + merged="$(jq -r .merged <<<"$pr")" + state="$(jq -r .state <<<"$pr")" + if [ "$merged" = "true" ]; then + title="Upstream PR #$PR_NUMBER was MERGED — time to switch back to the official image" + body="The hide-requested feature was merged into seerr upstream. + + **What to do now:** + 1. Wait for the next seerr release that includes it (check https://github.com/$UPSTREAM_REPO/releases — the release notes will mention hiding requested media, or PR #$PR_NUMBER). + 2. In Unraid, change the container repository back to \`ghcr.io/seerr-team/seerr:latest\`. + 3. In seerr Settings → General, enable **Hide Requested Media**. + 4. Archive or delete this repo. + + Until that release ships, your current image keeps working — it already contains this feature. This workflow has disabled itself and will not build again." + elif [ "$state" = "closed" ]; then + title="Upstream PR #$PR_NUMBER was CLOSED without merging" + body="The hide-requested PR was closed upstream without being merged. Your current image keeps working, but no new upstream releases will be picked up until you decide what to do (find a replacement PR and update PR_NUMBER in build.yml, or retire this repo). This workflow has disabled itself." + else + echo "PR #$PR_NUMBER is still open — continuing." + exit 0 + fi + existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$title\" in:title" --json number --jq length)" + if [ "$existing" = "0" ]; then + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body" + fi + gh workflow disable build.yml --repo "$GITHUB_REPOSITORY" || true + echo "::error::$title" + exit 1 + + - name: Determine latest upstream release + id: rel + env: + FORCE: ${{ inputs.force }} + run: | + tag="$(gh api "repos/$UPSTREAM_REPO/releases/latest" --jq .tag_name)" + if ! grep -qE '^v[0-9][A-Za-z0-9._-]*$' <<<"$tag"; then + echo "::error::Upstream release tag has unexpected format: $tag" + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + last="$(cat .last-built 2>/dev/null || echo none)" + if [ "$tag" = "$last" ] && [ "$FORCE" != "true" ]; then + echo "Already built $tag — nothing to do." + echo "build=false" >> "$GITHUB_OUTPUT" + else + echo "New upstream release $tag (last built: $last)." + echo "build=true" >> "$GITHUB_OUTPUT" + fi + + - name: Keepalive commit if repo is going stale + if: steps.rel.outputs.build == 'false' + run: | + age_days=$(( ( $(date +%s) - $(git log -1 --format=%ct) ) / 86400 )) + if [ "$age_days" -ge 45 ]; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit --allow-empty -m "chore: keepalive so the schedule is not auto-disabled" + git push + fi + + - name: Fetch upstream source at release tag + if: steps.rel.outputs.build == 'true' + env: + TAG: ${{ steps.rel.outputs.tag }} + run: git clone --depth 1 --branch "$TAG" "https://github.com/$UPSTREAM_REPO.git" upstream + + - name: Apply PR 1855 diff + if: steps.rel.outputs.build == 'true' + env: + TAG: ${{ steps.rel.outputs.tag }} + run: | + gh api -H "Accept: application/vnd.github.diff" "repos/$UPSTREAM_REPO/pulls/$PR_NUMBER" > pr.diff + cd upstream + if ! git apply --check ../pr.diff; then + title="PR #$PR_NUMBER no longer applies to $TAG" + existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$title\" in:title" --json number --jq length)" + if [ "$existing" = "0" ]; then + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \ + --body "The patch conflicts with the new upstream release. Your current image keeps working, but it is now behind upstream. Either wait for the PR author to rebase, or retire this repo and live without the filter until the PR merges." + fi + echo "::error::$title" + exit 1 + fi + git apply ../pr.diff + + - name: Set up Docker Buildx + if: steps.rel.outputs.build == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: steps.rel.outputs.build == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push image + if: steps.rel.outputs.build == 'true' + uses: docker/build-push-action@v6 + with: + context: upstream + push: true + platforms: linux/amd64 + build-args: COMMIT_TAG=${{ steps.rel.outputs.tag }}-pr1855 + tags: | + ${{ env.IMAGE }}:latest + ${{ env.IMAGE }}:${{ steps.rel.outputs.tag }}-pr1855 + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=seerr ${{ steps.rel.outputs.tag }} + PR 1855 (hide requested media) + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Record built tag + if: steps.rel.outputs.build == 'true' + env: + TAG: ${{ steps.rel.outputs.tag }} + run: | + echo "$TAG" > .last-built + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .last-built + git commit -m "chore: built $TAG" + git push diff --git a/.last-built b/.last-built new file mode 100644 index 0000000..621e94f --- /dev/null +++ b/.last-built @@ -0,0 +1 @@ +none diff --git a/README.md b/README.md index b7f0017..47d6ed1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ # seerr-hide-requested -Setup PR incoming — see the open pull request for the full build pipeline and docs. +Automated build of [seerr](https://github.com/seerr-team/seerr) with [PR #1855](https://github.com/seerr-team/seerr/pull/1855) ("hide already requested media") applied on top of the latest official release. Adds a **Hide Requested Media** checkbox to Settings → General, next to the existing Hide Available Media setting. + +This is a temporary bridge until PR #1855 merges upstream — see [issue #1048](https://github.com/seerr-team/seerr/issues/1048). + +## Image + +``` +ghcr.io/danieldjupvik/seerr-hide-requested:latest +``` + +Drop-in replacement for `ghcr.io/seerr-team/seerr:latest` — same app, same config format, same volumes and ports (`linux/amd64` only). In the Unraid container settings, change the **Repository** field to the image above; leave everything else untouched. Switching back later is the same edit in reverse. + +Each build is also tagged `-pr1855` (e.g. `v3.3.0-pr1855`) if you ever need to pin or roll back. + +## How it works + +A daily scheduled workflow ([build.yml](.github/workflows/build.yml)): + +1. Checks whether PR #1855 is still open upstream. If it was **merged or closed**, the workflow opens an issue in this repo telling you to switch back to the official image, disables itself, and fails — that issue is your signal to retire this repo. +2. Fetches the latest upstream release tag and compares it to [.last-built](.last-built). If already built, it exits in seconds (with an occasional empty keepalive commit so GitHub doesn't auto-disable the schedule after 60 days of inactivity). +3. On a new release: clones upstream at that tag, applies the PR diff fresh from the GitHub API, builds with upstream's own Dockerfile, and pushes to GHCR. If the diff no longer applies, it opens an issue and fails instead of shipping a broken image. + +There is no fork to keep in sync — every build starts from pristine upstream source plus the PR diff. + +## One-time setup after the first successful build + +The first push creates the GHCR package as **private**. Make it public so Unraid can pull without credentials and it doesn't count against private package storage: + +GitHub profile → Packages → `seerr-hide-requested` → Package settings → Change visibility → Public. + +## Manual build + +Actions → build → Run workflow (tick **force** to rebuild the current release, e.g. to pick up a rebased version of the PR diff). + +## Failure modes + +| Event | What happens | +| --- | --- | +| PR merged upstream | Issue opened with switch-back instructions, workflow disables itself. Your running image keeps working. | +| PR closed without merge | Issue opened, workflow disables itself. | +| PR diff conflicts with a new release | Issue opened, build fails, previous image stays available. You're behind upstream until the PR is rebased or you retire this repo. | +| No new release | ~5-second no-op run. | From 7dd1ae94520d4355568b543a4018bdf3eec9ac2b Mon Sep 17 00:00:00 2001 From: danieldjupvik Date: Tue, 7 Jul 2026 00:18:45 +0200 Subject: [PATCH 2/6] chore: derive image name from repo, update docs after rename --- .github/workflows/build.yml | 2 +- README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f92b7ed..a910c9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ concurrency: env: UPSTREAM_REPO: seerr-team/seerr PR_NUMBER: '1855' - IMAGE: ghcr.io/${{ github.repository_owner }}/seerr-hide-requested + IMAGE: ghcr.io/${{ github.repository }} GH_TOKEN: ${{ github.token }} jobs: diff --git a/README.md b/README.md index 47d6ed1..9d07f96 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# seerr-hide-requested +# seerr (patched) Automated build of [seerr](https://github.com/seerr-team/seerr) with [PR #1855](https://github.com/seerr-team/seerr/pull/1855) ("hide already requested media") applied on top of the latest official release. Adds a **Hide Requested Media** checkbox to Settings → General, next to the existing Hide Available Media setting. -This is a temporary bridge until PR #1855 merges upstream — see [issue #1048](https://github.com/seerr-team/seerr/issues/1048). +This is a temporary bridge until PR #1855 merges upstream — see [issue #1048](https://github.com/seerr-team/seerr/issues/1048). The same pipeline can carry additional patches in the future: each one is just another diff applied in [build.yml](.github/workflows/build.yml) before the image is built. ## Image ``` -ghcr.io/danieldjupvik/seerr-hide-requested:latest +ghcr.io/danieldjupvik/seerr:latest ``` Drop-in replacement for `ghcr.io/seerr-team/seerr:latest` — same app, same config format, same volumes and ports (`linux/amd64` only). In the Unraid container settings, change the **Repository** field to the image above; leave everything else untouched. Switching back later is the same edit in reverse. @@ -18,7 +18,7 @@ Each build is also tagged `-pr1855` (e.g. `v3.3.0-pr1855`) if A daily scheduled workflow ([build.yml](.github/workflows/build.yml)): -1. Checks whether PR #1855 is still open upstream. If it was **merged or closed**, the workflow opens an issue in this repo telling you to switch back to the official image, disables itself, and fails — that issue is your signal to retire this repo. +1. Checks whether PR #1855 is still open upstream. If it was **merged or closed**, the workflow opens an issue in this repo telling you to switch back to the official image, disables itself, and fails — that issue is your signal to retire the patch. 2. Fetches the latest upstream release tag and compares it to [.last-built](.last-built). If already built, it exits in seconds (with an occasional empty keepalive commit so GitHub doesn't auto-disable the schedule after 60 days of inactivity). 3. On a new release: clones upstream at that tag, applies the PR diff fresh from the GitHub API, builds with upstream's own Dockerfile, and pushes to GHCR. If the diff no longer applies, it opens an issue and fails instead of shipping a broken image. @@ -28,7 +28,7 @@ There is no fork to keep in sync — every build starts from pristine upstream s The first push creates the GHCR package as **private**. Make it public so Unraid can pull without credentials and it doesn't count against private package storage: -GitHub profile → Packages → `seerr-hide-requested` → Package settings → Change visibility → Public. +GitHub profile → Packages → `seerr` → Package settings → Change visibility → Public. ## Manual build @@ -40,5 +40,5 @@ Actions → build → Run workflow (tick **force** to rebuild the current releas | --- | --- | | PR merged upstream | Issue opened with switch-back instructions, workflow disables itself. Your running image keeps working. | | PR closed without merge | Issue opened, workflow disables itself. | -| PR diff conflicts with a new release | Issue opened, build fails, previous image stays available. You're behind upstream until the PR is rebased or you retire this repo. | +| PR diff conflicts with a new release | Issue opened, build fails, previous image stays available. You're behind upstream until the PR is rebased or you retire the patch. | | No new release | ~5-second no-op run. | From 06a847330955242ba70b487a6c8a171700614608 Mon Sep 17 00:00:00 2001 From: danieldjupvik <50077377+danieldjupvik@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:29:18 +0200 Subject: [PATCH 3/6] fix(ci): apply vendored, pinned patch instead of live PR diff Addresses CodeRabbit supply-chain finding: the build no longer fetches the PR diff from the network. The reviewed diff (pinned at PR head cfeeb8ce) is committed under patches/ and can only change via a reviewed commit in this repo. --- .github/workflows/build.yml | 26 ++-- patches/README.md | 9 ++ patches/pr1855.diff | 267 ++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 patches/README.md create mode 100644 patches/pr1855.diff diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a910c9b..e9f1e14 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,24 +100,26 @@ jobs: TAG: ${{ steps.rel.outputs.tag }} run: git clone --depth 1 --branch "$TAG" "https://github.com/$UPSTREAM_REPO.git" upstream - - name: Apply PR 1855 diff + - name: Apply vendored patches if: steps.rel.outputs.build == 'true' env: TAG: ${{ steps.rel.outputs.tag }} run: | - gh api -H "Accept: application/vnd.github.diff" "repos/$UPSTREAM_REPO/pulls/$PR_NUMBER" > pr.diff cd upstream - if ! git apply --check ../pr.diff; then - title="PR #$PR_NUMBER no longer applies to $TAG" - existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$title\" in:title" --json number --jq length)" - if [ "$existing" = "0" ]; then - gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \ - --body "The patch conflicts with the new upstream release. Your current image keeps working, but it is now behind upstream. Either wait for the PR author to rebase, or retire this repo and live without the filter until the PR merges." + for patch in ../patches/*.diff; do + if ! git apply --check "$patch"; then + title="Vendored patch $(basename "$patch") no longer applies to $TAG" + existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$title\" in:title" --json number --jq length)" + if [ "$existing" = "0" ]; then + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \ + --body "The vendored patch conflicts with the new upstream release. Your current image keeps working, but it is now behind upstream. Re-download the updated diff from its source PR, review it, replace the file in patches/, and commit — see patches/README.md." + fi + echo "::error::$title" + exit 1 fi - echo "::error::$title" - exit 1 - fi - git apply ../pr.diff + git apply "$patch" + echo "Applied $(basename "$patch")" + done - name: Set up Docker Buildx if: steps.rel.outputs.build == 'true' diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..2081b4f --- /dev/null +++ b/patches/README.md @@ -0,0 +1,9 @@ +# Vendored patches + +Applied in order by the build workflow on top of the upstream release source. + +| File | Source | Pinned at | +| --- | --- | --- | +| `pr1855.diff` | [seerr-team/seerr#1855](https://github.com/seerr-team/seerr/pull/1855) | head `cfeeb8ce8416f9775e503cfa8a03845d8f9f56f4` (2026-06-18) | + +To pick up a rebased/updated PR: re-download the diff, **review it**, replace the file, and commit — the content shipped into the image only ever changes through a reviewed commit in this repo. diff --git a/patches/pr1855.diff b/patches/pr1855.diff new file mode 100644 index 0000000..96906d6 --- /dev/null +++ b/patches/pr1855.diff @@ -0,0 +1,267 @@ +diff --git a/docs/using-seerr/settings/general.md b/docs/using-seerr/settings/general.md +index 18fd32765f..798c136cc8 100644 +--- a/docs/using-seerr/settings/general.md ++++ b/docs/using-seerr/settings/general.md +@@ -84,6 +84,14 @@ When enabled, media that has been blocklisted will not appear on the "Discover" + + This setting is **disabled** by default. + ++## Hide Requested Media ++ ++When enabled, media that has been requested (but not yet available) will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages. ++ ++Requested media will still appear in search results, however, so it is possible to locate and view hidden items by searching for them by title. ++ ++This setting is **disabled** by default. ++ + ## Allow Partial Series Requests + + When enabled, users will be able to submit requests for specific seasons of TV series. If disabled, users will only be able to submit requests for all unavailable seasons. +diff --git a/seerr-api.yml b/seerr-api.yml +index 99ef16cc3c..eb86e360fd 100644 +--- a/seerr-api.yml ++++ b/seerr-api.yml +@@ -236,6 +236,9 @@ components: + hideAvailable: + type: boolean + example: false ++ hideRequested: ++ type: boolean ++ example: false + partialRequestsEnabled: + type: boolean + example: false +diff --git a/server/entity/Media.ts b/server/entity/Media.ts +index 8737ad0e04..5314123570 100644 +--- a/server/entity/Media.ts ++++ b/server/entity/Media.ts +@@ -53,7 +53,8 @@ class Media { + 'watchlist', + 'media.id= watchlist.media and watchlist.requestedBy = :userId', + { userId: user?.id } +- ) //, ++ ) ++ .leftJoinAndSelect('media.requests', 'requests') + .where(' media.tmdbId in (:...finalIds)', { finalIds }) + .getMany(); + +diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts +index ea08d4e61d..cfa72e4856 100644 +--- a/server/interfaces/api/settingsInterfaces.ts ++++ b/server/interfaces/api/settingsInterfaces.ts +@@ -31,6 +31,7 @@ export interface PublicSettingsResponse { + applicationUrl: string; + hideAvailable: boolean; + hideBlocklisted: boolean; ++ hideRequested: boolean; + localLogin: boolean; + mediaServerLogin: boolean; + movie4kEnabled: boolean; +diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts +index 7057cf2b01..5ab7220a2d 100644 +--- a/server/lib/settings/index.ts ++++ b/server/lib/settings/index.ts +@@ -134,6 +134,7 @@ export interface MainSettings { + }; + hideAvailable: boolean; + hideBlocklisted: boolean; ++ hideRequested: boolean; + localLogin: boolean; + mediaServerLogin: boolean; + newPlexLogin: boolean; +@@ -184,6 +185,7 @@ interface FullPublicSettings extends PublicSettings { + applicationUrl: string; + hideAvailable: boolean; + hideBlocklisted: boolean; ++ hideRequested: boolean; + localLogin: boolean; + mediaServerLogin: boolean; + movie4kEnabled: boolean; +@@ -394,6 +396,7 @@ class Settings { + }, + hideAvailable: false, + hideBlocklisted: false, ++ hideRequested: false, + localLogin: true, + mediaServerLogin: true, + newPlexLogin: true, +@@ -678,6 +681,7 @@ class Settings { + applicationUrl: this.data.main.applicationUrl, + hideAvailable: this.data.main.hideAvailable, + hideBlocklisted: this.data.main.hideBlocklisted, ++ hideRequested: this.data.main.hideRequested, + localLogin: this.data.main.localLogin, + mediaServerLogin: this.data.main.mediaServerLogin, + jellyfinExternalHost: this.data.jellyfin.externalHostname, +diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx +index 4418c12314..e7e09cbfe0 100644 +--- a/src/components/MediaSlider/index.tsx ++++ b/src/components/MediaSlider/index.tsx +@@ -82,6 +82,21 @@ const MediaSlider = ({ + ); + } + ++ if (settings.currentSettings.hideRequested) { ++ titles = titles.filter((i) => { ++ if (i.mediaType !== 'movie' && i.mediaType !== 'tv') { ++ return true; ++ } ++ ++ return ( ++ i.mediaInfo?.status === MediaStatus.AVAILABLE || ++ i.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE || ++ !i.mediaInfo?.requests || ++ i.mediaInfo.requests.length === 0 ++ ); ++ }); ++ } ++ + useEffect(() => { + if ( + titles.length < 24 && +diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx +index 96d41ced7b..40743a0a8d 100644 +--- a/src/components/Search/index.tsx ++++ b/src/components/Search/index.tsx +@@ -34,7 +34,7 @@ const Search = () => { + { + query: router.query.query, + }, +- { hideAvailable: false, hideBlocklisted: false } ++ { hideAvailable: false, hideBlocklisted: false, hideRequested: false } + ); + + if (error) { +diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx +index 4944c2349f..c564b4cc68 100644 +--- a/src/components/Settings/SettingsMain/index.tsx ++++ b/src/components/Settings/SettingsMain/index.tsx +@@ -56,6 +56,9 @@ const messages = defineMessages('components.Settings.SettingsMain', { + hideAvailable: 'Hide Available Media', + hideAvailableTip: + 'Hide available media from the discover pages but not search results', ++ hideRequested: 'Hide Requested Media', ++ hideRequestedTip: ++ 'Hide media that has been requested from the discover pages but not search results', + cacheImages: 'Enable Image Caching', + cacheImagesTip: + 'Cache externally sourced images (requires a significant amount of disk space)', +@@ -165,6 +168,7 @@ const SettingsMain = () => { + applicationUrl: data?.applicationUrl, + hideAvailable: data?.hideAvailable, + hideBlocklisted: data?.hideBlocklisted, ++ hideRequested: data?.hideRequested, + locale: data?.locale ?? 'en', + discoverRegion: data?.discoverRegion, + originalLanguage: data?.originalLanguage, +@@ -185,6 +189,7 @@ const SettingsMain = () => { + applicationUrl: values.applicationUrl, + hideAvailable: values.hideAvailable, + hideBlocklisted: values.hideBlocklisted, ++ hideRequested: values.hideRequested, + locale: values.locale, + discoverRegion: values.discoverRegion, + streamingRegion: values.streamingRegion, +@@ -489,6 +494,26 @@ const SettingsMain = () => { + /> + + ++
++ ++
++ { ++ setFieldValue('hideRequested', !values.hideRequested); ++ }} ++ /> ++
++
+
+