Commit bbcdcae8 bbcdcae811256a2de8207fe8a38da6eb8c01d8f3 by 沈秋雨

feat(records2): 添加完整录音元数据的关联查询与初始化逻辑

- 优化长任务连接刷新和分页批处理机制
1 parent 3a86c294
...@@ -95,6 +95,37 @@ WHERE mr.id IN ({placeholders}) ...@@ -95,6 +95,37 @@ WHERE mr.id IN ({placeholders})
95 # 同 _PLATFORM_QUERY,但不做 per-platform 去重,保留同平台全部录音供 singer fallback 遍历 95 # 同 _PLATFORM_QUERY,但不做 per-platform 去重,保留同平台全部录音供 singer fallback 遍历
96 _ALL_PLATFORM_RECORDS_QUERY = _PLATFORM_QUERY 96 _ALL_PLATFORM_RECORDS_QUERY = _PLATFORM_QUERY
97 97
98 # records2 只接收具备完整录音元数据的关联关系。
99 _RECORDS2_RELATIONS_QUERY = """
100 SELECT
101 sar.song_id AS source_song_id,
102 mr.id AS record_id,
103 mr.platform,
104 mr.platform_unique_key,
105 mr.platform_mid,
106 mr.album_audio_id,
107 sar.is_main_version,
108 mr.is_high,
109 mr.pub_time
110 FROM hk_song_and_record sar
111 JOIN hk_music_record mr ON mr.id = sar.record_id
112 WHERE sar.song_id IN ({placeholders})
113 AND mr.platform IN ('1','2','4')
114 AND mr.platform_unique_key IS NOT NULL
115 AND TRIM(mr.platform_unique_key) != ''
116 AND mr.deleted = 0
117 AND mr.record_name IS NOT NULL AND TRIM(mr.record_name) != ''
118 AND mr.duration IS NOT NULL AND mr.duration > 0
119 AND mr.singer_name IS NOT NULL AND TRIM(mr.singer_name) != ''
120 AND mr.platform_index_url IS NOT NULL AND TRIM(mr.platform_index_url) != ''
121 AND (
122 (mr.storage_url IS NOT NULL AND TRIM(mr.storage_url) != '')
123 OR (mr.platform_play_url IS NOT NULL AND TRIM(mr.platform_play_url) != '')
124 )
125 AND mr.pub_time IS NOT NULL
126 ORDER BY sar.song_id, mr.id
127 """
128
98 129
99 def iter_hk_songs_batches( 130 def iter_hk_songs_batches(
100 conn: pymysql.Connection, 131 conn: pymysql.Connection,
...@@ -196,11 +227,11 @@ def fetch_all_platform_records(source_conn: pymysql.Connection, song_ids: list[i ...@@ -196,11 +227,11 @@ def fetch_all_platform_records(source_conn: pymysql.Connection, song_ids: list[i
196 227
197 228
198 def fetch_all_song_record_relations(source_conn: pymysql.Connection, song_ids: list[int]) -> list[dict]: 229 def fetch_all_song_record_relations(source_conn: pymysql.Connection, song_ids: list[int]) -> list[dict]:
199 """返回指定 song_id 的全部有效 (song_id, record_id, platform) 关联。""" 230 """返回指定 song_id 的全部必填字段完整的 (song_id, record_id, platform) 关联。"""
200 if not song_ids: 231 if not song_ids:
201 return [] 232 return []
202 placeholders = ','.join(['%s'] * len(song_ids)) 233 placeholders = ','.join(['%s'] * len(song_ids))
203 query = _ALL_PLATFORM_RECORDS_QUERY.format(placeholders=placeholders) 234 query = _RECORDS2_RELATIONS_QUERY.format(placeholders=placeholders)
204 with source_conn.cursor() as cur: 235 with source_conn.cursor() as cur:
205 cur.execute(query, song_ids) 236 cur.execute(query, song_ids)
206 rows = cur.fetchall() 237 rows = cur.fetchall()
......
...@@ -8,7 +8,6 @@ from .config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, BATCH_SIZE, B ...@@ -8,7 +8,6 @@ from .config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, BATCH_SIZE, B
8 from .connections import get_hk_songs_conn, get_source_conn, get_spider_conn, get_pg_conn, get_oss_bucket, refresh_conn, close_all_pools 8 from .connections import get_hk_songs_conn, get_source_conn, get_spider_conn, get_pg_conn, get_oss_bucket, refresh_conn, close_all_pools
9 from .reader import ( 9 from .reader import (
10 iter_hk_songs_batches, 10 iter_hk_songs_batches,
11 iter_hk_songs_records2_batches,
12 fetch_hk_songs_by_source_ids, 11 fetch_hk_songs_by_source_ids,
13 mark_hk_songs_deleted, 12 mark_hk_songs_deleted,
14 fetch_platform_records, 13 fetch_platform_records,
...@@ -30,6 +29,7 @@ from .writer import ( ...@@ -30,6 +29,7 @@ from .writer import (
30 insert_yinyan_song_records, 29 insert_yinyan_song_records,
31 insert_yinyan_song_records2, 30 insert_yinyan_song_records2,
32 delete_yinyan_song_records2_existing_relations, 31 delete_yinyan_song_records2_existing_relations,
32 fetch_yinyan_song_ids,
33 update_yinyan_record_platforms, 33 update_yinyan_record_platforms,
34 upsert_yinyan_song_records, 34 upsert_yinyan_song_records,
35 fetch_existing_yinyan_song_ids, 35 fetch_existing_yinyan_song_ids,
...@@ -1247,26 +1247,26 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None ...@@ -1247,26 +1247,26 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None
1247 1247
1248 1248
1249 def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None: 1249 def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None:
1250 """写入所有合格歌曲关联,再删除 records1 已存在的关联。""" 1250 """以 records1 全量 song_id 为范围,写入补充录音关联并去重。"""
1251 hk_conn = get_hk_songs_conn()
1252 src_conn = get_source_conn() 1251 src_conn = get_source_conn()
1253 pg_conn = get_pg_conn() 1252 pg_conn = get_pg_conn()
1254 total_inserted = 0 1253 total_inserted = 0
1255 1254
1256 try: 1255 try:
1257 for index, batch in enumerate( 1256 with pg_conn.cursor() as pg_cur:
1258 tqdm(iter_hk_songs_records2_batches(hk_conn, BATCH_SIZE), desc='init-yinyan-records2') 1257 song_ids = fetch_yinyan_song_ids(pg_cur)
1259 ): 1258 log.info('records2 source scope: records1 song_ids=%d', len(song_ids))
1259
1260 for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')):
1260 # 长任务连接可能断开,每批次开始前刷新 1261 # 长任务连接可能断开,每批次开始前刷新
1261 hk_conn = refresh_conn(hk_conn, 'hk_songs')
1262 src_conn = refresh_conn(src_conn, 'source') 1262 src_conn = refresh_conn(src_conn, 'source')
1263 pg_conn = refresh_conn(pg_conn, 'pg') 1263 pg_conn = refresh_conn(pg_conn, 'pg')
1264 1264
1265 if max_batches is not None and index >= max_batches: 1265 if max_batches is not None and index >= max_batches:
1266 break 1266 break
1267 1267
1268 song_ids = list({int(row['source_song_id']) for row in batch}) 1268 batch_song_ids = song_ids[start:start + BATCH_SIZE]
1269 relations = fetch_all_song_record_relations(src_conn, song_ids) 1269 relations = fetch_all_song_record_relations(src_conn, batch_song_ids)
1270 rows = [ 1270 rows = [
1271 { 1271 {
1272 'song_id': int(relation['source_song_id']), 1272 'song_id': int(relation['source_song_id']),
......
...@@ -70,6 +70,18 @@ def delete_yinyan_song_records2_existing_relations(cur) -> int: ...@@ -70,6 +70,18 @@ def delete_yinyan_song_records2_existing_relations(cur) -> int:
70 return cur.rowcount 70 return cur.rowcount
71 71
72 72
73 def fetch_yinyan_song_ids(cur) -> list[int]:
74 """返回 records1 全量 song_id,作为 records2 补充范围。"""
75 cur.execute(
76 """
77 SELECT DISTINCT song_id
78 FROM yinyan_song_records
79 ORDER BY song_id
80 """
81 )
82 return [int(row[0]) for row in cur.fetchall()]
83
84
73 def fetch_existing_yinyan_song_ids(cur) -> set[int]: 85 def fetch_existing_yinyan_song_ids(cur) -> set[int]:
74 """返回已完成初始化 platform 的 song_id 集合。""" 86 """返回已完成初始化 platform 的 song_id 集合。"""
75 cur.execute("SELECT DISTINCT song_id FROM yinyan_song_records WHERE platform IS NOT NULL") 87 cur.execute("SELECT DISTINCT song_id FROM yinyan_song_records WHERE platform IS NOT NULL")
......
...@@ -98,6 +98,15 @@ def test_fetch_all_song_record_relations_keeps_all_records_for_a_song(): ...@@ -98,6 +98,15 @@ def test_fetch_all_song_record_relations_keeps_all_records_for_a_song():
98 98
99 result = fetch_all_song_record_relations(conn, [10]) 99 result = fetch_all_song_record_relations(conn, [10])
100 100
101 sql, params = conn.cursor.return_value.execute.call_args[0]
102 assert 'mr.record_name IS NOT NULL' in sql
103 assert 'mr.duration IS NOT NULL AND mr.duration > 0' in sql
104 assert 'mr.singer_name IS NOT NULL' in sql
105 assert 'mr.platform_index_url IS NOT NULL' in sql
106 assert 'mr.storage_url IS NOT NULL' in sql
107 assert 'mr.platform_play_url IS NOT NULL' in sql
108 assert 'mr.pub_time IS NOT NULL' in sql
109 assert params == [10]
101 assert [(row['record_id'], row['platform']) for row in result] == [ 110 assert [(row['record_id'], row['platform']) for row in result] == [
102 (100, '1'), (101, '1'), (200, '2'), 111 (100, '1'), (101, '1'), (200, '2'),
103 ] 112 ]
......
...@@ -263,12 +263,10 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( ...@@ -263,12 +263,10 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates(
263 inserted = [] 263 inserted = []
264 dedupe_calls = [] 264 dedupe_calls = []
265 265
266 monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: _Connection())
267 monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) 266 monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection())
268 monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) 267 monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn)
269 monkeypatch.setattr(runner, 'iter_hk_songs_records2_batches', lambda conn, batch_size: [[ 268 monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn)
270 {'id': 50, 'source_song_id': 10}, 269 monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10])
271 ]])
272 monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ 270 monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [
273 {'source_song_id': 10, 'record_id': 100, 'platform': '1'}, 271 {'source_song_id': 10, 'record_id': 100, 'platform': '1'},
274 {'source_song_id': 10, 'record_id': 200, 'platform': '2'}, 272 {'source_song_id': 10, 'record_id': 200, 'platform': '2'},
......
...@@ -5,6 +5,7 @@ from etl_to_crawler.writer import ( ...@@ -5,6 +5,7 @@ from etl_to_crawler.writer import (
5 insert_yinyan_song_records, 5 insert_yinyan_song_records,
6 insert_yinyan_song_records2, 6 insert_yinyan_song_records2,
7 delete_yinyan_song_records2_existing_relations, 7 delete_yinyan_song_records2_existing_relations,
8 fetch_yinyan_song_ids,
8 update_yinyan_record_platforms, 9 update_yinyan_record_platforms,
9 upsert_kugou_albums, 10 upsert_kugou_albums,
10 upsert_kugou_singers, 11 upsert_kugou_singers,
...@@ -148,6 +149,19 @@ def test_delete_yinyan_song_records2_existing_relations_uses_records1_as_dedup_s ...@@ -148,6 +149,19 @@ def test_delete_yinyan_song_records2_existing_relations_uses_records1_as_dedup_s
148 assert 'USING yinyan_song_records AS ysr1' in sql 149 assert 'USING yinyan_song_records AS ysr1' in sql
149 assert 'ysr2.song_id = ysr1.song_id' in sql 150 assert 'ysr2.song_id = ysr1.song_id' in sql
150 assert 'ysr2.record_id = ysr1.record_id' in sql 151 assert 'ysr2.record_id = ysr1.record_id' in sql
152 assert 'is_yinyan_push' not in sql
153
154
155 def test_fetch_yinyan_song_ids_reads_all_records1_song_ids():
156 cur = MagicMock()
157 cur.fetchall.return_value = [(10,), (11,)]
158
159 song_ids = fetch_yinyan_song_ids(cur)
160
161 sql = cur.execute.call_args[0][0]
162 assert 'SELECT DISTINCT song_id' in sql
163 assert 'WHERE' not in sql
164 assert song_ids == [10, 11]
151 165
152 166
153 def test_fetch_yinyan_records_missing_platform_reads_null_platform_rows(): 167 def test_fetch_yinyan_records_missing_platform_reads_null_platform_rows():
......