mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-02 18:43:59 -06:00
Purge intervals: hours→minutes, default ON at 1440min (24h)
- PurgeConfig: max_age_hours→max_age_minutes (default 1440) - PurgeConfig: privacy_retention_hours→privacy_retention_minutes (default 1440) - PurgeConfig: enabled default False→True - PurgeConfig: cron default every minute (was daily 3am) - Purge scheduler runs every minute for minute-granularity testing - All API fields renamed: purge_max_age_minutes, privacy_retention_minutes - Frontend admin panel inputs show minutes with updated labels - Updated test assertions for new defaults
This commit is contained in:
parent
0a67cb45bc
commit
43ddf43951
12 changed files with 90 additions and 56 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -38,3 +38,6 @@ vendor/
|
|||
coverage/
|
||||
.cache/
|
||||
tmp/
|
||||
|
||||
# ── GSD baseline (auto-generated) ──
|
||||
.gsd
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
31
backend/start.py
Normal file
31
backend/start.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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 {
|
|||
<div class="retention-input-row">
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="privacyRetentionHours"
|
||||
v-model.number="privacyRetentionMinutes"
|
||||
min="1"
|
||||
max="8760"
|
||||
max="525600"
|
||||
class="settings-input retention-input"
|
||||
/>
|
||||
<span class="retention-unit">hours</span>
|
||||
<span class="retention-unit">minutes</span>
|
||||
</div>
|
||||
<p class="field-hint">
|
||||
Data older than this is automatically purged (default: 24 hours).
|
||||
Data older than this is automatically purged (default: 1440 min / 24 hours).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -464,12 +464,12 @@ function formatFilesize(bytes: number | null): string {
|
|||
<div class="retention-input-row">
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="purgeMaxAgeHours"
|
||||
v-model.number="purgeMaxAgeMinutes"
|
||||
min="1"
|
||||
max="87600"
|
||||
max="5256000"
|
||||
class="settings-input retention-input"
|
||||
/>
|
||||
<span class="retention-unit">hours</span>
|
||||
<span class="retention-unit">minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue