Compare commits
8 Commits
release/1.3.1
...
v1.3.1a
| Author | SHA1 | Date | |
|---|---|---|---|
| ae9e877445 | |||
| 853b205c46 | |||
| 8c4cc20551 | |||
| da77f083fe | |||
| 71feaf0175 | |||
| 65b9f0f395 | |||
| b41f943407 | |||
| 9debd77392 |
@@ -7,16 +7,24 @@ on:
|
|||||||
- "package.json"
|
- "package.json"
|
||||||
- "package-lock.json"
|
- "package-lock.json"
|
||||||
- ".gitea/workflows/licence-check.yml"
|
- ".gitea/workflows/licence-check.yml"
|
||||||
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.jsx"
|
||||||
|
- "**/*.tsx"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["**", "!main", "!release/**"]
|
branches: ["**", "!main", "!release/**"]
|
||||||
paths:
|
paths:
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "package-lock.json"
|
- "package-lock.json"
|
||||||
- ".gitea/workflows/licence-check.yml"
|
- ".gitea/workflows/licence-check.yml"
|
||||||
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.jsx"
|
||||||
|
- "**/*.tsx"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
licence-check:
|
licence-check:
|
||||||
name: Dependency licence compatibility
|
name: Licence compatibility and copyright header verification
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
@@ -36,3 +44,40 @@ jobs:
|
|||||||
--onlyAllow "MIT;ISC;MIT-0;BSD-2-Clause;BSD-3-Clause;Apache-2.0;CC0-1.0;BlueOak-1.0.0" \
|
--onlyAllow "MIT;ISC;MIT-0;BSD-2-Clause;BSD-3-Clause;Apache-2.0;CC0-1.0;BlueOak-1.0.0" \
|
||||||
--excludePrivatePackages \
|
--excludePrivatePackages \
|
||||||
&& echo "All production dependency licences are compatible with MIT."
|
&& echo "All production dependency licences are compatible with MIT."
|
||||||
|
|
||||||
|
- name: Check copyright headers in source files
|
||||||
|
run: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Find all source files, excluding build artifacts and node_modules
|
||||||
|
SOURCE_FILES=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) \
|
||||||
|
! -path "./node_modules/*" \
|
||||||
|
! -path "./.git/*" \
|
||||||
|
! -path "./dist/*" \
|
||||||
|
! -path "./build/*" \
|
||||||
|
! -path "./.gitea/*")
|
||||||
|
|
||||||
|
MISSING_HEADER=0
|
||||||
|
|
||||||
|
# Check each file for MIT-compliant copyright header
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if [ -z "$file" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if file starts with a copyright header containing: Copyright, year (4 digits), name, and MIT License
|
||||||
|
if ! head -n 5 "$file" | grep -qiE "Copyright.*[0-9]{4}.*MIT"; then
|
||||||
|
echo "❌ Missing MIT-compliant copyright header in: $file"
|
||||||
|
echo " Required format: // Copyright (c) YYYY Name. MIT License."
|
||||||
|
MISSING_HEADER=$((MISSING_HEADER + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$SOURCE_FILES"
|
||||||
|
|
||||||
|
if [ $MISSING_HEADER -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Found $MISSING_HEADER file(s) with missing or non-compliant copyright headers."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All source files have MIT-compliant copyright headers."
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ sofarr/
|
|||||||
│ └── integration/ # Supertest integration tests (nock for external HTTP)
|
│ └── integration/ # Supertest integration tests (nock for external HTTP)
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── ARCHITECTURE.md # This document
|
│ ├── ARCHITECTURE.md # This document
|
||||||
│ └── diagrams/ # PlantUML source files
|
|
||||||
├── .gitea/workflows/
|
├── .gitea/workflows/
|
||||||
│ ├── ci.yml # Security audit + test/coverage CI jobs
|
│ ├── ci.yml # Security audit + test/coverage CI jobs
|
||||||
│ ├── build-image.yml # Docker image build and push
|
│ ├── build-image.yml # Docker image build and push
|
||||||
@@ -901,21 +900,23 @@ volumes:
|
|||||||
|
|
||||||
### CI / CD
|
### CI / CD
|
||||||
|
|
||||||
The `.gitea/workflows/` directory contains three pipeline definitions:
|
The `.gitea/workflows/` directory contains five pipeline definitions:
|
||||||
|
|
||||||
| File | Trigger | Purpose |
|
| File | Trigger | Purpose |
|
||||||
|------|---------|--------|
|
|------|---------|--------|
|
||||||
| `ci.yml` | Every push / PR | Security audit (`npm audit --audit-level=high`) + tests with V8 coverage |
|
| `ci.yml` | Every push / PR (all branches) | Security audit (`npm audit --audit-level=high`) + tests with V8 coverage |
|
||||||
| `build-image.yml` | Push to `main` / `develop` | Build and push Docker image to `docker.i3omb.com` |
|
| `build-image.yml` | Push to `release/**` or `develop` | Build and push Docker image to `reg.i3omb.com`. `release/**` pushes versioned + `latest` tags; `develop` pushes a `:develop` tag. |
|
||||||
| `create-release.yml` | Tag push (`v*`) | Create a Gitea release |
|
| `create-release.yml` | Tag push (`v*`) | Generate release notes from git log and create a Gitea release |
|
||||||
|
| `docs-check.yml` | Push / PR touching `**.md` (non-main / non-release branches) | Markdown lint + Mermaid diagram parse validation |
|
||||||
|
| `licence-check.yml` | Push / PR touching `package.json` or `package-lock.json` | Verify all production dependency licences are compatible with MIT |
|
||||||
|
|
||||||
> **Diagrams** are written in Mermaid and render natively in Gitea — no CI workflow required. See [Section 13](#13-diagrams).
|
> **Diagrams** are written in Mermaid and render natively in Gitea — no separate diagram files or CI render step required. See [Section 13](#13-diagrams).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Diagrams
|
## 13. Diagrams
|
||||||
|
|
||||||
All diagrams are written in [Mermaid](https://mermaid.js.org/) and render natively in Gitea and GitHub markdown.
|
All diagrams are written in [Mermaid](https://mermaid.js.org/) and render natively in Gitea and GitHub markdown. No external tooling or PNG exports are required — the source is the diagram.
|
||||||
|
|
||||||
### 13.1 Component Diagram
|
### 13.1 Component Diagram
|
||||||
|
|
||||||
@@ -1302,6 +1303,13 @@ classDiagram
|
|||||||
+availability string
|
+availability string
|
||||||
+hash string
|
+hash string
|
||||||
+completedAt string
|
+completedAt string
|
||||||
|
+canBlocklist boolean
|
||||||
|
+addedOn number
|
||||||
|
+arrQueueId number
|
||||||
|
+arrType string
|
||||||
|
+arrInstanceUrl string
|
||||||
|
+arrContentId number
|
||||||
|
+arrContentType string
|
||||||
}
|
}
|
||||||
class TagBadge {
|
class TagBadge {
|
||||||
+label string
|
+label string
|
||||||
@@ -1346,6 +1354,7 @@ classDiagram
|
|||||||
+num_seeds number
|
+num_seeds number
|
||||||
+num_leechs number
|
+num_leechs number
|
||||||
+availability number
|
+availability number
|
||||||
|
+added_on number
|
||||||
}
|
}
|
||||||
class SonarrQueueRecord {
|
class SonarrQueueRecord {
|
||||||
+seriesId number
|
+seriesId number
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 139 KiB |
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let downloads = [];
|
let downloads = [];
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Express application factory — imported by both server/index.js (production)
|
* Express application factory — imported by both server/index.js (production)
|
||||||
* and the test suite. Keeping app creation separate from app.listen() means
|
* and the test suite. Keeping app creation separate from app.listen() means
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Gordon Bolton. MIT License.
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
function requireAuth(req, res, next) {
|
function requireAuth(req, res, next) {
|
||||||
const signed = !!process.env.COOKIE_SECRET;
|
const signed = !!process.env.COOKIE_SECRET;
|
||||||
const raw = signed ? req.signedCookies.emby_user : req.cookies.emby_user;
|
const raw = signed ? req.signedCookies.emby_user : req.cookies.emby_user;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* CSRF protection using the double-submit cookie pattern.
|
* CSRF protection using the double-submit cookie pattern.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const requireAuth = require('../middleware/requireAuth');
|
const requireAuth = require('../middleware/requireAuth');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const { logToFile } = require('./logger');
|
const { logToFile } = require('./logger');
|
||||||
|
|
||||||
class MemoryCache {
|
class MemoryCache {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Gordon Bolton. MIT License.
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const { logToFile } = require('./logger');
|
const { logToFile } = require('./logger');
|
||||||
|
|
||||||
// Validate that a configured service URL is well-formed and uses http(s).
|
// Validate that a configured service URL is well-formed and uses http(s).
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const cache = require('./cache');
|
const cache = require('./cache');
|
||||||
const { getSonarrInstances, getRadarrInstances } = require('./config');
|
const { getSonarrInstances, getRadarrInstances } = require('./config');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Gordon Bolton. MIT License.
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
//
|
//
|
||||||
// Docker secrets support: if an environment variable named FOO_FILE is set,
|
// Docker secrets support: if an environment variable named FOO_FILE is set,
|
||||||
// read its contents from the file at that path and expose it as FOO.
|
// read its contents from the file at that path and expose it as FOO.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Gordon Bolton. MIT License.
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const cache = require('./cache');
|
const cache = require('./cache');
|
||||||
const { getTorrents } = require('./qbittorrent');
|
const { getTorrents } = require('./qbittorrent');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { logToFile } = require('./logger');
|
const { logToFile } = require('./logger');
|
||||||
const { getQbittorrentInstances } = require('./config');
|
const { getQbittorrentInstances } = require('./config');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Gordon Bolton. MIT License.
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
// Query-param secrets (SABnzbd apikey, generic token/password params)
|
// Query-param secrets (SABnzbd apikey, generic token/password params)
|
||||||
const QUERY_SECRET_PATTERN = /([?&](?:apikey|token|password|api_key|key|secret)=)[^&\s#]*/gi;
|
const QUERY_SECRET_PATTERN = /([?&](?:apikey|token|password|api_key|key|secret)=)[^&\s#]*/gi;
|
||||||
// HTTP auth header values (X-Api-Key, X-MediaBrowser-Token, Authorization, X-Emby-Authorization)
|
// HTTP auth header values (X-Api-Key, X-MediaBrowser-Token, Authorization, X-Emby-Authorization)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Persistent token store backed by a JSON file.
|
* Persistent token store backed by a JSON file.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Integration tests for authentication routes.
|
* Integration tests for authentication routes.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Integration tests for health and readiness endpoints.
|
* Integration tests for health and readiness endpoints.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Integration tests for GET /api/history/recent
|
* Integration tests for GET /api/history/recent
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
import { vi, beforeEach, afterEach } from 'vitest';
|
import { vi, beforeEach, afterEach } from 'vitest';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/utils/config.js
|
* Tests for server/utils/config.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Unit tests for server/utils/historyFetcher.js
|
* Unit tests for server/utils/historyFetcher.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/utils/qbittorrent.js pure utility functions.
|
* Tests for server/utils/qbittorrent.js pure utility functions.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/middleware/requireAuth.js
|
* Tests for server/middleware/requireAuth.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/utils/sanitizeError.js
|
* Tests for server/utils/sanitizeError.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/utils/tokenStore.js
|
* Tests for server/utils/tokenStore.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
/**
|
/**
|
||||||
* Tests for server/middleware/verifyCsrf.js
|
* Tests for server/middleware/verifyCsrf.js
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||