chore(config): 支持切换音眼导入状态表以兼容 records2
- 在配置中添加 YINYAN_IMPORT_TABLE 环境变量,支持 records 和 records2 两张表 - 增加对 YINYAN_IMPORT_TABLE 合法值的校验,防止配置错误 - 修改写入和查询相关函数以适配动态表名 - 优化 upsert 逻辑,针对 records2 版本避免多条同 song_id 记录一同更新 - run_etl.py 中打印日志增加当前使用的导入表信息 - writer.py、runner.py 调整查询及更新逻辑以支持动态导入表切换 - oss.py 新增下载歌词文本函数,确保即使目标 OSS 包含歌词文件也重新下载内容 - runner.py 优化歌词上传流程,优先转存音眼歌词 URL,失败后回退爬虫端歌词文本 - 增加多处歌词和封面判断逻辑,支持基于音眼歌词 URL 判断歌词可用性 - 添加单元测试覆盖眼眼歌词转存优先逻辑及导入状态表切换功能验证
Showing
10 changed files
with
179 additions
and
27 deletions
| ... | @@ -13,6 +13,9 @@ TARGET_DB_PASSWORD= | ... | @@ -13,6 +13,9 @@ TARGET_DB_PASSWORD= |
| 13 | TARGET_DB_NAME= | 13 | TARGET_DB_NAME= |
| 14 | TARGET_TABLE_NAME=hk_songs | 14 | TARGET_TABLE_NAME=hk_songs |
| 15 | 15 | ||
| 16 | # run_etl.py 默认读取并回写的导入状态表:yinyan_song_records 或 yinyan_song_records2 | ||
| 17 | YINYAN_IMPORT_TABLE=yinyan_song_records | ||
| 18 | |||
| 16 | # OSS 配置 | 19 | # OSS 配置 |
| 17 | OSS_ACCESS_KEY_ID= | 20 | OSS_ACCESS_KEY_ID= |
| 18 | OSS_ACCESS_KEY_SECRET= | 21 | OSS_ACCESS_KEY_SECRET= | ... | ... |
| ... | @@ -48,6 +48,14 @@ PLATFORM_KUGOU = '2' | ... | @@ -48,6 +48,14 @@ PLATFORM_KUGOU = '2' |
| 48 | PLATFORM_NETEASE = '4' | 48 | PLATFORM_NETEASE = '4' |
| 49 | PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE] | 49 | PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE] |
| 50 | 50 | ||
| 51 | YINYAN_IMPORT_TABLE = os.environ.get('YINYAN_IMPORT_TABLE', 'yinyan_song_records').strip() | ||
| 52 | _ALLOWED_YINYAN_IMPORT_TABLES = {'yinyan_song_records', 'yinyan_song_records2'} | ||
| 53 | if YINYAN_IMPORT_TABLE not in _ALLOWED_YINYAN_IMPORT_TABLES: | ||
| 54 | raise ValueError( | ||
| 55 | f"YINYAN_IMPORT_TABLE must be one of {sorted(_ALLOWED_YINYAN_IMPORT_TABLES)}, " | ||
| 56 | f"got {YINYAN_IMPORT_TABLE!r}" | ||
| 57 | ) | ||
| 58 | |||
| 51 | BATCH_SIZE = 1000 | 59 | BATCH_SIZE = 1000 |
| 52 | BACKFILL_BATCH_SIZE = int(os.environ.get('BACKFILL_BATCH_SIZE', '5000')) | 60 | BACKFILL_BATCH_SIZE = int(os.environ.get('BACKFILL_BATCH_SIZE', '5000')) |
| 53 | HTTP_POOL_MAXSIZE = int(os.environ.get('HTTP_POOL_MAXSIZE', '128')) | 61 | HTTP_POOL_MAXSIZE = int(os.environ.get('HTTP_POOL_MAXSIZE', '128')) | ... | ... |
| ... | @@ -85,6 +85,15 @@ def transfer_url(url: str | None, oss_key: str, bucket: oss2.Bucket, base_url: s | ... | @@ -85,6 +85,15 @@ def transfer_url(url: str | None, oss_key: str, bucket: oss2.Bucket, base_url: s |
| 85 | return f"{base_url.rstrip('/')}/{oss_key}" | 85 | return f"{base_url.rstrip('/')}/{oss_key}" |
| 86 | 86 | ||
| 87 | 87 | ||
| 88 | def download_text_url(url: str | None, base_url: str) -> str: | ||
| 89 | """下载歌词等文本 URL;即使源文件已在目标 OSS,也实际读取其内容。""" | ||
| 90 | url = _clean_url(url) | ||
| 91 | if not url: | ||
| 92 | return '' | ||
| 93 | content = _download_content(url, base_url) | ||
| 94 | return content.decode('utf-8-sig') | ||
| 95 | |||
| 96 | |||
| 88 | def transfer_url_with_md5(url: str | None, oss_key: str, bucket: oss2.Bucket, base_url: str) -> tuple[str, str]: | 97 | def transfer_url_with_md5(url: str | None, oss_key: str, bucket: oss2.Bucket, base_url: str) -> tuple[str, str]: |
| 89 | """ | 98 | """ |
| 90 | 将音频 URL 转存到 OSS,并基于下载到的音频字节计算 MD5。 | 99 | 将音频 URL 转存到 OSS,并基于下载到的音频字节计算 MD5。 | ... | ... |
This diff is collapsed.
Click to expand it.
| 1 | import uuid | 1 | import uuid |
| 2 | 2 | ||
| 3 | from .config import YINYAN_IMPORT_TABLE | ||
| 4 | |||
| 3 | _SINGER_INDEX_VALUES = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ#') | 5 | _SINGER_INDEX_VALUES = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ#') |
| 4 | _SINGER_SEX_VALUES = {'M', 'F', 'C', 'U'} | 6 | _SINGER_SEX_VALUES = {'M', 'F', 'C', 'U'} |
| 5 | _SINGER_AREA_VALUES = {'华语', '欧美', '韩国', '日本', '其他'} | 7 | _SINGER_AREA_VALUES = {'华语', '欧美', '韩国', '日本', '其他'} |
| 6 | # 资源转存失败哨兵值:platform_song_id 设为 -1 表示 OSS 失败,区别于 NULL(待导入)和正整数(成功) | 8 | # platform_song_id 负数为失败码;NULL 表示待导入,正整数表示成功。 |
| 7 | PLATFORM_SONG_ID_RESOURCE_FAILED = -1 | 9 | YINYAN_IMPORT_FAILURE_UNKNOWN = -1 |
| 10 | YINYAN_IMPORT_FAILURE_NO_LYRIC = -2 | ||
| 11 | YINYAN_IMPORT_FAILURE_NO_COVER = -3 | ||
| 12 | YINYAN_IMPORT_FAILURE_NO_LYRIC_AND_COVER = -4 | ||
| 13 | YINYAN_IMPORT_FAILURE_SOURCE_DATA_MISSING = -5 | ||
| 14 | YINYAN_IMPORT_FAILURE_AUDIO_TRANSFER = -6 | ||
| 15 | YINYAN_IMPORT_FAILURE_COVER_TRANSFER = -7 | ||
| 16 | YINYAN_IMPORT_FAILURE_LYRIC_TRANSFER = -8 | ||
| 17 | YINYAN_IMPORT_FAILURE_MULTIPLE_ASSET_TRANSFER = -9 | ||
| 18 | YINYAN_IMPORT_FAILURE_UNEXPECTED = -10 | ||
| 19 | |||
| 20 | YINYAN_IMPORT_FAILURE_CODES = { | ||
| 21 | YINYAN_IMPORT_FAILURE_UNKNOWN: 'unknown', | ||
| 22 | YINYAN_IMPORT_FAILURE_NO_LYRIC: 'no_usable_lyric', | ||
| 23 | YINYAN_IMPORT_FAILURE_NO_COVER: 'no_usable_cover', | ||
| 24 | YINYAN_IMPORT_FAILURE_NO_LYRIC_AND_COVER: 'no_usable_lyric_and_cover', | ||
| 25 | YINYAN_IMPORT_FAILURE_SOURCE_DATA_MISSING: 'source_or_spider_data_missing', | ||
| 26 | YINYAN_IMPORT_FAILURE_AUDIO_TRANSFER: 'audio_oss_transfer_failed', | ||
| 27 | YINYAN_IMPORT_FAILURE_COVER_TRANSFER: 'cover_oss_transfer_failed', | ||
| 28 | YINYAN_IMPORT_FAILURE_LYRIC_TRANSFER: 'lyric_oss_transfer_failed', | ||
| 29 | YINYAN_IMPORT_FAILURE_MULTIPLE_ASSET_TRANSFER: 'multiple_oss_transfers_failed', | ||
| 30 | YINYAN_IMPORT_FAILURE_UNEXPECTED: 'unexpected_error', | ||
| 31 | } | ||
| 8 | 32 | ||
| 9 | 33 | ||
| 10 | def _singer_index(value) -> str: | 34 | def _singer_index(value) -> str: |
| ... | @@ -90,9 +114,9 @@ def fetch_existing_yinyan_song_ids(cur) -> set[int]: | ... | @@ -90,9 +114,9 @@ def fetch_existing_yinyan_song_ids(cur) -> set[int]: |
| 90 | 114 | ||
| 91 | def fetch_pending_yinyan_song_records(cur, limit: int) -> list[dict]: | 115 | def fetch_pending_yinyan_song_records(cur, limit: int) -> list[dict]: |
| 92 | cur.execute( | 116 | cur.execute( |
| 93 | """ | 117 | f""" |
| 94 | SELECT song_id, record_id, platform | 118 | SELECT song_id, record_id, platform |
| 95 | FROM yinyan_song_records | 119 | FROM {YINYAN_IMPORT_TABLE} |
| 96 | WHERE is_yinyan_push = FALSE | 120 | WHERE is_yinyan_push = FALSE |
| 97 | AND platform IS NOT NULL | 121 | AND platform IS NOT NULL |
| 98 | AND platform_song_id IS NULL | 122 | AND platform_song_id IS NULL |
| ... | @@ -142,23 +166,31 @@ def update_yinyan_record_platforms(cur, records: list[dict]) -> None: | ... | @@ -142,23 +166,31 @@ def update_yinyan_record_platforms(cur, records: list[dict]) -> None: |
| 142 | 166 | ||
| 143 | def upsert_yinyan_song_records(cur, records: list[dict]) -> None: | 167 | def upsert_yinyan_song_records(cur, records: list[dict]) -> None: |
| 144 | """Mark pre-initialized yinyan song-record rows as pushed to crawler. | 168 | """Mark pre-initialized yinyan song-record rows as pushed to crawler. |
| 145 | Matches only on song_id so singer-fallback can use a different record_id than initialized. | 169 | 精确定位原始待处理关系,避免 records2 同一 song_id 的多条录音被一起更新。 |
| 146 | """ | 170 | """ |
| 147 | if not records: | 171 | if not records: |
| 148 | return | 172 | return |
| 149 | values_sql = ', '.join(['(%s::bigint, %s::bigint, %s::varchar, %s::bigint)'] * len(records)) | 173 | values_sql = ', '.join( |
| 174 | ['(%s::bigint, %s::bigint, %s::bigint, %s::varchar, %s::bigint)'] * len(records) | ||
| 175 | ) | ||
| 150 | params = [] | 176 | params = [] |
| 151 | for r in records: | 177 | for r in records: |
| 152 | params.extend([r['song_id'], r['record_id'], r['platform'], r['platform_song_id']]) | 178 | params.extend([ |
| 179 | r['song_id'], r.get('source_record_id', r['record_id']), r['record_id'], | ||
| 180 | r['platform'], r['platform_song_id'], | ||
| 181 | ]) | ||
| 182 | record_id_assignment = '' if YINYAN_IMPORT_TABLE == 'yinyan_song_records2' else 'record_id = v.record_id,' | ||
| 153 | cur.execute( | 183 | cur.execute( |
| 154 | f""" | 184 | f""" |
| 155 | UPDATE yinyan_song_records AS ysr | 185 | UPDATE {YINYAN_IMPORT_TABLE} AS ysr |
| 156 | SET platform = v.platform, | 186 | SET platform = v.platform, |
| 157 | platform_song_id = v.platform_song_id, | 187 | platform_song_id = v.platform_song_id, |
| 158 | record_id = v.record_id, | 188 | {record_id_assignment} |
| 159 | is_yinyan_push = TRUE | 189 | is_yinyan_push = TRUE |
| 160 | FROM (VALUES {values_sql}) AS v(song_id, record_id, platform, platform_song_id) | 190 | FROM (VALUES {values_sql}) AS v(song_id, source_record_id, record_id, platform, platform_song_id) |
| 161 | WHERE ysr.song_id = v.song_id | 191 | WHERE ysr.song_id = v.song_id |
| 192 | AND ysr.record_id = v.source_record_id | ||
| 193 | AND ysr.platform = v.platform | ||
| 162 | AND ysr.is_yinyan_push = FALSE | 194 | AND ysr.is_yinyan_push = FALSE |
| 163 | """, | 195 | """, |
| 164 | tuple(params), | 196 | tuple(params), |
| ... | @@ -254,15 +286,23 @@ def delete_qq_song_and_yinyan_record(cur, song_id: int, platform_song_id: int, s | ... | @@ -254,15 +286,23 @@ def delete_qq_song_and_yinyan_record(cur, song_id: int, platform_song_id: int, s |
| 254 | ) | 286 | ) |
| 255 | 287 | ||
| 256 | 288 | ||
| 257 | def mark_yinyan_record_resource_failed(cur, song_id: int, record_id: int, platform: str) -> None: | 289 | def mark_yinyan_record_resource_failed( |
| 258 | """保留未导入状态,并将 platform_song_id 置为哨兵值 -1,避免后续批次重复处理。""" | 290 | cur, |
| 291 | song_id: int, | ||
| 292 | record_id: int, | ||
| 293 | platform: str, | ||
| 294 | failure_code: int = YINYAN_IMPORT_FAILURE_UNKNOWN, | ||
| 295 | ) -> None: | ||
| 296 | """保留未导入状态,并以负数 platform_song_id 记录具体失败类型。""" | ||
| 297 | if failure_code not in YINYAN_IMPORT_FAILURE_CODES: | ||
| 298 | failure_code = YINYAN_IMPORT_FAILURE_UNKNOWN | ||
| 259 | cur.execute( | 299 | cur.execute( |
| 260 | """ | 300 | f""" |
| 261 | UPDATE yinyan_song_records | 301 | UPDATE {YINYAN_IMPORT_TABLE} |
| 262 | SET is_yinyan_push = FALSE, platform_song_id = %s | 302 | SET is_yinyan_push = FALSE, platform_song_id = %s |
| 263 | WHERE song_id = %s AND record_id = %s AND platform = %s AND is_yinyan_push = FALSE | 303 | WHERE song_id = %s AND record_id = %s AND platform = %s AND is_yinyan_push = FALSE |
| 264 | """, | 304 | """, |
| 265 | (PLATFORM_SONG_ID_RESOURCE_FAILED, song_id, record_id, platform), | 305 | (failure_code, song_id, record_id, platform), |
| 266 | ) | 306 | ) |
| 267 | 307 | ||
| 268 | 308 | ... | ... |
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | import argparse | 2 | import argparse |
| 3 | from etl_to_crawler.config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, PLATFORMS | 3 | from etl_to_crawler.config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, PLATFORMS, YINYAN_IMPORT_TABLE |
| 4 | from etl_to_crawler.runner import backfill_yinyan_record_platforms, initialize_yinyan_song_records, initialize_yinyan_song_records2, run, backfill_empty_singers, backfill_qq_invalid_covers | 4 | from etl_to_crawler.runner import backfill_yinyan_record_platforms, initialize_yinyan_song_records, initialize_yinyan_song_records2, run, backfill_empty_singers, backfill_qq_invalid_covers |
| 5 | 5 | ||
| 6 | PLATFORM_MAP = { | 6 | PLATFORM_MAP = { |
| ... | @@ -33,7 +33,7 @@ if __name__ == '__main__': | ... | @@ -33,7 +33,7 @@ if __name__ == '__main__': |
| 33 | else: | 33 | else: |
| 34 | platforms = [PLATFORM_MAP[args.platform]] | 34 | platforms = [PLATFORM_MAP[args.platform]] |
| 35 | 35 | ||
| 36 | print(f"Starting ETL for platforms: {platforms}") | 36 | print(f"Starting ETL for platforms: {platforms}, import table: {YINYAN_IMPORT_TABLE}") |
| 37 | if args.backfill_yinyan_platforms: | 37 | if args.backfill_yinyan_platforms: |
| 38 | backfill_yinyan_record_platforms(max_batches=args.max_batches) | 38 | backfill_yinyan_record_platforms(max_batches=args.max_batches) |
| 39 | elif args.init_yinyan_records: | 39 | elif args.init_yinyan_records: | ... | ... |
| 1 | from unittest.mock import MagicMock, patch | 1 | from unittest.mock import MagicMock, patch |
| 2 | import math | 2 | import math |
| 3 | import requests | 3 | import requests |
| 4 | from etl_to_crawler.oss import transfer_url, transfer_url_with_md5 | 4 | from etl_to_crawler.oss import download_text_url, transfer_url, transfer_url_with_md5 |
| 5 | 5 | ||
| 6 | ARCHIVE_URL = "https://archive-dev.oss-cn-beijing.aliyuncs.com/some/path.mp3" | 6 | ARCHIVE_URL = "https://archive-dev.oss-cn-beijing.aliyuncs.com/some/path.mp3" |
| 7 | OTHER_URL = "https://hikoon-data-platform.oss-cn-beijing.aliyuncs.com/qq-audio/abc.mp3" | 7 | OTHER_URL = "https://hikoon-data-platform.oss-cn-beijing.aliyuncs.com/qq-audio/abc.mp3" |
| ... | @@ -13,6 +13,15 @@ def test_already_archive_dev_returns_as_is(): | ... | @@ -13,6 +13,15 @@ def test_already_archive_dev_returns_as_is(): |
| 13 | assert result == ARCHIVE_URL | 13 | assert result == ARCHIVE_URL |
| 14 | bucket.put_object.assert_not_called() | 14 | bucket.put_object.assert_not_called() |
| 15 | 15 | ||
| 16 | |||
| 17 | def test_download_text_url_reads_source_even_when_it_is_target_oss(): | ||
| 18 | with patch('etl_to_crawler.oss._http_get') as mock_get: | ||
| 19 | mock_get.return_value.content = b'\xef\xbb\xbf[00:01.00]lyric' | ||
| 20 | mock_get.return_value.raise_for_status = MagicMock() | ||
| 21 | result = download_text_url(ARCHIVE_URL, BASE_URL) | ||
| 22 | |||
| 23 | assert result == '[00:01.00]lyric' | ||
| 24 | |||
| 16 | def test_empty_url_returns_empty(): | 25 | def test_empty_url_returns_empty(): |
| 17 | bucket = MagicMock() | 26 | bucket = MagicMock() |
| 18 | result = transfer_url('', "any/key.mp3", bucket, BASE_URL) | 27 | result = transfer_url('', "any/key.mp3", bucket, BASE_URL) | ... | ... |
| ... | @@ -78,11 +78,33 @@ def test_required_asset_validation_rejects_partial_transfer(): | ... | @@ -78,11 +78,33 @@ def test_required_asset_validation_rejects_partial_transfer(): |
| 78 | }) | 78 | }) |
| 79 | 79 | ||
| 80 | 80 | ||
| 81 | def test_lyric_upload_does_not_fall_back_to_external_source_url(monkeypatch): | 81 | def test_lyric_upload_prefers_hk_url_and_forces_oss_transfer(monkeypatch): |
| 82 | monkeypatch.setattr(runner, 'upload_plain_lyric_to_bucket', lambda *args: '') | 82 | download = MagicMock(return_value='[00:01.00]HK lyric') |
| 83 | monkeypatch.setattr(runner, 'download_text_url', download) | ||
| 84 | upload = MagicMock(return_value='https://archive.example/crawler/lyric/qq/mid.txt') | ||
| 85 | monkeypatch.setattr(runner, 'upload_plain_lyric_to_bucket', upload) | ||
| 86 | bucket = object() | ||
| 87 | |||
| 88 | result = runner._safe_upload_lyric( | ||
| 89 | 'qq', 'mid', 'spider lyric', 'https://source.example/lyric.txt', bucket, 'https://archive.example', | ||
| 90 | ) | ||
| 91 | |||
| 92 | assert result == 'https://archive.example/crawler/lyric/qq/mid.txt' | ||
| 93 | download.assert_called_once_with('https://source.example/lyric.txt', 'https://archive.example') | ||
| 94 | upload.assert_called_once_with( | ||
| 95 | 'qq', 'mid', '[00:01.00]HK lyric', bucket, 'https://archive.example', | ||
| 96 | ) | ||
| 97 | |||
| 98 | |||
| 99 | def test_lyric_upload_falls_back_to_spider_text_when_hk_transfer_fails(monkeypatch): | ||
| 100 | monkeypatch.setattr(runner, 'download_text_url', MagicMock(side_effect=TimeoutError('timeout'))) | ||
| 101 | upload = MagicMock(return_value='https://archive.example/crawler/lyric/qq/mid.txt') | ||
| 102 | monkeypatch.setattr(runner, 'upload_plain_lyric_to_bucket', upload) | ||
| 103 | |||
| 83 | assert runner._safe_upload_lyric( | 104 | assert runner._safe_upload_lyric( |
| 84 | 'qq', 'mid', '', 'https://source.example/lyric.txt', object(), 'https://archive.example', | 105 | 'qq', 'mid', 'spider lyric', 'https://source.example/lyric.txt', object(), 'https://archive.example', |
| 85 | ) == '' | 106 | ) == 'https://archive.example/crawler/lyric/qq/mid.txt' |
| 107 | assert upload.call_args.args[2] == 'spider lyric' | ||
| 86 | 108 | ||
| 87 | 109 | ||
| 88 | def test_qq_cover_source_ignores_missing_album_placeholder_from_hk(): | 110 | def test_qq_cover_source_ignores_missing_album_placeholder_from_hk(): |
| ... | @@ -131,6 +153,25 @@ def test_select_qq_import_record_replaces_missing_lyric_with_same_source_candida | ... | @@ -131,6 +153,25 @@ def test_select_qq_import_record_replaces_missing_lyric_with_same_source_candida |
| 131 | assert reason == 'ok' | 153 | assert reason == 'ok' |
| 132 | 154 | ||
| 133 | 155 | ||
| 156 | def test_select_qq_import_record_accepts_hk_lyric_when_spider_lyric_is_empty(): | ||
| 157 | current = {'record_id': 10, 'platform_unique_key': 'without-spider-lyric'} | ||
| 158 | selected, reason = runner._select_qq_import_record( | ||
| 159 | {'lyrics_url': 'https://archive-dev.example/lyrics/song.txt', 'cover_url': 'https://example.com/cover.jpg'}, | ||
| 160 | current, | ||
| 161 | [current], | ||
| 162 | { | ||
| 163 | 'without-spider-lyric': { | ||
| 164 | 'album_id': 100, | ||
| 165 | 'cover': 'https://example.com/cover.jpg', | ||
| 166 | 'lyric': '', | ||
| 167 | }, | ||
| 168 | }, | ||
| 169 | ) | ||
| 170 | |||
| 171 | assert selected == current | ||
| 172 | assert reason == 'ok' | ||
| 173 | |||
| 174 | |||
| 134 | def test_select_qq_import_record_returns_none_when_no_candidate_satisfies_missing_fields(): | 175 | def test_select_qq_import_record_returns_none_when_no_candidate_satisfies_missing_fields(): |
| 135 | current = {'record_id': 10, 'platform_unique_key': 'bad-mid'} | 176 | current = {'record_id': 10, 'platform_unique_key': 'bad-mid'} |
| 136 | result, reason = runner._select_qq_import_record( | 177 | result, reason = runner._select_qq_import_record( | ... | ... |
| 1 | from unittest.mock import MagicMock, call | 1 | from unittest.mock import MagicMock, call |
| 2 | import etl_to_crawler.writer as writer | ||
| 2 | from etl_to_crawler.writer import ( | 3 | from etl_to_crawler.writer import ( |
| 3 | fetch_pending_yinyan_song_records, | 4 | fetch_pending_yinyan_song_records, |
| 4 | fetch_yinyan_records_missing_platform, | 5 | fetch_yinyan_records_missing_platform, |
| ... | @@ -69,12 +70,19 @@ def test_mark_yinyan_record_resource_failed_keeps_unpushed_state_and_excludes_re | ... | @@ -69,12 +70,19 @@ def test_mark_yinyan_record_resource_failed_keeps_unpushed_state_and_excludes_re |
| 69 | mark_yinyan_record_resource_failed(cur, 10, 100, '1') | 70 | mark_yinyan_record_resource_failed(cur, 10, 100, '1') |
| 70 | 71 | ||
| 71 | sql, params = cur.execute.call_args[0] | 72 | sql, params = cur.execute.call_args[0] |
| 72 | assert 'UPDATE yinyan_song_records' in sql | 73 | assert f'UPDATE {writer.YINYAN_IMPORT_TABLE}' in sql |
| 73 | assert 'is_yinyan_push = FALSE' in sql | 74 | assert 'is_yinyan_push = FALSE' in sql |
| 74 | assert 'platform_song_id = %s' in sql | 75 | assert 'platform_song_id = %s' in sql |
| 75 | assert params == (-1, 10, 100, '1') | 76 | assert params == (-1, 10, 100, '1') |
| 76 | 77 | ||
| 77 | 78 | ||
| 79 | def test_mark_yinyan_record_resource_failed_writes_specific_failure_code(): | ||
| 80 | cur = MagicMock() | ||
| 81 | mark_yinyan_record_resource_failed(cur, 10, 100, '1', writer.YINYAN_IMPORT_FAILURE_NO_LYRIC) | ||
| 82 | |||
| 83 | assert cur.execute.call_args[0][1] == (-2, 10, 100, '1') | ||
| 84 | |||
| 85 | |||
| 78 | def test_upsert_qq_singers_empty_does_nothing(): | 86 | def test_upsert_qq_singers_empty_does_nothing(): |
| 79 | cur = MagicMock() | 87 | cur = MagicMock() |
| 80 | upsert_qq_singers(cur, []) | 88 | upsert_qq_singers(cur, []) |
| ... | @@ -97,16 +105,21 @@ def test_upsert_yinyan_song_records_marks_existing_relation_as_pushed(): | ... | @@ -97,16 +105,21 @@ def test_upsert_yinyan_song_records_marks_existing_relation_as_pushed(): |
| 97 | ]) | 105 | ]) |
| 98 | 106 | ||
| 99 | sql, params = cur.execute.call_args[0] | 107 | sql, params = cur.execute.call_args[0] |
| 100 | assert 'UPDATE yinyan_song_records' in sql | 108 | assert f'UPDATE {writer.YINYAN_IMPORT_TABLE}' in sql |
| 101 | assert 'UPDATE yinyan_song_records AS ysr' in sql | 109 | assert f'UPDATE {writer.YINYAN_IMPORT_TABLE} AS ysr' in sql |
| 102 | assert 'SET platform = v.platform' in sql | 110 | assert 'SET platform = v.platform' in sql |
| 103 | assert 'platform_song_id = v.platform_song_id' in sql | 111 | assert 'platform_song_id = v.platform_song_id' in sql |
| 112 | if writer.YINYAN_IMPORT_TABLE == 'yinyan_song_records2': | ||
| 113 | assert 'record_id = v.record_id' not in sql | ||
| 114 | else: | ||
| 104 | assert 'record_id = v.record_id' in sql | 115 | assert 'record_id = v.record_id' in sql |
| 105 | assert 'is_yinyan_push = TRUE' in sql | 116 | assert 'is_yinyan_push = TRUE' in sql |
| 106 | assert 'FROM (VALUES (%s::bigint, %s::bigint, %s::varchar, %s::bigint), (%s::bigint, %s::bigint, %s::varchar, %s::bigint))' in sql | 117 | assert 'source_record_id' in sql |
| 107 | assert 'ysr.song_id = v.song_id' in sql | 118 | assert 'ysr.song_id = v.song_id' in sql |
| 119 | assert 'ysr.record_id = v.source_record_id' in sql | ||
| 120 | assert 'ysr.platform = v.platform' in sql | ||
| 108 | assert 'ysr.is_yinyan_push = FALSE' in sql | 121 | assert 'ysr.is_yinyan_push = FALSE' in sql |
| 109 | assert params == (10, 100, '1', 1000, 11, 101, '2', 1001) | 122 | assert params == (10, 100, 100, '1', 1000, 11, 101, 101, '2', 1001) |
| 110 | 123 | ||
| 111 | 124 | ||
| 112 | def test_insert_yinyan_song_records_initializes_unpushed_rows(): | 125 | def test_insert_yinyan_song_records_initializes_unpushed_rows(): |
| ... | @@ -193,6 +206,23 @@ def test_fetch_pending_yinyan_song_records_reads_platform_for_single_platform_im | ... | @@ -193,6 +206,23 @@ def test_fetch_pending_yinyan_song_records_reads_platform_for_single_platform_im |
| 193 | ] | 206 | ] |
| 194 | 207 | ||
| 195 | 208 | ||
| 209 | def test_import_state_queries_can_target_records2(monkeypatch): | ||
| 210 | monkeypatch.setattr(writer, 'YINYAN_IMPORT_TABLE', 'yinyan_song_records2') | ||
| 211 | cur = MagicMock() | ||
| 212 | cur.fetchall.return_value = [] | ||
| 213 | |||
| 214 | writer.fetch_pending_yinyan_song_records(cur, 100) | ||
| 215 | assert 'FROM yinyan_song_records2' in cur.execute.call_args[0][0] | ||
| 216 | |||
| 217 | writer.upsert_yinyan_song_records(cur, [ | ||
| 218 | {'song_id': 10, 'record_id': 100, 'platform': '1', 'platform_song_id': 1000}, | ||
| 219 | ]) | ||
| 220 | assert 'UPDATE yinyan_song_records2 AS ysr' in cur.execute.call_args[0][0] | ||
| 221 | |||
| 222 | writer.mark_yinyan_record_resource_failed(cur, 10, 100, '1') | ||
| 223 | assert 'UPDATE yinyan_song_records2' in cur.execute.call_args[0][0] | ||
| 224 | |||
| 225 | |||
| 196 | def test_update_yinyan_record_platforms_fills_only_null_platform_rows(): | 226 | def test_update_yinyan_record_platforms_fills_only_null_platform_rows(): |
| 197 | cur = MagicMock() | 227 | cur = MagicMock() |
| 198 | update_yinyan_record_platforms(cur, [ | 228 | update_yinyan_record_platforms(cur, [ | ... | ... |
失败码映射.md
0 → 100644
| 1 | | **失败码** | **含义** | | ||
| 2 | | ---------- | ---------------------------- | | ||
| 3 | | -1 | 未分类/未知失败 | | ||
| 4 | | -2 | 没有可用歌词 | | ||
| 5 | | -3 | 没有可用封面 | | ||
| 6 | | -4 | 歌词和封面均不可用 | | ||
| 7 | | -5 | HK、音眼源库或spider数据缺失 | | ||
| 8 | | -6 | 音频转存OSS失败 | | ||
| 9 | | -7 | 封面转存OSS失败 | | ||
| 10 | | -8 | 歌词转存OSS失败 | | ||
| 11 | | -9 | 多项必需资源转存失败 | | ||
| 12 | | -10 | 未预期程序异常 | | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment