diff --git a/.gitignore b/.gitignore index f6d4ec6..817a8e6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ vendor/ coverage/ .cache/ tmp/ + +# ── GSD baseline (auto-generated) ── +.gsd diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fbceded..7723403 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -65,11 +65,11 @@ class SessionConfig(BaseModel): class PurgeConfig(BaseModel): """Automatic purge / cleanup settings.""" - enabled: bool = False - max_age_hours: int = 168 # 7 days - cron: str = "0 3 * * *" # 3 AM daily + enabled: bool = True + max_age_minutes: int = 1440 # 24 hours + cron: str = "* * * * *" # every minute privacy_mode: bool = False - privacy_retention_hours: int = 24 # default when privacy mode enabled + privacy_retention_minutes: int = 1440 # default when privacy mode enabled class UIConfig(BaseModel): diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index c4377b3..07575bd 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -288,13 +288,13 @@ async def get_settings( "default_video_format": getattr(request.app.state, "_default_video_format", "auto"), "default_audio_format": getattr(request.app.state, "_default_audio_format", "auto"), "privacy_mode": config.purge.privacy_mode, - "privacy_retention_hours": config.purge.privacy_retention_hours, + "privacy_retention_minutes": config.purge.privacy_retention_minutes, "max_concurrent": config.downloads.max_concurrent, "session_mode": config.session.mode, "session_timeout_hours": config.session.timeout_hours, "admin_username": config.admin.username, "purge_enabled": config.purge.enabled, - "purge_max_age_hours": config.purge.max_age_hours, + "purge_max_age_minutes": config.purge.max_age_minutes, } @@ -310,13 +310,13 @@ async def update_settings( - default_video_format: str (auto, mp4, webm) - default_audio_format: str (auto, mp3, m4a, flac, wav, opus) - privacy_mode: bool - - privacy_retention_hours: int (1-8760) + - privacy_retention_minutes: int (1-525600) - max_concurrent: int (1-10) - session_mode: str (isolated, shared, open) - session_timeout_hours: int (1-8760) - admin_username: str - purge_enabled: bool - - purge_max_age_hours: int (1-87600) + - purge_max_age_minutes: int (1-5256000) """ from app.services.settings import save_settings @@ -364,12 +364,12 @@ async def update_settings( if val and not getattr(request.app.state, "scheduler", None): _start_purge_scheduler(request.app.state, config, db) - if "privacy_retention_hours" in body: - val = body["privacy_retention_hours"] - if isinstance(val, (int, float)) and 1 <= val <= 8760: - config.purge.privacy_retention_hours = int(val) - to_persist["privacy_retention_hours"] = int(val) - updated.append("privacy_retention_hours") + if "privacy_retention_minutes" in body: + val = body["privacy_retention_minutes"] + if isinstance(val, (int, float)) and 1 <= val <= 525600: + config.purge.privacy_retention_minutes = int(val) + to_persist["privacy_retention_minutes"] = int(val) + updated.append("privacy_retention_minutes") if "max_concurrent" in body: val = body["max_concurrent"] @@ -411,12 +411,12 @@ async def update_settings( if val and not getattr(request.app.state, "scheduler", None): _start_purge_scheduler(request.app.state, config, db) - if "purge_max_age_hours" in body: - val = body["purge_max_age_hours"] - if isinstance(val, int) and 1 <= val <= 87600: - config.purge.max_age_hours = val - to_persist["purge_max_age_hours"] = val - updated.append("purge_max_age_hours") + if "purge_max_age_minutes" in body: + val = body["purge_max_age_minutes"] + if isinstance(val, int) and 1 <= val <= 5256000: + config.purge.max_age_minutes = val + to_persist["purge_max_age_minutes"] = val + updated.append("purge_max_age_minutes") # --- Persist to DB --- if to_persist: @@ -485,7 +485,7 @@ def _start_purge_scheduler(state, config, db) -> None: scheduler = AsyncIOScheduler() scheduler.add_job( run_purge, - CronTrigger(minute="*/30"), + CronTrigger(minute="*"), args=[db, config], id="purge_job", name="Scheduled purge", diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index aa75a9c..4228289 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -28,7 +28,7 @@ async def public_config(request: Request) -> dict: "default_video_format": getattr(request.app.state, "_default_video_format", "auto"), "default_audio_format": getattr(request.app.state, "_default_audio_format", "auto"), "privacy_mode": config.purge.privacy_mode, - "privacy_retention_hours": config.purge.privacy_retention_hours, + "privacy_retention_minutes": config.purge.privacy_retention_minutes, "admin_enabled": config.admin.enabled, "admin_setup_complete": bool(config.admin.password_hash), } diff --git a/backend/app/services/purge.py b/backend/app/services/purge.py index 5515566..18d29c1 100644 --- a/backend/app/services/purge.py +++ b/backend/app/services/purge.py @@ -42,12 +42,12 @@ async def run_purge( privacy_on = overrides.get("privacy_mode", config.purge.privacy_mode) if privacy_on: retention = overrides.get( - "privacy_retention_hours", config.purge.privacy_retention_hours + "privacy_retention_minutes", config.purge.privacy_retention_minutes ) else: - retention = config.purge.max_age_hours - cutoff = (datetime.now(timezone.utc) - timedelta(hours=retention)).isoformat() - logger.info("Purge starting: retention=%dh (privacy=%s), cutoff=%s", retention, privacy_on, cutoff) + retention = config.purge.max_age_minutes + cutoff = (datetime.now(timezone.utc) - timedelta(minutes=retention)).isoformat() + logger.info("Purge starting: retention=%dm (privacy=%s), cutoff=%s", retention, privacy_on, cutoff) output_dir = Path(config.downloads.output_dir) diff --git a/backend/app/services/settings.py b/backend/app/services/settings.py index 01882b1..76968f5 100644 --- a/backend/app/services/settings.py +++ b/backend/app/services/settings.py @@ -26,14 +26,14 @@ ADMIN_WRITABLE_KEYS = { "default_video_format", "default_audio_format", "privacy_mode", - "privacy_retention_hours", + "privacy_retention_minutes", "max_concurrent", "session_mode", "session_timeout_hours", "admin_username", "admin_password_hash", "purge_enabled", - "purge_max_age_hours", + "purge_max_age_minutes", } @@ -104,12 +104,12 @@ def apply_persisted_to_config(config, settings: dict) -> None: config.admin.password_hash = settings["admin_password_hash"] if "purge_enabled" in settings: config.purge.enabled = settings["purge_enabled"] - if "purge_max_age_hours" in settings: - config.purge.max_age_hours = settings["purge_max_age_hours"] + if "purge_max_age_minutes" in settings: + config.purge.max_age_minutes = settings["purge_max_age_minutes"] if "privacy_mode" in settings: config.purge.privacy_mode = settings["privacy_mode"] - if "privacy_retention_hours" in settings: - config.purge.privacy_retention_hours = settings["privacy_retention_hours"] + if "privacy_retention_minutes" in settings: + config.purge.privacy_retention_minutes = settings["privacy_retention_minutes"] logger.info("Applied %d persisted settings to config", len(settings)) @@ -123,7 +123,7 @@ def _deserialize(key: str, raw: str) -> object: # Type coercion for known keys bool_keys = {"privacy_mode", "purge_enabled"} - int_keys = {"max_concurrent", "session_timeout_hours", "purge_max_age_hours", "privacy_retention_hours"} + int_keys = {"max_concurrent", "session_timeout_hours", "purge_max_age_minutes", "privacy_retention_minutes"} if key in bool_keys: return bool(value) diff --git a/backend/start.py b/backend/start.py new file mode 100644 index 0000000..1ca05d3 --- /dev/null +++ b/backend/start.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +"""media.rip() entrypoint — reads config and launches uvicorn with the correct port.""" + +import os +import sys + + +def main(): + # Port from env var (MEDIARIP__SERVER__PORT) or default 8000 + port = os.environ.get("MEDIARIP__SERVER__PORT", "8000") + host = os.environ.get("MEDIARIP__SERVER__HOST", "0.0.0.0") + + sys.exit( + os.execvp( + "python", + [ + "python", + "-m", + "uvicorn", + "app.main:app", + "--host", + host, + "--port", + port, + ], + ) + ) + + +if __name__ == "__main__": + main() diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index c61dc65..95ed27a 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -173,7 +173,7 @@ class TestPublicConfig: data = resp.json() assert data["session_mode"] == "isolated" assert data["default_theme"] == "dark" - assert data["purge_enabled"] is False + assert data["purge_enabled"] is True assert data["max_concurrent_downloads"] == 3 diff --git a/backend/tests/test_purge.py b/backend/tests/test_purge.py index 5690ebf..d651f01 100644 --- a/backend/tests/test_purge.py +++ b/backend/tests/test_purge.py @@ -42,7 +42,7 @@ class TestPurge: async def test_purge_deletes_old_completed_jobs(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 24}, + purge={"max_age_minutes": 1440}, ) sid = str(uuid.uuid4()) @@ -57,7 +57,7 @@ class TestPurge: async def test_purge_skips_recent_completed(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 24}, + purge={"max_age_minutes": 1440}, ) sid = str(uuid.uuid4()) @@ -72,7 +72,7 @@ class TestPurge: async def test_purge_skips_active_jobs(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 0}, # purge everything terminal + purge={"max_age_minutes": 0}, # purge everything terminal ) sid = str(uuid.uuid4()) @@ -88,7 +88,7 @@ class TestPurge: async def test_purge_deletes_files(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 0}, + purge={"max_age_minutes": 0}, ) sid = str(uuid.uuid4()) @@ -107,7 +107,7 @@ class TestPurge: async def test_purge_handles_missing_files(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 0}, + purge={"max_age_minutes": 0}, ) sid = str(uuid.uuid4()) @@ -123,7 +123,7 @@ class TestPurge: async def test_purge_mixed_statuses(self, db, tmp_path): config = AppConfig( downloads={"output_dir": str(tmp_path)}, - purge={"max_age_hours": 0}, + purge={"max_age_minutes": 0}, ) sid = str(uuid.uuid4()) diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 5c2a83c..f813f9b 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -76,7 +76,7 @@ export interface PublicConfig { default_video_format: string default_audio_format: string privacy_mode: boolean - privacy_retention_hours: number + privacy_retention_minutes: number admin_enabled: boolean admin_setup_complete: boolean } diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index d3a74ce..67cc965 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -23,7 +23,7 @@ const defaultVideoFormat = ref('auto') const defaultAudioFormat = ref('auto') const settingsSaved = ref(false) const privacyMode = ref(false) -const privacyRetentionHours = ref(24) +const privacyRetentionMinutes = ref(1440) const purgeConfirming = ref(false) let purgeConfirmTimer: ReturnType | null = null @@ -32,8 +32,8 @@ const maxConcurrent = ref(3) const sessionMode = ref('isolated') const sessionTimeoutHours = ref(72) const adminUsername = ref('admin') -const purgeEnabled = ref(false) -const purgeMaxAgeHours = ref(168) +const purgeEnabled = ref(true) +const purgeMaxAgeMinutes = ref(1440) // Change password state const currentPassword = ref('') @@ -73,13 +73,13 @@ async function switchTab(tab: typeof activeTab.value) { defaultVideoFormat.value = data.default_video_format || 'auto' defaultAudioFormat.value = data.default_audio_format || 'auto' privacyMode.value = data.privacy_mode ?? false - privacyRetentionHours.value = data.privacy_retention_hours ?? 24 + privacyRetentionMinutes.value = data.privacy_retention_minutes ?? 1440 maxConcurrent.value = data.max_concurrent ?? 3 sessionMode.value = data.session_mode ?? 'isolated' sessionTimeoutHours.value = data.session_timeout_hours ?? 72 adminUsername.value = data.admin_username ?? 'admin' purgeEnabled.value = data.purge_enabled ?? false - purgeMaxAgeHours.value = data.purge_max_age_hours ?? 168 + purgeMaxAgeMinutes.value = data.purge_max_age_minutes ?? 1440 } } catch { // Keep current values @@ -94,13 +94,13 @@ async function saveAllSettings() { default_video_format: defaultVideoFormat.value, default_audio_format: defaultAudioFormat.value, privacy_mode: privacyMode.value, - privacy_retention_hours: privacyRetentionHours.value, + privacy_retention_minutes: privacyRetentionMinutes.value, max_concurrent: maxConcurrent.value, session_mode: sessionMode.value, session_timeout_hours: sessionTimeoutHours.value, admin_username: adminUsername.value, purge_enabled: purgeEnabled.value, - purge_max_age_hours: purgeMaxAgeHours.value, + purge_max_age_minutes: purgeMaxAgeMinutes.value, }) if (ok) { await configStore.loadConfig() @@ -384,15 +384,15 @@ function formatFilesize(bytes: number | null): string {
- hours + minutes

- Data older than this is automatically purged (default: 24 hours). + Data older than this is automatically purged (default: 1440 min / 24 hours).

@@ -464,12 +464,12 @@ function formatFilesize(bytes: number | null): string {
- hours + minutes
diff --git a/frontend/src/tests/stores/config.test.ts b/frontend/src/tests/stores/config.test.ts index 4f47c74..e69bd3b 100644 --- a/frontend/src/tests/stores/config.test.ts +++ b/frontend/src/tests/stores/config.test.ts @@ -34,7 +34,7 @@ describe('config store', () => { default_video_format: 'auto', default_audio_format: 'auto', privacy_mode: false, - privacy_retention_hours: 24, + privacy_retention_minutes: 1440, admin_enabled: true, admin_setup_complete: false, }