Reconcile verified account IDs and make language repairs observable
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Audited operator repair. Preview by default; --apply requires explicit authorization."""
|
||||
import argparse
|
||||
import asyncio
|
||||
from contextlib import closing
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
from app import db
|
||||
from app.services import duplicate_accounts as duplicates
|
||||
from app.services import identity_review as review
|
||||
|
||||
|
||||
async def reconcile(apply, output):
|
||||
os.umask(0o077)
|
||||
output.mkdir(parents=True, exist_ok=False, mode=0o700)
|
||||
if apply:
|
||||
with closing(db._connect()) as source, closing(sqlite3.connect(output / 'before.sqlite')) as target:
|
||||
source.backup(target)
|
||||
changes, blocked, seen = [], [], set()
|
||||
actor = {'username': 'maintenance:authorized-identity-reconciliation'}
|
||||
report, local, runtime = await review.review_identities()
|
||||
initial = report['counts']
|
||||
while True:
|
||||
target = next((row for row in report['rows'] if row['candidate_jellyfin_id']
|
||||
and row['candidate_jellyfin_id'] not in seen
|
||||
and len(duplicates.identity_group(report, row)) > 1), None)
|
||||
if not target:
|
||||
break
|
||||
identity = target['candidate_jellyfin_id']
|
||||
seen.add(identity)
|
||||
ids = [row['user']['id'] for row in duplicates.identity_group(report, target)]
|
||||
with closing(db._connect()) as conn:
|
||||
state = duplicates.account_state(conn, ids)
|
||||
preview = duplicates.build_preview(report, local, runtime, state, target['user']['id'])
|
||||
if preview['can_confirm']:
|
||||
result = duplicates.consolidate(preview, report, local, runtime, state, actor) if apply else {
|
||||
'kept_user_id': preview['keep_id'], 'consolidated': len(ids) - 1}
|
||||
changes.append(result)
|
||||
if apply:
|
||||
report, local, runtime = await review.review_identities()
|
||||
else:
|
||||
blocked.append({'ids': ids, 'jellyfin_id': identity, 'issues': preview['issues']})
|
||||
(output / 'progress.json').write_text(json.dumps({'changes': changes, 'blocked': blocked}))
|
||||
if len(seen) % 20 == 0:
|
||||
print('Reviewed groups:', len(seen), 'consolidated rows:', sum(r['consolidated'] for r in changes), flush=True)
|
||||
if apply:
|
||||
ready = [row['user']['id'] for row in report['rows'] if row['can_confirm']
|
||||
and row['basis'] in {'confirmed_id', 'stored_jellyfin_id', 'stored_seerr_id'}]
|
||||
if ready:
|
||||
review.save_confirmations(report, local, runtime, ready, actor)
|
||||
report, _, _ = await review.review_identities()
|
||||
summary = {'applied': apply, 'before': initial, 'after': report['counts'],
|
||||
'consolidated_rows': sum(r['consolidated'] for r in changes), 'groups': len(changes), 'blocked': blocked}
|
||||
(output / 'result.json').write_text(json.dumps(summary, indent=2))
|
||||
print(json.dumps(summary), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--apply', action='store_true')
|
||||
parser.add_argument('--output', type=Path, required=True, help='New private backup/report directory')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(reconcile(args.apply, args.output))
|
||||
Reference in New Issue
Block a user