feat(records2): 添加完整录音元数据的关联查询与初始化逻辑
- 优化长任务连接刷新和分页批处理机制
Showing
6 changed files
with
79 additions
and
15 deletions
| ... | @@ -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(): | ... | ... |
-
Please register or sign in to post a comment