diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78ce602 --- /dev/null +++ b/.env.example @@ -0,0 +1,28 @@ +# NFOGuard Environment Variables + +# User and Group IDs +PUID=1000 +PGID=1000 + +# Timezone +TZ=UTC + +# Media path (for read-only access to scan NFO files) +MEDIA_PATH=/path/to/your/media + +# Database settings (if using PostgreSQL) +# DB_USER=nfoguard +# DB_PASSWORD=your_secure_password + +# Emby Plugin Deployment - Bind Mount Method (Recommended) +# Set this to the path where Emby plugins are installed on your host +# Common paths: +# - /var/lib/emby/plugins (native Emby install) +# - /path/to/emby/config/plugins (Docker Emby) +# - /config/plugins (linuxserver.io Emby) +EMBY_PLUGINS_PATH=/path/to/emby/plugins + +# NFOGuard specific settings +DEBUG=false +PATH_DEBUG=false +SUPPRESS_TVDB_WARNINGS=true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bffa156 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help fixing issues +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots & Logs** +If applicable, add screenshots/logs to help explain your problem. + +**Please complete the following information:** + - OS: [e.g. Fedora Linux 42 x86_64] + - Architecture [e.g. amd64, arm64] + - Docker Version + - Remediarr Version [e.g. 0.1.9] + +**Additional context** +Add any other context about the problem here. Such as logs from Remediarr , Sonarr, Radarr. .env file(with API removed) docker compose file as well. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/changelog-config.json b/.github/changelog-config.json new file mode 100644 index 0000000..9611b8c --- /dev/null +++ b/.github/changelog-config.json @@ -0,0 +1,15 @@ +{ + "template": "# Changelog for {{currentTag}}\n\n{{#conventionalCommits}}\n- {{#scope}}**{{scope}}:** {{/scope}}{{subject}}\n{{/conventionalCommits}}\n", + "categories": [ + { "title": "### ๐Ÿš€ Features", "labels": ["feat", "feature"], "collapseAfter": 0 }, + { "title": "### ๐Ÿ› Fixes", "labels": ["fix", "bug"], "collapseAfter": 0 }, + { "title": "### ๐Ÿงน Chore", "labels": ["chore"], "collapseAfter": 0 }, + { "title": "### ๐Ÿ“ Docs", "labels": ["docs"], "collapseAfter": 0 }, + { "title": "### ๐Ÿ”ง Refactor", "labels": ["refactor"], "collapseAfter": 0 } + ], + "replacers": [], + "sort": { + "order": "ASC", + "onProperty": "scope" + } +} \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..ab36350 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,17 @@ +name-template: 'v$NEXT_PATCH_VERSION' +tag-template: 'v$NEXT_PATCH_VERSION' +categories: + - title: '๐Ÿš€ Features' + labels: ['feature', 'enhancement'] + - title: '๐Ÿ› Fixes' + labels: ['fix', 'bug'] + - title: '๐Ÿงฐ Maintenance' + labels: ['chore', 'maintenance', 'deps'] +change-template: '- $TITLE (#$NUMBER) by @$AUTHOR' +no-changes-template: 'Minor internal improvements.' +template: | + + ## Container Images + - `sbcrumb/nfoguard:latest` + - `sbcrumb/nfoguard:$NEXT_PATCH_VERSION` + diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..26a1ba0 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,35 @@ +name: Generate Changelog on Tag + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + changelog: + runs-on: ubuntu-latest + + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build changelog from commits + id: build + uses: mikepenz/release-changelog-builder-action@v4 + with: + configuration: .github/changelog-config.json + ignorePreReleases: false + toTag: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create / Update GitHub Release notes + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + body: ${{ steps.build.outputs.changelog }} \ No newline at end of file diff --git a/.github/workflows/ghcr-dev.yml b/.github/workflows/ghcr-dev.yml new file mode 100644 index 0000000..885eb6e --- /dev/null +++ b/.github/workflows/ghcr-dev.yml @@ -0,0 +1,67 @@ +name: Build & Push DEV to DockerHub + +on: + pull_request: + branches: [ dev ] + push: + branches: [ dev ] + +permissions: + contents: read + +jobs: + docker: + runs-on: ubuntu-latest + + env: + # PRs: single-arch build (faster). Push to dev: multi-arch. + PLATFORMS: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Read VERSION and compute DEV tags + run: | + if [[ ! -f VERSION ]]; then echo "VERSION file missing"; exit 1; fi + RAW=$(tr -d ' \n' < VERSION) + if [[ -z "$RAW" ]]; then echo "VERSION empty"; exit 1; fi + # Remove 'v' prefix if present for version number + if [[ "$RAW" =~ ^v ]]; then NUM="${RAW#v}"; else NUM="$RAW"; fi + + # Get repository name (assumes format: owner/repo) + REPO_NAME=$(echo "${GITHUB_REPOSITORY}" | cut -d'/' -f2) + + echo "VERSION_NUM=$NUM" >> $GITHUB_ENV + echo "DEV_VERSION=${NUM}-dev" >> $GITHUB_ENV + echo "DOCKER_REPO=${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}" >> $GITHUB_ENV + echo "DEV_TAGS=${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}:${NUM}-dev,${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}:dev" >> $GITHUB_ENV + echo "Computed DEV_VERSION=${NUM}-dev" + + - name: Set up QEMU + if: ${{ env.PLATFORMS == 'linux/amd64,linux/arm64' }} + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DockerHub + if: ${{ github.event_name != 'pull_request' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Build & (maybe) push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: ${{ env.PLATFORMS }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ env.DEV_TAGS }} + build-args: | + APP_VERSION=${{ env.DEV_VERSION }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/ghcr-main.yml b/.github/workflows/ghcr-main.yml new file mode 100644 index 0000000..dc1e2e0 --- /dev/null +++ b/.github/workflows/ghcr-main.yml @@ -0,0 +1,107 @@ +name: Build & Publish to DockerHub (from VERSION + Release) + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +permissions: + contents: write # create tag + release + pull-requests: read + +jobs: + docker: + runs-on: ubuntu-latest + + env: + PLATFORMS: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Read VERSION + id: ver + run: | + if [[ ! -f VERSION ]]; then echo "VERSION missing"; exit 1; fi + RAW=$(tr -d ' \n' < VERSION) + if [[ -z "$RAW" ]]; then echo "VERSION empty"; exit 1; fi + if [[ "$RAW" =~ ^v ]]; then TAG="$RAW"; NUM="${RAW#v}"; else TAG="v${RAW}"; NUM="$RAW"; fi + + # Get repository name for DockerHub + REPO_NAME=$(echo "${GITHUB_REPOSITORY}" | cut -d'/' -f2) + + echo "TAG=$TAG" >> $GITHUB_ENV + echo "NUM=$NUM" >> $GITHUB_ENV + echo "DOCKER_REPO=${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}" >> $GITHUB_ENV + echo "DOCKER_TAGS=${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}:${NUM},${{ secrets.DOCKER_HUB_USERNAME }}/${REPO_NAME}:latest" >> $GITHUB_ENV + echo "Resolved TAG=$TAG NUM=$NUM" + + - name: Create tag (if missing) + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: actions/github-script@v7 + with: + script: | + const tag = process.env.TAG; + const sha = context.sha; + const { owner, repo } = context.repo; + try { + await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); + core.info(`Tag ${tag} exists`); + } catch (e) { + if (e.status === 404) { + core.info(`Creating tag ${tag} -> ${sha}`); + await github.rest.git.createRef({ owner, repo, ref: `refs/tags/${tag}`, sha }); + } else { throw e; } + } + + - name: Update draft release notes + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + id: drafter + uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + publish: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish GitHub Release + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG }} + name: NFOGuard ${{ env.TAG }} + draft: false + prerelease: false + body: ${{ steps.drafter.outputs.body }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + if: ${{ env.PLATFORMS == 'linux/amd64,linux/arm64' }} + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DockerHub + if: ${{ github.event_name != 'pull_request' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Build & Push (main) + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: ${{ github.event_name != 'pull_request' }} + platforms: ${{ env.PLATFORMS }} + tags: ${{ env.DOCKER_TAGS }} + build-args: | + APP_VERSION=${{ env.NUM }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/notify-issues.yml b/.github/workflows/notify-issues.yml new file mode 100644 index 0000000..a762f25 --- /dev/null +++ b/.github/workflows/notify-issues.yml @@ -0,0 +1,66 @@ +name: Notify on new issues + +on: + issues: + types: [opened] + workflow_dispatch: {} + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Validate required secrets + run: | + if [[ -z "${{ secrets.GOTIFY_URL }}" ]]; then + echo "Error: GOTIFY_URL secret is not set" + exit 1 + fi + if [[ -z "${{ secrets.GOTIFY_TOKEN }}" ]]; then + echo "Error: GOTIFY_TOKEN secret is not set" + exit 1 + fi + echo "Secrets validation passed" + + - name: Build and send Gotify notification + env: + GOTIFY_URL: ${{ secrets.GOTIFY_URL }} + GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }} + run: | + set -euo pipefail + + # Remove trailing slash from URL if present + GOTIFY_URL="${GOTIFY_URL%/}" + + # Validate URL format + if [[ ! "$GOTIFY_URL" =~ ^https?:// ]]; then + echo "Error: GOTIFY_URL must start with http:// or https://" + exit 1 + fi + + # Build the notification payload + TITLE="New issue in ${{ github.repository }}" + MESSAGE="Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} + By: ${{ github.actor }} + ${{ github.event.issue.html_url }}" + + # Create JSON payload + PAYLOAD=$(jq -n \ + --arg title "$TITLE" \ + --arg message "$MESSAGE" \ + --argjson priority 5 \ + '{title: $title, message: $message, priority: $priority}') + + echo "Sending notification to Gotify..." + echo "URL: ${GOTIFY_URL}/message" + echo "Payload: $PAYLOAD" + + # Send the notification using header authentication + curl -X POST "${GOTIFY_URL}/message" \ + -H "Content-Type: application/json" \ + -H "X-Gotify-Key: ${GOTIFY_TOKEN}" \ + -d "$PAYLOAD" \ + --fail \ + --show-error \ + --silent + + echo "Notification sent successfully!" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..d51a8e4 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,21 @@ +name: Release Drafter +on: + push: + branches: [ main ] + pull_request: + types: [opened, reopened, synchronize, closed] + branches: [ main ] + +permissions: + contents: write + pull-requests: read + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..0798ec5 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,153 @@ +# NFOGuard Setup Guide + +## ๐Ÿ” Secure Configuration Setup + +NFOGuard now uses a **two-file configuration system** for better security and easier troubleshooting: + +- **`.env`** - Main configuration (safe to share for debugging) +- **`.env.secrets`** - Sensitive API keys and passwords (never commit to git) + +### Step 1: Copy Configuration Templates + +```bash +# Copy main configuration +cp .env.template .env + +# Copy secrets configuration +cp .env.secrets.template .env.secrets +``` + +### Step 2: Configure Main Settings + +Edit your `.env` file with your specific paths and preferences: + +```bash +# Media paths (adjust to your directory structure) +TV_PATHS=/media/TV/tv,/media/TV/tv6 +MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6 + +# Database connection details +RADARR_DB_HOST=radarr-postgres +RADARR_DB_PORT=5432 +RADARR_DB_NAME=radarr +RADARR_DB_USER=radarr + +# Processing preferences +PREFER_RELEASE_DATES_OVER_FILE_DATES=true +ALLOW_FILE_DATE_FALLBACK=false +RELEASE_DATE_PRIORITY=digital,physical,theatrical + +# TV webhook processing mode (v0.6.0+) +TV_WEBHOOK_PROCESSING_MODE=targeted +``` + +### Step 3: Configure Secrets + +Edit your `.env.secrets` file with your actual API keys and passwords: + +```bash +# Database password +RADARR_DB_PASSWORD=your_actual_radarr_password + +# TMDB API key (required for release date detection) +TMDB_API_KEY=your_actual_tmdb_api_key + +# Sonarr API key (REQUIRED for v0.6.0+ Enhanced TV NFO Generation) +SONARR_API_KEY=your_actual_sonarr_api_key + +# Optional API keys +RADARR_API_KEY=your_radarr_api_key +OMDB_API_KEY=your_omdb_api_key +``` + +### Step 4: Verify Configuration + +Test your setup: + +```bash +# Test database connections +curl -X POST "http://localhost:8080/test/bulk-update" + +# Test movie scanning +curl -X POST "http://localhost:8080/test/movie-scan" + +# Check system health +curl "http://localhost:8080/health" +``` + +## ๐Ÿ”’ Security Features + +### API Key Masking +All API keys and passwords are automatically masked in logs: +``` +[2025-09-09T12:34:56] INFO: TMDB API call with key=***masked*** +[2025-09-09T12:34:56] INFO: Database connection password=***masked*** +``` + +### Sensitive Data Separation +- **Main `.env`**: Paths, preferences, URLs (safe to share) +- **`.env.secrets`**: API keys, passwords (never commit to version control) +- **Automatic loading**: Both files loaded automatically at startup + +### Git Protection +The `.gitignore` file prevents accidental commits: +``` +.env +.env.secrets +.env.local +``` + +## ๐Ÿ›  Troubleshooting + +### "Environment files not loaded" Warning +Install python-dotenv: +```bash +pip install python-dotenv==1.0.0 +# or +docker-compose build # rebuilds with updated requirements.txt +``` + +### Sharing Configuration for Help +You can safely share your `.env` file for debugging since it contains no sensitive data. The `.env.secrets` file should never be shared. + +### Migration from Old Setup +If you have an existing `.env` with API keys: +1. Move all `*_API_KEY` and `*_PASSWORD` variables to `.env.secrets` +2. Remove sensitive data from `.env` +3. Restart NFOGuard + +## ๐ŸŽฏ Docker Compose Example + +```yaml +version: '3.8' +services: + nfoguard: + image: sbcrumb/nfoguard:latest + container_name: nfoguard + ports: + - "8080:8080" + volumes: + - /path/to/your/media:/media:rw + - ./data:/app/data + - ./.env:/app/.env:ro # Main configuration + - ./.env.secrets:/app/.env.secrets:ro # Secrets + environment: + - PORT=8080 + depends_on: + - radarr-postgres +``` + +## ๐Ÿ“‹ Configuration Reference + +### Main Configuration (.env) +- **Paths**: `TV_PATHS`, `MOVIE_PATHS`, `DB_PATH` +- **Processing**: `MOVIE_PRIORITY`, `RELEASE_DATE_PRIORITY` +- **Features**: `MANAGE_NFO`, `FIX_DIR_MTIMES`, `LOCK_METADATA` +- **URLs**: `RADARR_URL`, `SONARR_URL`, `JELLYSEERR_URL` + +### Secrets Configuration (.env.secrets) +- **Database**: `RADARR_DB_PASSWORD` +- **APIs**: `TMDB_API_KEY`, `OMDB_API_KEY`, `RADARR_API_KEY`, `SONARR_API_KEY` +- **Optional**: `JELLYSEERR_API_KEY` + +This setup makes NFOGuard more secure while keeping configuration manageable for troubleshooting and deployment. \ No newline at end of file