refactor(crawler): 统一新增 provider_name 与 crawler_source_data 字段
- 替换所有歌手、专辑和歌曲的 provider_name 常量为 PROVIDER_YINYAN - 增加 crawler_source_data 字段传递原始采集数据 JSON - 更新 upsert 函数,支持写入 provider_name 和 crawler_source_data 字段 - 修改数据库写入 SQL,新增 provider_name 和 crawler_source_data 两列 - 添加相关单元测试,验证 provider_name 和 crawler_source_data 是否正确写入 - 统一整合 QQ、酷狗、网易等平台数据结构,增强数据来源追踪能力
Showing
3 changed files
with
210 additions
and
25 deletions
| ... | @@ -26,7 +26,7 @@ from .lyric import strip_timestamps | ... | @@ -26,7 +26,7 @@ from .lyric import strip_timestamps |
| 26 | 26 | ||
| 27 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') | 27 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') |
| 28 | log = logging.getLogger(__name__) | 28 | log = logging.getLogger(__name__) |
| 29 | PROVIDER_KUGOU = 'kugou' | 29 | PROVIDER_YINYAN = 'yinyan' |
| 30 | 30 | ||
| 31 | 31 | ||
| 32 | def _safe_transfer(url, oss_key, bucket, base_url): | 32 | def _safe_transfer(url, oss_key, bucket, base_url): |
| ... | @@ -83,7 +83,13 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): | ... | @@ -83,7 +83,13 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): |
| 83 | build_oss_key('qq', 'singer', sg['mid'] + '.jpg'), | 83 | build_oss_key('qq', 'singer', sg['mid'] + '.jpg'), |
| 84 | bucket, base_url | 84 | bucket, base_url |
| 85 | ) | 85 | ) |
| 86 | singer_rows.append({**sg, 'id': sg['singer_id'], 'avatar': avatar}) | 86 | singer_rows.append({ |
| 87 | **sg, | ||
| 88 | 'id': sg['singer_id'], | ||
| 89 | 'avatar': avatar, | ||
| 90 | 'provider_name': PROVIDER_YINYAN, | ||
| 91 | 'crawler_source_data': _source_json(sg), | ||
| 92 | }) | ||
| 87 | upsert_qq_singers(pg_cur, singer_rows) | 93 | upsert_qq_singers(pg_cur, singer_rows) |
| 88 | 94 | ||
| 89 | # singers JSONB | 95 | # singers JSONB |
| ... | @@ -103,6 +109,19 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): | ... | @@ -103,6 +109,19 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): |
| 103 | 'intro': sp.get('album_intro'), 'type': sp.get('album_type') or '', | 109 | 'intro': sp.get('album_intro'), 'type': sp.get('album_type') or '', |
| 104 | 'company_id': sp.get('company_id') or 0, 'company': sp.get('company') or '', | 110 | 'company_id': sp.get('company_id') or 0, 'company': sp.get('company') or '', |
| 105 | 'is_owner': sp.get('is_owner') or 0, 'published_at': sp.get('album_published_at'), | 111 | 'is_owner': sp.get('is_owner') or 0, 'published_at': sp.get('album_published_at'), |
| 112 | 'provider_name': PROVIDER_YINYAN, | ||
| 113 | 'crawler_source_data': _source_json({ | ||
| 114 | 'id': sp.get('album_id'), | ||
| 115 | 'mid': sp.get('album_mid'), | ||
| 116 | 'cover': sp.get('album_cover'), | ||
| 117 | 'title': sp.get('album_title'), | ||
| 118 | 'intro': sp.get('album_intro'), | ||
| 119 | 'type': sp.get('album_type'), | ||
| 120 | 'company_id': sp.get('company_id'), | ||
| 121 | 'company': sp.get('company'), | ||
| 122 | 'is_owner': sp.get('is_owner'), | ||
| 123 | 'published_at': sp.get('album_published_at'), | ||
| 124 | }), | ||
| 106 | }]) | 125 | }]) |
| 107 | 126 | ||
| 108 | # 写入 song | 127 | # 写入 song |
| ... | @@ -125,6 +144,8 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): | ... | @@ -125,6 +144,8 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): |
| 125 | 'platform_index_url': sp.get('platform_index_url') or f'https://y.qq.com/n/ryqq/songDetail/{mid}', | 144 | 'platform_index_url': sp.get('platform_index_url') or f'https://y.qq.com/n/ryqq/songDetail/{mid}', |
| 126 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), | 145 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), |
| 127 | 'singers_json': singers_json, | 146 | 'singers_json': singers_json, |
| 147 | 'provider_name': PROVIDER_YINYAN, | ||
| 148 | 'crawler_source_data': _source_json(sp), | ||
| 128 | }]) | 149 | }]) |
| 129 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) | 150 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) |
| 130 | pg_cur.execute('SELECT id FROM crawler_qqmusic_songs WHERE platform_song_id = %s', (platform_song_id,)) | 151 | pg_cur.execute('SELECT id FROM crawler_qqmusic_songs WHERE platform_song_id = %s', (platform_song_id,)) |
| ... | @@ -178,7 +199,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url | ... | @@ -178,7 +199,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url |
| 178 | **sg, | 199 | **sg, |
| 179 | 'id': sg['singer_id'], | 200 | 'id': sg['singer_id'], |
| 180 | 'avatar': avatar, | 201 | 'avatar': avatar, |
| 181 | 'provider_name': PROVIDER_KUGOU, | 202 | 'provider_name': PROVIDER_YINYAN, |
| 182 | 'crawler_source_data': _source_json(sg), | 203 | 'crawler_source_data': _source_json(sg), |
| 183 | }) | 204 | }) |
| 184 | upsert_kugou_singers(pg_cur, singer_rows) | 205 | upsert_kugou_singers(pg_cur, singer_rows) |
| ... | @@ -198,7 +219,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url | ... | @@ -198,7 +219,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url |
| 198 | 'type': sp.get('album_type') or '', 'company_id': sp.get('company_id') or 0, | 219 | 'type': sp.get('album_type') or '', 'company_id': sp.get('company_id') or 0, |
| 199 | 'company': sp.get('company') or '', 'is_owner': sp.get('is_owner') or 0, | 220 | 'company': sp.get('company') or '', 'is_owner': sp.get('is_owner') or 0, |
| 200 | 'published_at': sp.get('album_published_at'), | 221 | 'published_at': sp.get('album_published_at'), |
| 201 | 'provider_name': PROVIDER_KUGOU, | 222 | 'provider_name': PROVIDER_YINYAN, |
| 202 | 'crawler_source_data': _source_json({ | 223 | 'crawler_source_data': _source_json({ |
| 203 | 'id': sp.get('album_id'), | 224 | 'id': sp.get('album_id'), |
| 204 | 'cover': sp.get('album_cover'), | 225 | 'cover': sp.get('album_cover'), |
| ... | @@ -231,7 +252,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url | ... | @@ -231,7 +252,7 @@ def _process_kugou(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url |
| 231 | 'platform_index_url': sp.get('platform_index_url') or f'http://www.kugou.com/song/#hash={sp.get("hid") or pr.get("platform_mid", "")}', | 252 | 'platform_index_url': sp.get('platform_index_url') or f'http://www.kugou.com/song/#hash={sp.get("hid") or pr.get("platform_mid", "")}', |
| 232 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), | 253 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), |
| 233 | 'singers_json': singers_json, | 254 | 'singers_json': singers_json, |
| 234 | 'provider_name': PROVIDER_KUGOU, | 255 | 'provider_name': PROVIDER_YINYAN, |
| 235 | 'crawler_source_data': _source_json(sp), | 256 | 'crawler_source_data': _source_json(sp), |
| 236 | }]) | 257 | }]) |
| 237 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) | 258 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) |
| ... | @@ -281,7 +302,13 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u | ... | @@ -281,7 +302,13 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u |
| 281 | build_oss_key('netease', 'singer', str(sg['singer_id']) + '.jpg'), | 302 | build_oss_key('netease', 'singer', str(sg['singer_id']) + '.jpg'), |
| 282 | bucket, base_url | 303 | bucket, base_url |
| 283 | ) | 304 | ) |
| 284 | singer_rows.append({**sg, 'id': sg['singer_id'], 'avatar': avatar}) | 305 | singer_rows.append({ |
| 306 | **sg, | ||
| 307 | 'id': sg['singer_id'], | ||
| 308 | 'avatar': avatar, | ||
| 309 | 'provider_name': PROVIDER_YINYAN, | ||
| 310 | 'crawler_source_data': _source_json(sg), | ||
| 311 | }) | ||
| 285 | upsert_netease_singers(pg_cur, singer_rows) | 312 | upsert_netease_singers(pg_cur, singer_rows) |
| 286 | 313 | ||
| 287 | singers_json = json.dumps([{ | 314 | singers_json = json.dumps([{ |
| ... | @@ -312,6 +339,18 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u | ... | @@ -312,6 +339,18 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u |
| 312 | 'type': sp.get('album_type') or '', 'company_id': sp.get('company_id') or 0, | 339 | 'type': sp.get('album_type') or '', 'company_id': sp.get('company_id') or 0, |
| 313 | 'company': sp.get('company') or '', 'is_owner': sp.get('is_owner') or 0, | 340 | 'company': sp.get('company') or '', 'is_owner': sp.get('is_owner') or 0, |
| 314 | 'published_at': sp.get('album_published_at'), | 341 | 'published_at': sp.get('album_published_at'), |
| 342 | 'provider_name': PROVIDER_YINYAN, | ||
| 343 | 'crawler_source_data': _source_json({ | ||
| 344 | 'id': sp.get('album_id'), | ||
| 345 | 'cover': sp.get('album_cover'), | ||
| 346 | 'title': sp.get('album_title'), | ||
| 347 | 'intro': sp.get('album_intro'), | ||
| 348 | 'type': sp.get('album_type'), | ||
| 349 | 'company_id': sp.get('company_id'), | ||
| 350 | 'company': sp.get('company'), | ||
| 351 | 'is_owner': sp.get('is_owner'), | ||
| 352 | 'published_at': sp.get('album_published_at'), | ||
| 353 | }), | ||
| 315 | }]) | 354 | }]) |
| 316 | 355 | ||
| 317 | song_uuid = str(uuid.uuid4()) | 356 | song_uuid = str(uuid.uuid4()) |
| ... | @@ -332,6 +371,8 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u | ... | @@ -332,6 +371,8 @@ def _process_netease(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_u |
| 332 | 'platform_index_url': sp.get('platform_index_url') or f'https://music.163.com/#/song?id={song_id}', | 371 | 'platform_index_url': sp.get('platform_index_url') or f'https://music.163.com/#/song?id={song_id}', |
| 333 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), | 372 | 'published_at': sp.get('published_at') or hk_row.get('issue_time'), |
| 334 | 'singers_json': singers_json, | 373 | 'singers_json': singers_json, |
| 374 | 'provider_name': PROVIDER_YINYAN, | ||
| 375 | 'crawler_source_data': _source_json(sp), | ||
| 335 | }]) | 376 | }]) |
| 336 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) | 377 | # 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID) |
| 337 | pg_cur.execute('SELECT id FROM crawler_netease_songs WHERE platform_song_id = %s', (song_id,)) | 378 | pg_cur.execute('SELECT id FROM crawler_netease_songs WHERE platform_song_id = %s', (song_id,)) | ... | ... |
| ... | @@ -25,14 +25,17 @@ def upsert_qq_singers(cur, singers: list[dict]) -> None: | ... | @@ -25,14 +25,17 @@ def upsert_qq_singers(cur, singers: list[dict]) -> None: |
| 25 | return | 25 | return |
| 26 | sql = """ | 26 | sql = """ |
| 27 | INSERT INTO crawler_qqmusic_singers | 27 | INSERT INTO crawler_qqmusic_singers |
| 28 | (id, mid, name, avatar, sex, area, "index", intro, home_url, created_at, updated_at) | 28 | (id, mid, name, avatar, sex, area, "index", intro, home_url, |
| 29 | VALUES (%s, %s, %s, %s, %s::singer_sex, %s::singer_area, %s::singer_index, %s, %s, NOW(), NOW()) | 29 | created_at, updated_at, provider_name, crawler_source_data) |
| 30 | VALUES (%s, %s, %s, %s, %s::singer_sex, %s::singer_area, %s::singer_index, %s, %s, | ||
| 31 | NOW(), NOW(), %s, %s::json) | ||
| 30 | ON CONFLICT (id) DO NOTHING | 32 | ON CONFLICT (id) DO NOTHING |
| 31 | """ | 33 | """ |
| 32 | rows = [( | 34 | rows = [( |
| 33 | s['id'], s['mid'], s['name'], s.get('avatar', ''), | 35 | s['id'], s['mid'], s['name'], s.get('avatar', ''), |
| 34 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', | 36 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', |
| 35 | s.get('intro'), s.get('home_url'), | 37 | s.get('intro'), s.get('home_url'), |
| 38 | s.get('provider_name'), s.get('crawler_source_data'), | ||
| 36 | ) for s in singers] | 39 | ) for s in singers] |
| 37 | cur.executemany(sql, rows) | 40 | cur.executemany(sql, rows) |
| 38 | 41 | ||
| ... | @@ -42,14 +45,16 @@ def upsert_qq_albums(cur, albums: list[dict]) -> None: | ... | @@ -42,14 +45,16 @@ def upsert_qq_albums(cur, albums: list[dict]) -> None: |
| 42 | return | 45 | return |
| 43 | sql = """ | 46 | sql = """ |
| 44 | INSERT INTO crawler_qqmusic_albums | 47 | INSERT INTO crawler_qqmusic_albums |
| 45 | (id, mid, cover, title, intro, type, company_id, company, is_owner, published_at, created_at, updated_at) | 48 | (id, mid, cover, title, intro, type, company_id, company, is_owner, published_at, |
| 46 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | 49 | created_at, updated_at, provider_name, crawler_source_data) |
| 50 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW(), %s, %s::json) | ||
| 47 | ON CONFLICT (id) DO NOTHING | 51 | ON CONFLICT (id) DO NOTHING |
| 48 | """ | 52 | """ |
| 49 | rows = [( | 53 | rows = [( |
| 50 | a['id'], a.get('mid', ''), a.get('cover', ''), a.get('title', ''), | 54 | a['id'], a.get('mid', ''), a.get('cover', ''), a.get('title', ''), |
| 51 | a.get('intro'), a.get('type', ''), a.get('company_id', 0) or 0, | 55 | a.get('intro'), a.get('type', ''), a.get('company_id', 0) or 0, |
| 52 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), | 56 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), |
| 57 | a.get('provider_name'), a.get('crawler_source_data'), | ||
| 53 | ) for a in albums] | 58 | ) for a in albums] |
| 54 | cur.executemany(sql, rows) | 59 | cur.executemany(sql, rows) |
| 55 | 60 | ||
| ... | @@ -61,8 +66,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: | ... | @@ -61,8 +66,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: |
| 61 | INSERT INTO crawler_qqmusic_songs | 66 | INSERT INTO crawler_qqmusic_songs |
| 62 | (id, platform_song_id, mid, album_id, cover, title, name, duration, | 67 | (id, platform_song_id, mid, album_id, cover, title, name, duration, |
| 63 | lyric, composer_name, lyricist_name, url, lyric_url, | 68 | lyric, composer_name, lyricist_name, url, lyric_url, |
| 64 | platform_index_url, published_at, singers, status, created_at, updated_at) | 69 | platform_index_url, published_at, singers, status, created_at, updated_at, |
| 65 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW()) | 70 | provider_name, crawler_source_data) |
| 71 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW(), %s, %s::json) | ||
| 66 | ON CONFLICT (platform_song_id) DO NOTHING | 72 | ON CONFLICT (platform_song_id) DO NOTHING |
| 67 | """ | 73 | """ |
| 68 | rows = [( | 74 | rows = [( |
| ... | @@ -71,6 +77,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: | ... | @@ -71,6 +77,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: |
| 71 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | 77 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), |
| 72 | s.get('url', ''), s.get('lyric_url'), | 78 | s.get('url', ''), s.get('lyric_url'), |
| 73 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), | 79 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), |
| 80 | s.get('provider_name'), s.get('crawler_source_data'), | ||
| 74 | ) for s in songs] | 81 | ) for s in songs] |
| 75 | cur.executemany(sql, rows) | 82 | cur.executemany(sql, rows) |
| 76 | 83 | ||
| ... | @@ -195,14 +202,17 @@ def upsert_netease_singers(cur, singers: list[dict]) -> None: | ... | @@ -195,14 +202,17 @@ def upsert_netease_singers(cur, singers: list[dict]) -> None: |
| 195 | return | 202 | return |
| 196 | sql = """ | 203 | sql = """ |
| 197 | INSERT INTO crawler_netease_singers | 204 | INSERT INTO crawler_netease_singers |
| 198 | (id, name, avatar, sex, area, "index", intro, home_url, created_at, updated_at) | 205 | (id, name, avatar, sex, area, "index", intro, home_url, |
| 199 | VALUES (%s, %s, %s, %s::netease_singer_sex, %s::netease_singer_area, %s::netease_singer_index, %s, %s, NOW(), NOW()) | 206 | created_at, updated_at, provider_name, crawler_source_data) |
| 207 | VALUES (%s, %s, %s, %s::netease_singer_sex, %s::netease_singer_area, %s::netease_singer_index, %s, %s, | ||
| 208 | NOW(), NOW(), %s, %s::json) | ||
| 200 | ON CONFLICT (id) DO NOTHING | 209 | ON CONFLICT (id) DO NOTHING |
| 201 | """ | 210 | """ |
| 202 | rows = [( | 211 | rows = [( |
| 203 | s['id'], s['name'], s.get('avatar', ''), | 212 | s['id'], s['name'], s.get('avatar', ''), |
| 204 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', | 213 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', |
| 205 | s.get('intro'), s.get('home_url'), | 214 | s.get('intro'), s.get('home_url'), |
| 215 | s.get('provider_name'), s.get('crawler_source_data'), | ||
| 206 | ) for s in singers] | 216 | ) for s in singers] |
| 207 | cur.executemany(sql, rows) | 217 | cur.executemany(sql, rows) |
| 208 | 218 | ||
| ... | @@ -212,14 +222,16 @@ def upsert_netease_albums(cur, albums: list[dict]) -> None: | ... | @@ -212,14 +222,16 @@ def upsert_netease_albums(cur, albums: list[dict]) -> None: |
| 212 | return | 222 | return |
| 213 | sql = """ | 223 | sql = """ |
| 214 | INSERT INTO crawler_netease_albums | 224 | INSERT INTO crawler_netease_albums |
| 215 | (id, cover, title, intro, type, company_id, company, is_owner, published_at, created_at, updated_at) | 225 | (id, cover, title, intro, type, company_id, company, is_owner, published_at, |
| 216 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | 226 | created_at, updated_at, provider_name, crawler_source_data) |
| 227 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW(), %s, %s::json) | ||
| 217 | ON CONFLICT (id) DO NOTHING | 228 | ON CONFLICT (id) DO NOTHING |
| 218 | """ | 229 | """ |
| 219 | rows = [( | 230 | rows = [( |
| 220 | a['id'], a.get('cover', ''), a.get('title', ''), a.get('intro'), | 231 | a['id'], a.get('cover', ''), a.get('title', ''), a.get('intro'), |
| 221 | a.get('type', ''), a.get('company_id', 0) or 0, | 232 | a.get('type', ''), a.get('company_id', 0) or 0, |
| 222 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), | 233 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), |
| 234 | a.get('provider_name'), a.get('crawler_source_data'), | ||
| 223 | ) for a in albums] | 235 | ) for a in albums] |
| 224 | cur.executemany(sql, rows) | 236 | cur.executemany(sql, rows) |
| 225 | 237 | ||
| ... | @@ -231,8 +243,9 @@ def upsert_netease_songs(cur, songs: list[dict]) -> None: | ... | @@ -231,8 +243,9 @@ def upsert_netease_songs(cur, songs: list[dict]) -> None: |
| 231 | INSERT INTO crawler_netease_songs | 243 | INSERT INTO crawler_netease_songs |
| 232 | (id, platform_song_id, album_id, cover, title, name, duration, | 244 | (id, platform_song_id, album_id, cover, title, name, duration, |
| 233 | lyric, composer_name, lyricist_name, url, lyric_url, | 245 | lyric, composer_name, lyricist_name, url, lyric_url, |
| 234 | platform_index_url, published_at, album, singers, status, created_at, updated_at) | 246 | platform_index_url, published_at, album, singers, status, created_at, updated_at, |
| 235 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::json, %s::jsonb, 0, NOW(), NOW()) | 247 | provider_name, crawler_source_data) |
| 248 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::json, %s::jsonb, 0, NOW(), NOW(), %s, %s::json) | ||
| 236 | ON CONFLICT (platform_song_id) DO NOTHING | 249 | ON CONFLICT (platform_song_id) DO NOTHING |
| 237 | """ | 250 | """ |
| 238 | rows = [( | 251 | rows = [( |
| ... | @@ -241,6 +254,7 @@ def upsert_netease_songs(cur, songs: list[dict]) -> None: | ... | @@ -241,6 +254,7 @@ def upsert_netease_songs(cur, songs: list[dict]) -> None: |
| 241 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | 254 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), |
| 242 | s.get('url', ''), s.get('lyric_url'), | 255 | s.get('url', ''), s.get('lyric_url'), |
| 243 | s.get('platform_index_url'), s.get('published_at'), s.get('album_json'), s.get('singers_json', '[]'), | 256 | s.get('platform_index_url'), s.get('published_at'), s.get('album_json'), s.get('singers_json', '[]'), |
| 257 | s.get('provider_name'), s.get('crawler_source_data'), | ||
| 244 | ) for s in songs] | 258 | ) for s in songs] |
| 245 | cur.executemany(sql, rows) | 259 | cur.executemany(sql, rows) |
| 246 | 260 | ... | ... |
| ... | @@ -3,8 +3,12 @@ from etl_to_crawler.writer import ( | ... | @@ -3,8 +3,12 @@ from etl_to_crawler.writer import ( |
| 3 | upsert_kugou_albums, | 3 | upsert_kugou_albums, |
| 4 | upsert_kugou_singers, | 4 | upsert_kugou_singers, |
| 5 | upsert_kugou_songs, | 5 | upsert_kugou_songs, |
| 6 | upsert_netease_albums, | ||
| 7 | upsert_netease_singers, | ||
| 6 | upsert_netease_songs, | 8 | upsert_netease_songs, |
| 9 | upsert_qq_albums, | ||
| 7 | upsert_qq_singers, | 10 | upsert_qq_singers, |
| 11 | upsert_qq_songs, | ||
| 8 | upsert_qq_singer_songs, | 12 | upsert_qq_singer_songs, |
| 9 | upsert_yinyan_song_records, | 13 | upsert_yinyan_song_records, |
| 10 | ) | 14 | ) |
| ... | @@ -75,7 +79,7 @@ def test_upsert_netease_songs_writes_album_json_column(): | ... | @@ -75,7 +79,7 @@ def test_upsert_netease_songs_writes_album_json_column(): |
| 75 | sql, rows = cur.executemany.call_args[0] | 79 | sql, rows = cur.executemany.call_args[0] |
| 76 | assert 'album, singers' in sql | 80 | assert 'album, singers' in sql |
| 77 | assert '%s::json, %s::jsonb' in sql | 81 | assert '%s::json, %s::jsonb' in sql |
| 78 | assert rows[0][-2] == '{"id": 20, "title": "专辑"}' | 82 | assert rows[0][14] == '{"id": 20, "title": "专辑"}' |
| 79 | 83 | ||
| 80 | 84 | ||
| 81 | def test_upsert_kugou_songs_writes_provider_and_source_data(): | 85 | def test_upsert_kugou_songs_writes_provider_and_source_data(): |
| ... | @@ -98,14 +102,14 @@ def test_upsert_kugou_songs_writes_provider_and_source_data(): | ... | @@ -98,14 +102,14 @@ def test_upsert_kugou_songs_writes_provider_and_source_data(): |
| 98 | 'platform_index_url': 'https://www.kugou.com/song/#hash=hash', | 102 | 'platform_index_url': 'https://www.kugou.com/song/#hash=hash', |
| 99 | 'published_at': '2020-01-01', | 103 | 'published_at': '2020-01-01', |
| 100 | 'singers_json': '[]', | 104 | 'singers_json': '[]', |
| 101 | 'provider_name': 'kugou', | 105 | 'provider_name': 'yinyan', |
| 102 | 'crawler_source_data': '{"id": 200}', | 106 | 'crawler_source_data': '{"id": 200}', |
| 103 | }]) | 107 | }]) |
| 104 | 108 | ||
| 105 | sql, rows = cur.executemany.call_args[0] | 109 | sql, rows = cur.executemany.call_args[0] |
| 106 | assert 'provider_name, crawler_source_data' in sql | 110 | assert 'provider_name, crawler_source_data' in sql |
| 107 | assert '%s::json' in sql | 111 | assert '%s::json' in sql |
| 108 | assert rows[0][-2:] == ('kugou', '{"id": 200}') | 112 | assert rows[0][-2:] == ('yinyan', '{"id": 200}') |
| 109 | 113 | ||
| 110 | 114 | ||
| 111 | def test_upsert_kugou_singers_writes_provider_and_source_data(): | 115 | def test_upsert_kugou_singers_writes_provider_and_source_data(): |
| ... | @@ -119,13 +123,13 @@ def test_upsert_kugou_singers_writes_provider_and_source_data(): | ... | @@ -119,13 +123,13 @@ def test_upsert_kugou_singers_writes_provider_and_source_data(): |
| 119 | 'index': 'G', | 123 | 'index': 'G', |
| 120 | 'intro': '简介', | 124 | 'intro': '简介', |
| 121 | 'home_url': 'https://example.com/singer', | 125 | 'home_url': 'https://example.com/singer', |
| 122 | 'provider_name': 'kugou', | 126 | 'provider_name': 'yinyan', |
| 123 | 'crawler_source_data': '{"id": 1}', | 127 | 'crawler_source_data': '{"id": 1}', |
| 124 | }]) | 128 | }]) |
| 125 | 129 | ||
| 126 | sql, rows = cur.executemany.call_args[0] | 130 | sql, rows = cur.executemany.call_args[0] |
| 127 | assert 'provider_name, crawler_source_data' in sql | 131 | assert 'provider_name, crawler_source_data' in sql |
| 128 | assert rows[0][-2:] == ('kugou', '{"id": 1}') | 132 | assert rows[0][-2:] == ('yinyan', '{"id": 1}') |
| 129 | 133 | ||
| 130 | 134 | ||
| 131 | def test_upsert_kugou_albums_writes_provider_and_source_data(): | 135 | def test_upsert_kugou_albums_writes_provider_and_source_data(): |
| ... | @@ -140,10 +144,136 @@ def test_upsert_kugou_albums_writes_provider_and_source_data(): | ... | @@ -140,10 +144,136 @@ def test_upsert_kugou_albums_writes_provider_and_source_data(): |
| 140 | 'company': '唱片公司', | 144 | 'company': '唱片公司', |
| 141 | 'is_owner': 1, | 145 | 'is_owner': 1, |
| 142 | 'published_at': '2020-01-01', | 146 | 'published_at': '2020-01-01', |
| 143 | 'provider_name': 'kugou', | 147 | 'provider_name': 'yinyan', |
| 144 | 'crawler_source_data': '{"id": 20}', | 148 | 'crawler_source_data': '{"id": 20}', |
| 145 | }]) | 149 | }]) |
| 146 | 150 | ||
| 147 | sql, rows = cur.executemany.call_args[0] | 151 | sql, rows = cur.executemany.call_args[0] |
| 148 | assert 'provider_name, crawler_source_data' in sql | 152 | assert 'provider_name, crawler_source_data' in sql |
| 149 | assert rows[0][-2:] == ('kugou', '{"id": 20}') | 153 | assert rows[0][-2:] == ('yinyan', '{"id": 20}') |
| 154 | |||
| 155 | |||
| 156 | def test_upsert_qq_entities_write_provider_and_source_data(): | ||
| 157 | cur = MagicMock() | ||
| 158 | upsert_qq_singers(cur, [{ | ||
| 159 | 'id': 1, | ||
| 160 | 'mid': 'singer-mid', | ||
| 161 | 'name': '歌手', | ||
| 162 | 'avatar': 'https://example.com/avatar.jpg', | ||
| 163 | 'sex': 'M', | ||
| 164 | 'area': '华语', | ||
| 165 | 'index': 'G', | ||
| 166 | 'intro': '简介', | ||
| 167 | 'home_url': 'https://example.com/singer', | ||
| 168 | 'provider_name': 'yinyan', | ||
| 169 | 'crawler_source_data': '{"id": 1}', | ||
| 170 | }]) | ||
| 171 | sql, rows = cur.executemany.call_args[0] | ||
| 172 | assert 'provider_name, crawler_source_data' in sql | ||
| 173 | assert rows[0][-2:] == ('yinyan', '{"id": 1}') | ||
| 174 | |||
| 175 | cur = MagicMock() | ||
| 176 | upsert_qq_albums(cur, [{ | ||
| 177 | 'id': 20, | ||
| 178 | 'mid': 'album-mid', | ||
| 179 | 'cover': 'https://example.com/album.jpg', | ||
| 180 | 'title': '专辑', | ||
| 181 | 'intro': '简介', | ||
| 182 | 'type': '专辑类型', | ||
| 183 | 'company_id': 7, | ||
| 184 | 'company': '唱片公司', | ||
| 185 | 'is_owner': 1, | ||
| 186 | 'published_at': '2020-01-01', | ||
| 187 | 'provider_name': 'yinyan', | ||
| 188 | 'crawler_source_data': '{"id": 20}', | ||
| 189 | }]) | ||
| 190 | sql, rows = cur.executemany.call_args[0] | ||
| 191 | assert 'provider_name, crawler_source_data' in sql | ||
| 192 | assert rows[0][-2:] == ('yinyan', '{"id": 20}') | ||
| 193 | |||
| 194 | cur = MagicMock() | ||
| 195 | upsert_qq_songs(cur, [{ | ||
| 196 | 'song_uuid': 'song-uuid', | ||
| 197 | 'platform_song_id': 200, | ||
| 198 | 'mid': 'song-mid', | ||
| 199 | 'album_id': 20, | ||
| 200 | 'cover': 'https://example.com/cover.jpg', | ||
| 201 | 'title': '标题', | ||
| 202 | 'name': '词曲名', | ||
| 203 | 'duration': 180, | ||
| 204 | 'lyric': '歌词', | ||
| 205 | 'composer_name': '曲作者', | ||
| 206 | 'lyricist_name': '词作者', | ||
| 207 | 'url': 'https://example.com/audio.mp3', | ||
| 208 | 'lyric_url': 'https://example.com/lyric.lrc', | ||
| 209 | 'platform_index_url': 'https://y.qq.com/n/ryqq/songDetail/song-mid', | ||
| 210 | 'published_at': '2020-01-01', | ||
| 211 | 'singers_json': '[]', | ||
| 212 | 'provider_name': 'yinyan', | ||
| 213 | 'crawler_source_data': '{"id": 200}', | ||
| 214 | }]) | ||
| 215 | sql, rows = cur.executemany.call_args[0] | ||
| 216 | assert 'provider_name, crawler_source_data' in sql | ||
| 217 | assert rows[0][-2:] == ('yinyan', '{"id": 200}') | ||
| 218 | |||
| 219 | |||
| 220 | def test_upsert_netease_entities_write_provider_and_source_data(): | ||
| 221 | cur = MagicMock() | ||
| 222 | upsert_netease_singers(cur, [{ | ||
| 223 | 'id': 1, | ||
| 224 | 'name': '歌手', | ||
| 225 | 'avatar': 'https://example.com/avatar.jpg', | ||
| 226 | 'sex': 'M', | ||
| 227 | 'area': '华语', | ||
| 228 | 'index': 'G', | ||
| 229 | 'intro': '简介', | ||
| 230 | 'home_url': 'https://example.com/singer', | ||
| 231 | 'provider_name': 'yinyan', | ||
| 232 | 'crawler_source_data': '{"id": 1}', | ||
| 233 | }]) | ||
| 234 | sql, rows = cur.executemany.call_args[0] | ||
| 235 | assert 'provider_name, crawler_source_data' in sql | ||
| 236 | assert rows[0][-2:] == ('yinyan', '{"id": 1}') | ||
| 237 | |||
| 238 | cur = MagicMock() | ||
| 239 | upsert_netease_albums(cur, [{ | ||
| 240 | 'id': 20, | ||
| 241 | 'cover': 'https://example.com/album.jpg', | ||
| 242 | 'title': '专辑', | ||
| 243 | 'intro': '简介', | ||
| 244 | 'type': '专辑类型', | ||
| 245 | 'company_id': 7, | ||
| 246 | 'company': '唱片公司', | ||
| 247 | 'is_owner': 1, | ||
| 248 | 'published_at': '2020-01-01', | ||
| 249 | 'provider_name': 'yinyan', | ||
| 250 | 'crawler_source_data': '{"id": 20}', | ||
| 251 | }]) | ||
| 252 | sql, rows = cur.executemany.call_args[0] | ||
| 253 | assert 'provider_name, crawler_source_data' in sql | ||
| 254 | assert rows[0][-2:] == ('yinyan', '{"id": 20}') | ||
| 255 | |||
| 256 | cur = MagicMock() | ||
| 257 | upsert_netease_songs(cur, [{ | ||
| 258 | 'song_uuid': 'song-uuid', | ||
| 259 | 'platform_song_id': 300, | ||
| 260 | 'album_id': 20, | ||
| 261 | 'album_json': '{"id": 20, "title": "专辑"}', | ||
| 262 | 'cover': 'https://example.com/cover.jpg', | ||
| 263 | 'title': '标题', | ||
| 264 | 'name': '词曲名', | ||
| 265 | 'duration': 180, | ||
| 266 | 'lyric': '歌词', | ||
| 267 | 'composer_name': '曲作者', | ||
| 268 | 'lyricist_name': '词作者', | ||
| 269 | 'url': 'https://example.com/audio.mp3', | ||
| 270 | 'lyric_url': 'https://example.com/lyric.lrc', | ||
| 271 | 'platform_index_url': 'https://music.163.com/#/song?id=300', | ||
| 272 | 'published_at': '2020-01-01', | ||
| 273 | 'singers_json': '[]', | ||
| 274 | 'provider_name': 'yinyan', | ||
| 275 | 'crawler_source_data': '{"id": 300}', | ||
| 276 | }]) | ||
| 277 | sql, rows = cur.executemany.call_args[0] | ||
| 278 | assert 'provider_name, crawler_source_data' in sql | ||
| 279 | assert rows[0][-2:] == ('yinyan', '{"id": 300}') | ... | ... |
-
Please register or sign in to post a comment