Efficiently apply a regex find/replace to the `name` field of multiple channels.
(self, request)
| 1415 | ) |
| 1416 | @action(detail=False, methods=["post"], url_path="edit/bulk-regex") |
| 1417 | def bulk_regex_rename(self, request): |
| 1418 | """ |
| 1419 | Efficiently apply a regex find/replace to the `name` field of multiple channels. |
| 1420 | """ |
| 1421 | import regex as re |
| 1422 | |
| 1423 | channel_ids = request.data.get("channel_ids", []) |
| 1424 | pattern = request.data.get("find", "") |
| 1425 | replace = request.data.get("replace", "") |
| 1426 | flags_str = request.data.get("flags", "") or "" |
| 1427 | |
| 1428 | if not isinstance(channel_ids, list) or len(channel_ids) == 0: |
| 1429 | return Response({"error": "channel_ids must be a non-empty list"}, status=status.HTTP_400_BAD_REQUEST) |
| 1430 | if not isinstance(pattern, str) or pattern.strip() == "": |
| 1431 | return Response({"error": "find (regex pattern) is required"}, status=status.HTTP_400_BAD_REQUEST) |
| 1432 | if not isinstance(replace, str): |
| 1433 | return Response({"error": "replace must be a string"}, status=status.HTTP_400_BAD_REQUEST) |
| 1434 | |
| 1435 | # Convert JS-style named groups to Python (?<name>...) -> (?P<name>...) |
| 1436 | try: |
| 1437 | converted_pattern = re.sub(r"\(\?<([^>]+)>", r"(?P<\1>", pattern) |
| 1438 | except Exception as e: |
| 1439 | return Response({"error": f"Failed to normalize pattern: {e}"}, status=status.HTTP_400_BAD_REQUEST) |
| 1440 | |
| 1441 | # Compile flags |
| 1442 | re_flags = 0 |
| 1443 | if "i" in flags_str: |
| 1444 | re_flags |= re.IGNORECASE |
| 1445 | # Note: 'g' (global) is the default behavior of re.sub; no action needed. |
| 1446 | |
| 1447 | # Translate common JS replacement tokens to Python |
| 1448 | def translate_js_replacement(rep: str) -> str: |
| 1449 | # $$ -> $ |
| 1450 | rep = rep.replace("$$", "$") |
| 1451 | # $& -> \g<0> |
| 1452 | rep = rep.replace("$&", r"\g<0>") |
| 1453 | # $<name> -> \g<name> |
| 1454 | rep = re.sub(r"\$<([A-Za-z_][A-Za-z0-9_]*)>", r"\\g<\1>", rep) |
| 1455 | # $1 -> \g<1>, $2 -> \g<2>, etc. |
| 1456 | rep = re.sub(r"\$(\d+)", r"\\g<\1>", rep) |
| 1457 | return rep |
| 1458 | |
| 1459 | try: |
| 1460 | replacement_py = translate_js_replacement(replace) |
| 1461 | compiled = re.compile(converted_pattern, flags=re_flags) |
| 1462 | except Exception as e: |
| 1463 | return Response({"error": f"Invalid regex pattern: {e}"}, status=status.HTTP_400_BAD_REQUEST) |
| 1464 | |
| 1465 | # Fetch channels in one query |
| 1466 | channels = list(Channel.objects.filter(id__in=channel_ids)) |
| 1467 | if not channels: |
| 1468 | return Response({"error": "No matching channels found for provided IDs"}, status=status.HTTP_404_NOT_FOUND) |
| 1469 | |
| 1470 | changed = [] |
| 1471 | for ch in channels: |
| 1472 | current = ch.name or "" |
| 1473 | try: |
| 1474 | new_name = compiled.sub(replacement_py, current) |