Commit 7a8fed26 7a8fed26c310df80e77422db55d47c9077b986ca by 沈秋雨

refactor(runner): 移除 spider DB 歌手关联校验,简化 records2 初始化逻辑

1 parent e2fbce20
...@@ -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(),
......