+4
moderation/src/db.rs
+4
moderation/src/db.rs
···
95
95
FingerprintNoise,
96
96
/// Legal cover version or remix
97
97
CoverVersion,
98
+
/// Content was deleted from plyr.fm
99
+
ContentDeleted,
98
100
/// Other reason (see resolution_notes)
99
101
Other,
100
102
}
···
107
109
Self::Licensed => "licensed",
108
110
Self::FingerprintNoise => "fingerprint noise",
109
111
Self::CoverVersion => "cover/remix",
112
+
Self::ContentDeleted => "content deleted",
110
113
Self::Other => "other",
111
114
}
112
115
}
···
118
121
"licensed" => Some(Self::Licensed),
119
122
"fingerprint_noise" => Some(Self::FingerprintNoise),
120
123
"cover_version" => Some(Self::CoverVersion),
124
+
"content_deleted" => Some(Self::ContentDeleted),
121
125
"other" => Some(Self::Other),
122
126
_ => None,
123
127
}
+66
-1
scripts/moderation_loop.py
+66
-1
scripts/moderation_loop.py
···
117
117
118
118
119
119
@dataclass
120
+
class PlyrClient:
121
+
"""client for checking track existence in plyr.fm."""
122
+
123
+
env: str = "prod"
124
+
_client: httpx.AsyncClient = field(init=False, repr=False)
125
+
126
+
def __post_init__(self) -> None:
127
+
base_url = {
128
+
"prod": "https://api.plyr.fm",
129
+
"staging": "https://api-stg.plyr.fm",
130
+
"dev": "http://localhost:8001",
131
+
}.get(self.env, "https://api.plyr.fm")
132
+
self._client = httpx.AsyncClient(base_url=base_url, timeout=10.0)
133
+
134
+
async def close(self) -> None:
135
+
await self._client.aclose()
136
+
137
+
async def track_exists(self, track_id: int) -> bool:
138
+
"""check if a track exists (returns False if 404)."""
139
+
try:
140
+
r = await self._client.get(f"/tracks/{track_id}")
141
+
return r.status_code == 200
142
+
except Exception:
143
+
return True # assume exists on error (don't accidentally delete labels)
144
+
145
+
146
+
@dataclass
120
147
class ModClient:
121
148
base_url: str
122
149
auth_token: str
···
203
230
204
231
dm = DMClient(settings.bot_handle, settings.bot_password, settings.recipient_handle)
205
232
mod = ModClient(settings.moderation_service_url, settings.moderation_auth_token)
233
+
plyr = PlyrClient(env=env)
206
234
207
235
try:
208
236
await dm.setup()
···
215
243
216
244
console.print(f"[bold]{len(pending)} pending flags[/bold]")
217
245
218
-
# analyze flags
246
+
# check for deleted tracks and auto-resolve them
247
+
console.print("[dim]checking for deleted tracks...[/dim]")
248
+
active_flags = []
249
+
deleted_count = 0
250
+
for flag in pending:
251
+
track_id = flag.get("context", {}).get("track_id")
252
+
if track_id and not await plyr.track_exists(track_id):
253
+
# track was deleted - resolve the flag
254
+
if not dry_run:
255
+
try:
256
+
await mod.resolve(
257
+
flag["uri"], "content_deleted", "track no longer exists"
258
+
)
259
+
console.print(
260
+
f" [yellow]⌫[/yellow] deleted: {flag['uri'][-40:]}"
261
+
)
262
+
deleted_count += 1
263
+
except Exception as e:
264
+
console.print(f" [red]✗[/red] {e}")
265
+
active_flags.append(flag)
266
+
else:
267
+
console.print(
268
+
f" [yellow]would resolve deleted:[/yellow] {flag['uri'][-40:]}"
269
+
)
270
+
deleted_count += 1
271
+
else:
272
+
active_flags.append(flag)
273
+
274
+
if deleted_count > 0:
275
+
console.print(f"[yellow]{deleted_count} deleted tracks resolved[/yellow]")
276
+
277
+
pending = active_flags
278
+
if not pending:
279
+
console.print("[green]all flags were for deleted tracks[/green]")
280
+
return
281
+
282
+
# analyze remaining flags
219
283
if limit:
220
284
pending = pending[:limit]
221
285
···
268
332
269
333
finally:
270
334
await mod.close()
335
+
await plyr.close()
271
336
272
337
273
338
def main() -> None: