refactor(runner): 移除 spider DB 歌手关联校验,简化 records2 初始化逻辑
Showing
2 changed files
with
60 additions
and
75 deletions
| ... | @@ -1409,54 +1409,15 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None | ... | @@ -1409,54 +1409,15 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None |
| 1409 | log.info("Initialized yinyan_song_records candidates=%d, skipped_batches=%d", total, skipped_batches) | 1409 | log.info("Initialized yinyan_song_records candidates=%d, skipped_batches=%d", total, skipped_batches) |
| 1410 | 1410 | ||
| 1411 | 1411 | ||
| 1412 | def _row_singer_key(row: dict) -> str | int: | ||
| 1413 | """返回一条 relation 用于匹配歌手的 key:QQ 用 mid(str),其他平台用 int id。""" | ||
| 1414 | if row['platform'] == PLATFORM_QQ: | ||
| 1415 | return row.get('platform_unique_key', '') | ||
| 1416 | return int(row.get('platform_unique_key', 0)) | ||
| 1417 | |||
| 1418 | |||
| 1419 | def _fetch_valid_singer_keys(spider_conn, rows: list[dict]) -> set: | ||
| 1420 | """查询 spider DB,返回有有效歌手关联的 platform_unique_key 集合。""" | ||
| 1421 | qq_mids = [r.get('platform_unique_key') for r in rows if r['platform'] == PLATFORM_QQ and r.get('platform_unique_key')] | ||
| 1422 | kugou_ids = [int(r['platform_unique_key']) for r in rows if r['platform'] == PLATFORM_KUGOU and r.get('platform_unique_key')] | ||
| 1423 | netease_ids = [int(r['platform_unique_key']) for r in rows if r['platform'] == PLATFORM_NETEASE and r.get('platform_unique_key')] | ||
| 1424 | |||
| 1425 | valid: set = set() | ||
| 1426 | |||
| 1427 | if qq_mids: | ||
| 1428 | songs = fetch_qq_songs(spider_conn, qq_mids) | ||
| 1429 | db_ids = [v['id'] for v in songs.values()] | ||
| 1430 | singers = fetch_qq_singers(spider_conn, db_ids) if db_ids else {} | ||
| 1431 | # singers 以 spider DB song_id 为 key,反查回 mid | ||
| 1432 | id_to_mid = {v['id']: mid for mid, v in songs.items()} | ||
| 1433 | for song_id in singers: | ||
| 1434 | mid = id_to_mid.get(song_id) | ||
| 1435 | if mid: | ||
| 1436 | valid.add(mid) | ||
| 1437 | |||
| 1438 | if kugou_ids: | ||
| 1439 | songs = fetch_kugou_songs(spider_conn, kugou_ids) | ||
| 1440 | singers = fetch_kugou_singers(spider_conn, list(songs.keys())) | ||
| 1441 | valid.update(int(k) for k in singers) | ||
| 1442 | |||
| 1443 | if netease_ids: | ||
| 1444 | songs = fetch_netease_songs(spider_conn, netease_ids) | ||
| 1445 | singers = fetch_netease_singers(spider_conn, list(songs.keys())) | ||
| 1446 | valid.update(int(k) for k in singers) | ||
| 1447 | |||
| 1448 | return valid | ||
| 1449 | |||
| 1450 | |||
| 1451 | def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None: | 1412 | def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None: |
| 1452 | """以 records1 全量 song_id 为范围,写入补充录音关联并去重。 | 1413 | """以 records1 全量 song_id 为范围,写入补充录音关联并去重。 |
| 1453 | 初始化阶段即校验 spider DB 歌手关联,过滤掉歌手数据缺失的录音。 | 1414 | |
| 1415 | 初始化只校验音眼录音的必要元数据,不以 spider 歌手数据作为准入条件; | ||
| 1416 | 后续导入允许歌曲的 singers 为空。 | ||
| 1454 | """ | 1417 | """ |
| 1455 | src_conn = get_source_conn() | 1418 | src_conn = get_source_conn() |
| 1456 | spider_conn = get_spider_conn() | ||
| 1457 | pg_conn = get_pg_conn() | 1419 | pg_conn = get_pg_conn() |
| 1458 | total_inserted = 0 | 1420 | total_inserted = 0 |
| 1459 | total_filtered_no_singer = 0 | ||
| 1460 | 1421 | ||
| 1461 | try: | 1422 | try: |
| 1462 | with pg_conn.cursor() as pg_cur: | 1423 | with pg_conn.cursor() as pg_cur: |
| ... | @@ -1466,7 +1427,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1466,7 +1427,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1466 | for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')): | 1427 | for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')): |
| 1467 | # 长任务连接可能断开,每批次开始前刷新 | 1428 | # 长任务连接可能断开,每批次开始前刷新 |
| 1468 | src_conn = refresh_conn(src_conn, 'source') | 1429 | src_conn = refresh_conn(src_conn, 'source') |
| 1469 | spider_conn = refresh_conn(spider_conn, 'spider') | ||
| 1470 | pg_conn = refresh_conn(pg_conn, 'pg') | 1430 | pg_conn = refresh_conn(pg_conn, 'pg') |
| 1471 | 1431 | ||
| 1472 | if max_batches is not None and index >= max_batches: | 1432 | if max_batches is not None and index >= max_batches: |
| ... | @@ -1479,7 +1439,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1479,7 +1439,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1479 | 'song_id': int(relation['source_song_id']), | 1439 | 'song_id': int(relation['source_song_id']), |
| 1480 | 'record_id': int(relation['record_id']), | 1440 | 'record_id': int(relation['record_id']), |
| 1481 | 'platform': str(relation['platform']), | 1441 | 'platform': str(relation['platform']), |
| 1482 | 'platform_unique_key': relation.get('platform_unique_key', ''), | ||
| 1483 | } | 1442 | } |
| 1484 | for relation in relations | 1443 | for relation in relations |
| 1485 | if str(relation['platform']) in platforms | 1444 | if str(relation['platform']) in platforms |
| ... | @@ -1487,20 +1446,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1487,20 +1446,6 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1487 | if not rows: | 1446 | if not rows: |
| 1488 | continue | 1447 | continue |
| 1489 | 1448 | ||
| 1490 | # ── 校验 spider DB 歌手关联,过滤无效录音 ── | ||
| 1491 | valid_keys = _fetch_valid_singer_keys(spider_conn, rows) | ||
| 1492 | before = len(rows) | ||
| 1493 | rows = [ | ||
| 1494 | r for r in rows | ||
| 1495 | if _row_singer_key(r) in valid_keys | ||
| 1496 | ] | ||
| 1497 | filtered = before - len(rows) | ||
| 1498 | total_filtered_no_singer += filtered | ||
| 1499 | if filtered: | ||
| 1500 | log.info("init-records2 batch %d: filtered %d rows without valid singers", index, filtered) | ||
| 1501 | if not rows: | ||
| 1502 | continue | ||
| 1503 | |||
| 1504 | with pg_conn.cursor() as pg_cur: | 1449 | with pg_conn.cursor() as pg_cur: |
| 1505 | insert_yinyan_song_records2(pg_cur, rows) | 1450 | insert_yinyan_song_records2(pg_cur, rows) |
| 1506 | pg_conn.commit() | 1451 | pg_conn.commit() |
| ... | @@ -1514,11 +1459,9 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1514,11 +1459,9 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1514 | close_all_pools() | 1459 | close_all_pools() |
| 1515 | 1460 | ||
| 1516 | log.info( | 1461 | log.info( |
| 1517 | "Initialized yinyan_song_records2 candidates=%d, removed_existing_records1_relations=%d, " | 1462 | "Initialized yinyan_song_records2 candidates=%d, removed_existing_records1_relations=%d", |
| 1518 | "filtered_no_singer=%d", | ||
| 1519 | total_inserted, | 1463 | total_inserted, |
| 1520 | deleted, | 1464 | deleted, |
| 1521 | total_filtered_no_singer, | ||
| 1522 | ) | 1465 | ) |
| 1523 | 1466 | ||
| 1524 | 1467 | ... | ... |
| ... | @@ -244,6 +244,44 @@ def test_records2_selection_accepts_current_record_with_empty_cover_and_lyric(): | ... | @@ -244,6 +244,44 @@ def test_records2_selection_accepts_current_record_with_empty_cover_and_lyric(): |
| 244 | assert reason == 'ok' | 244 | assert reason == 'ok' |
| 245 | 245 | ||
| 246 | 246 | ||
| 247 | def test_records2_prepare_payload_allows_empty_singers(monkeypatch): | ||
| 248 | bucket = object() | ||
| 249 | preparer = MagicMock(return_value={ | ||
| 250 | 'platform': runner.PLATFORM_QQ, | ||
| 251 | 'platform_song_id': 100, | ||
| 252 | 'result': {'platform': 'qq', 'platform_song_id': 100}, | ||
| 253 | 'singers': [], | ||
| 254 | 'albums': [], | ||
| 255 | 'songs': [{'platform_song_id': 100, 'singers_json': '[]'}], | ||
| 256 | 'singer_songs': [], | ||
| 257 | 'singer_albums': [], | ||
| 258 | }) | ||
| 259 | monkeypatch.setattr(runner, 'YINYAN_IMPORT_TABLE', 'yinyan_song_records2') | ||
| 260 | monkeypatch.setitem(runner._PREPARERS, runner.PLATFORM_QQ, preparer) | ||
| 261 | |||
| 262 | payload = runner._prepare_import_payload( | ||
| 263 | {'song_id': 10, 'record_id': 20, 'platform': runner.PLATFORM_QQ}, | ||
| 264 | {'name': '歌曲'}, | ||
| 265 | {'record_id': 20, 'platform_unique_key': 'qq-mid'}, | ||
| 266 | bucket, | ||
| 267 | 'https://oss.example.com', | ||
| 268 | {runner.PLATFORM_QQ: {'qq-mid': {'id': 100}}}, | ||
| 269 | {runner.PLATFORM_QQ: {}}, | ||
| 270 | ) | ||
| 271 | |||
| 272 | assert payload is not None | ||
| 273 | assert payload['singers'] == [] | ||
| 274 | assert payload['singer_songs'] == [] | ||
| 275 | preparer.assert_called_once_with( | ||
| 276 | {'name': '歌曲'}, | ||
| 277 | {'record_id': 20, 'platform_unique_key': 'qq-mid'}, | ||
| 278 | bucket, | ||
| 279 | 'https://oss.example.com', | ||
| 280 | {'id': 100}, | ||
| 281 | [], | ||
| 282 | ) | ||
| 283 | |||
| 284 | |||
| 247 | @pytest.mark.parametrize( | 285 | @pytest.mark.parametrize( |
| 248 | ('platform', 'songs_table', 'song_upsert_name', 'relation_upsert_name'), | 286 | ('platform', 'songs_table', 'song_upsert_name', 'relation_upsert_name'), |
| 249 | [ | 287 | [ |
| ... | @@ -463,7 +501,6 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( | ... | @@ -463,7 +501,6 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( |
| 463 | dedupe_calls = [] | 501 | dedupe_calls = [] |
| 464 | 502 | ||
| 465 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 503 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 466 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) | ||
| 467 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 504 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) |
| 468 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 505 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 469 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) | 506 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) |
| ... | @@ -471,7 +508,6 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( | ... | @@ -471,7 +508,6 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( |
| 471 | {'source_song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid100'}, | 508 | {'source_song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid100'}, |
| 472 | {'source_song_id': 10, 'record_id': 200, 'platform': '2', 'platform_unique_key': '200'}, | 509 | {'source_song_id': 10, 'record_id': 200, 'platform': '2', 'platform_unique_key': '200'}, |
| 473 | ]) | 510 | ]) |
| 474 | monkeypatch.setattr(runner, '_fetch_valid_singer_keys', lambda conn, rows: {'mid100', 200}) | ||
| 475 | monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows)) | 511 | monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows)) |
| 476 | monkeypatch.setattr( | 512 | monkeypatch.setattr( |
| 477 | runner, | 513 | runner, |
| ... | @@ -482,19 +518,20 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( | ... | @@ -482,19 +518,20 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( |
| 482 | runner.initialize_yinyan_song_records2(['1', '2']) | 518 | runner.initialize_yinyan_song_records2(['1', '2']) |
| 483 | 519 | ||
| 484 | assert inserted == [ | 520 | assert inserted == [ |
| 485 | {'song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid100'}, | 521 | {'song_id': 10, 'record_id': 100, 'platform': '1'}, |
| 486 | {'song_id': 10, 'record_id': 200, 'platform': '2', 'platform_unique_key': '200'}, | 522 | {'song_id': 10, 'record_id': 200, 'platform': '2'}, |
| 487 | ] | 523 | ] |
| 488 | assert dedupe_calls == [True] | 524 | assert dedupe_calls == [True] |
| 489 | assert pg_conn.commits == 2 | 525 | assert pg_conn.commits == 2 |
| 490 | 526 | ||
| 491 | 527 | ||
| 492 | def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monkeypatch): | 528 | def test_initialize_yinyan_song_records2_keeps_rows_without_valid_singers(monkeypatch): |
| 493 | pg_conn = _PgConnection() | 529 | pg_conn = _PgConnection() |
| 494 | inserted = [] | 530 | inserted = [] |
| 495 | 531 | ||
| 496 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 532 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 497 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) | 533 | get_spider_conn = MagicMock(side_effect=AssertionError('records2 init must not query spider')) |
| 534 | monkeypatch.setattr(runner, 'get_spider_conn', get_spider_conn) | ||
| 498 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 535 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) |
| 499 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 536 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 500 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) | 537 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) |
| ... | @@ -503,8 +540,6 @@ def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monk | ... | @@ -503,8 +540,6 @@ def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monk |
| 503 | {'source_song_id': 10, 'record_id': 200, 'platform': '1', 'platform_unique_key': 'mid_bad'}, | 540 | {'source_song_id': 10, 'record_id': 200, 'platform': '1', 'platform_unique_key': 'mid_bad'}, |
| 504 | {'source_song_id': 10, 'record_id': 300, 'platform': '2', 'platform_unique_key': '300'}, | 541 | {'source_song_id': 10, 'record_id': 300, 'platform': '2', 'platform_unique_key': '300'}, |
| 505 | ]) | 542 | ]) |
| 506 | # mid_bad 没有歌手关联,300 也没有 | ||
| 507 | monkeypatch.setattr(runner, '_fetch_valid_singer_keys', lambda conn, rows: {'mid_good'}) | ||
| 508 | monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows)) | 543 | monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows)) |
| 509 | monkeypatch.setattr( | 544 | monkeypatch.setattr( |
| 510 | runner, 'delete_yinyan_song_records2_existing_relations', lambda cur: 0, | 545 | runner, 'delete_yinyan_song_records2_existing_relations', lambda cur: 0, |
| ... | @@ -512,10 +547,13 @@ def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monk | ... | @@ -512,10 +547,13 @@ def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monk |
| 512 | 547 | ||
| 513 | runner.initialize_yinyan_song_records2(['1', '2']) | 548 | runner.initialize_yinyan_song_records2(['1', '2']) |
| 514 | 549 | ||
| 515 | # 只有 mid_good 的录音被保留 | 550 | # 初始化不查询 spider,所有满足音眼必要字段的录音都应保留。 |
| 516 | assert inserted == [ | 551 | assert inserted == [ |
| 517 | {'song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid_good'}, | 552 | {'song_id': 10, 'record_id': 100, 'platform': '1'}, |
| 553 | {'song_id': 10, 'record_id': 200, 'platform': '1'}, | ||
| 554 | {'song_id': 10, 'record_id': 300, 'platform': '2'}, | ||
| 518 | ] | 555 | ] |
| 556 | get_spider_conn.assert_not_called() | ||
| 519 | 557 | ||
| 520 | 558 | ||
| 521 | def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypatch): | 559 | def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypatch): |
| ... | @@ -597,7 +635,11 @@ def test_process_qq_builds_album_json_for_song_insert(monkeypatch): | ... | @@ -597,7 +635,11 @@ def test_process_qq_builds_album_json_for_song_insert(monkeypatch): |
| 597 | 'issue_time': '2019-01-01', | 635 | 'issue_time': '2019-01-01', |
| 598 | 'song_time': 120, | 636 | 'song_time': 120, |
| 599 | }, | 637 | }, |
| 600 | {'platform_unique_key': 'qq-mid', 'platform_mid': '200'}, | 638 | { |
| 639 | 'platform_unique_key': 'qq-mid', | ||
| 640 | 'platform_mid': '200', | ||
| 641 | 'record_name': '化风行万里 (DJ默涵版)', | ||
| 642 | }, | ||
| 601 | spider_conn=object(), | 643 | spider_conn=object(), |
| 602 | pg_cur=pg_cur, | 644 | pg_cur=pg_cur, |
| 603 | bucket=object(), | 645 | bucket=object(), |
| ... | @@ -665,7 +707,7 @@ def test_process_netease_builds_album_json_for_song_insert(monkeypatch): | ... | @@ -665,7 +707,7 @@ def test_process_netease_builds_album_json_for_song_insert(monkeypatch): |
| 665 | 'issue_time': '2019-01-01', | 707 | 'issue_time': '2019-01-01', |
| 666 | 'song_time': 120, | 708 | 'song_time': 120, |
| 667 | }, | 709 | }, |
| 668 | {'platform_unique_key': '300'}, | 710 | {'platform_unique_key': '300', 'record_name': '化风行万里 (DJ默涵版)'}, |
| 669 | spider_conn=object(), | 711 | spider_conn=object(), |
| 670 | pg_cur=pg_cur, | 712 | pg_cur=pg_cur, |
| 671 | bucket=object(), | 713 | bucket=object(), |
| ... | @@ -710,7 +752,7 @@ def test_process_netease_uses_prefetched_song_and_singers(monkeypatch): | ... | @@ -710,7 +752,7 @@ def test_process_netease_uses_prefetched_song_and_singers(monkeypatch): |
| 710 | 'issue_time': '2019-01-01', | 752 | 'issue_time': '2019-01-01', |
| 711 | 'song_time': 120, | 753 | 'song_time': 120, |
| 712 | }, | 754 | }, |
| 713 | {'platform_unique_key': '300'}, | 755 | {'platform_unique_key': '300', 'record_name': '录音标题'}, |
| 714 | spider_conn=object(), | 756 | spider_conn=object(), |
| 715 | pg_cur=pg_cur, | 757 | pg_cur=pg_cur, |
| 716 | bucket=object(), | 758 | bucket=object(), |
| ... | @@ -789,7 +831,7 @@ def test_process_netease_keeps_timestamped_lyric_and_uploads_plain_lyric(monkeyp | ... | @@ -789,7 +831,7 @@ def test_process_netease_keeps_timestamped_lyric_and_uploads_plain_lyric(monkeyp |
| 789 | 'issue_time': '2019-01-01', | 831 | 'issue_time': '2019-01-01', |
| 790 | 'song_time': 120, | 832 | 'song_time': 120, |
| 791 | }, | 833 | }, |
| 792 | {'platform_unique_key': '300'}, | 834 | {'platform_unique_key': '300', 'record_name': '录音标题'}, |
| 793 | spider_conn=object(), | 835 | spider_conn=object(), |
| 794 | pg_cur=pg_cur, | 836 | pg_cur=pg_cur, |
| 795 | bucket=Bucket(), | 837 | bucket=Bucket(), | ... | ... |
-
Please register or sign in to post a comment