Commit d8728ac1 d8728ac17dd31f351ace0b3558854dd5e396e17d by 沈秋雨

refactor(etl_to_crawler): 优化QQ音乐入库流程并新增album_json字段

- 将BATCH_SIZE由4调整为1以支持更细粒度的数据处理
- 在runner中为QQ音乐歌曲构建完整album_json对象并序列化
- 修改upsert_qq_songs接口,新增album_json参数的传递和入库支持
- 在writer模块SQL插入语句中添加album字段,支持json类型数据写入
- 编写单元测试验证album_json字段正确生成与写入逻辑
- 兼容旧逻辑,album_json字段为空时不影响其他数据写入
1 parent c403b29e
...@@ -43,4 +43,4 @@ PLATFORM_KUGOU = '2' ...@@ -43,4 +43,4 @@ PLATFORM_KUGOU = '2'
43 PLATFORM_NETEASE = '4' 43 PLATFORM_NETEASE = '4'
44 PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE] 44 PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE]
45 45
46 BATCH_SIZE = 4 46 BATCH_SIZE = 1
......
...@@ -133,8 +133,22 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): ...@@ -133,8 +133,22 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url):
133 133
134 # 写入 album 134 # 写入 album
135 album_id = None 135 album_id = None
136 album_json = None
136 if sp.get('album_id'): 137 if sp.get('album_id'):
137 album_id = sp['album_id'] 138 album_id = sp['album_id']
139 album_payload = {
140 'id': sp['album_id'],
141 'mid': sp.get('album_mid') or '',
142 'cover': album_cover,
143 'title': sp.get('album_title') or '',
144 'intro': sp.get('album_intro'),
145 'type': sp.get('album_type') or '',
146 'company_id': sp.get('company_id') or 0,
147 'company': sp.get('company') or '',
148 'is_owner': sp.get('is_owner') or 0,
149 'published_at': str(sp.get('album_published_at')) if sp.get('album_published_at') else None,
150 }
151 album_json = json.dumps(album_payload, ensure_ascii=False)
138 upsert_qq_albums(pg_cur, [{ 152 upsert_qq_albums(pg_cur, [{
139 'id': sp['album_id'], 'mid': sp.get('album_mid') or '', 153 'id': sp['album_id'], 'mid': sp.get('album_mid') or '',
140 'cover': album_cover, 'title': sp.get('album_title') or '', 154 'cover': album_cover, 'title': sp.get('album_title') or '',
...@@ -167,6 +181,7 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): ...@@ -167,6 +181,7 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url):
167 'platform_song_id': platform_song_id, 181 'platform_song_id': platform_song_id,
168 'mid': mid, 182 'mid': mid,
169 'album_id': album_id, 183 'album_id': album_id,
184 'album_json': album_json,
170 'cover': cover_url, 185 'cover': cover_url,
171 'title': title, 186 'title': title,
172 'version': version, 187 'version': version,
......
...@@ -138,9 +138,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: ...@@ -138,9 +138,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None:
138 INSERT INTO crawler_qqmusic_songs 138 INSERT INTO crawler_qqmusic_songs
139 (id, platform_song_id, mid, album_id, cover, title, name, duration, 139 (id, platform_song_id, mid, album_id, cover, title, name, duration,
140 lyric, composer_name, lyricist_name, url, lyric_url, 140 lyric, composer_name, lyricist_name, url, lyric_url,
141 platform_index_url, published_at, singers, status, created_at, updated_at, 141 platform_index_url, published_at, album, singers, status, created_at, updated_at,
142 version, audio_md5, provider_name, crawler_source_data) 142 version, audio_md5, provider_name, crawler_source_data)
143 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW(), %s, %s, %s, %s::json) 143 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::json, %s::jsonb, 0, NOW(), NOW(), %s, %s, %s, %s::json)
144 ON CONFLICT (platform_song_id) DO NOTHING 144 ON CONFLICT (platform_song_id) DO NOTHING
145 """ 145 """
146 rows = [( 146 rows = [(
...@@ -148,7 +148,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: ...@@ -148,7 +148,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None:
148 s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, 148 s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0,
149 s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), 149 s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'),
150 s.get('url', ''), s.get('lyric_url'), 150 s.get('url', ''), s.get('lyric_url'),
151 s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), 151 s.get('platform_index_url'), s.get('published_at'), s.get('album_json'), s.get('singers_json', '[]'),
152 s.get('version'), 152 s.get('version'),
153 s.get('audio_md5'), 153 s.get('audio_md5'),
154 s.get('provider_name'), s.get('crawler_source_data'), 154 s.get('provider_name'), s.get('crawler_source_data'),
......
...@@ -169,6 +169,70 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa ...@@ -169,6 +169,70 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa
169 assert pg_conn.commits == 1 169 assert pg_conn.commits == 1
170 170
171 171
172 def test_process_qq_builds_album_json_for_song_insert(monkeypatch):
173 pg_cur = MagicMock()
174 pg_cur.fetchone.return_value = ('song-uuid',)
175 inserted_songs = []
176
177 monkeypatch.setattr(runner, 'fetch_qq_songs', lambda conn, mids: {
178 'qq-mid': {
179 'id': 200,
180 'mid': 'qq-mid',
181 'album_id': 20,
182 'album_mid': 'album-mid',
183 'album_cover': 'https://example.com/album.jpg',
184 'album_title': '专辑',
185 'album_intro': '简介',
186 'album_type': '专辑类型',
187 'company_id': 7,
188 'company': '唱片公司',
189 'is_owner': 1,
190 'album_published_at': '2020-01-01',
191 'cover': 'https://example.com/cover.jpg',
192 'title': '化风行万里 (DJ默涵版)',
193 'duration': 180,
194 'lyric': '[00:01.00]歌词',
195 'composer_name': '曲作者',
196 'lyricist_name': '词作者',
197 'platform_index_url': None,
198 'published_at': '2020-01-02',
199 },
200 })
201 monkeypatch.setattr(runner, 'fetch_qq_singers', lambda conn, song_ids: {})
202 monkeypatch.setattr(runner, '_safe_transfer', lambda url, oss_key, bucket, base_url: url)
203 monkeypatch.setattr(runner, '_safe_transfer_audio', lambda url, oss_key, bucket, base_url: (url, 'audio-md5'))
204 monkeypatch.setattr(runner, 'upsert_qq_singers', lambda cur, singers: None)
205 monkeypatch.setattr(runner, 'upsert_qq_albums', lambda cur, albums: None)
206 monkeypatch.setattr(runner, 'upsert_qq_songs', lambda cur, songs: inserted_songs.extend(songs))
207 monkeypatch.setattr(runner, 'upsert_qq_singer_songs', lambda cur, pairs: None)
208 monkeypatch.setattr(runner, 'upsert_qq_singer_albums', lambda cur, pairs: None)
209
210 runner._process_qq(
211 {
212 'name': '词曲名',
213 'audio_url': 'https://example.com/audio.mp3',
214 'lyrics_url': 'https://example.com/lyric.lrc',
215 'cover_url': '',
216 'composer': '词曲曲作者',
217 'lyricist': '词曲词作者',
218 'issue_time': '2019-01-01',
219 'song_time': 120,
220 },
221 {'platform_unique_key': 'qq-mid', 'platform_mid': '200'},
222 spider_conn=object(),
223 pg_cur=pg_cur,
224 bucket=object(),
225 base_url='https://bucket.example.com',
226 )
227
228 assert inserted_songs[0]['album_json']
229 assert '"id": 20' in inserted_songs[0]['album_json']
230 assert '"mid": "album-mid"' in inserted_songs[0]['album_json']
231 assert '"title": "专辑"' in inserted_songs[0]['album_json']
232 assert inserted_songs[0]['title'] == '化风行万里'
233 assert inserted_songs[0]['version'] == 'DJ默涵版'
234
235
172 def test_process_netease_builds_album_json_for_song_insert(monkeypatch): 236 def test_process_netease_builds_album_json_for_song_insert(monkeypatch):
173 pg_cur = MagicMock() 237 pg_cur = MagicMock()
174 pg_cur.fetchone.return_value = ('song-uuid',) 238 pg_cur.fetchone.return_value = ('song-uuid',)
......
...@@ -130,6 +130,34 @@ def test_upsert_netease_songs_writes_album_json_column(): ...@@ -130,6 +130,34 @@ def test_upsert_netease_songs_writes_album_json_column():
130 assert rows[0][14] == '{"id": 20, "title": "专辑"}' 130 assert rows[0][14] == '{"id": 20, "title": "专辑"}'
131 131
132 132
133 def test_upsert_qq_songs_writes_album_json_column():
134 cur = MagicMock()
135 upsert_qq_songs(cur, [{
136 'song_uuid': 'song-uuid',
137 'platform_song_id': 200,
138 'mid': 'song-mid',
139 'album_id': 20,
140 'album_json': '{"id": 20, "title": "专辑"}',
141 'cover': 'https://example.com/cover.jpg',
142 'title': '标题',
143 'name': '词曲名',
144 'duration': 180,
145 'lyric': '歌词',
146 'composer_name': '曲作者',
147 'lyricist_name': '词作者',
148 'url': 'https://example.com/audio.mp3',
149 'lyric_url': 'https://example.com/lyric.lrc',
150 'platform_index_url': 'https://y.qq.com/n/ryqq/songDetail/song-mid',
151 'published_at': '2020-01-01',
152 'singers_json': '[]',
153 }])
154
155 sql, rows = cur.executemany.call_args[0]
156 assert 'album, singers' in sql
157 assert '%s::json, %s::jsonb' in sql
158 assert rows[0][15] == '{"id": 20, "title": "专辑"}'
159
160
133 def test_upsert_kugou_songs_writes_provider_and_source_data(): 161 def test_upsert_kugou_songs_writes_provider_and_source_data():
134 cur = MagicMock() 162 cur = MagicMock()
135 upsert_kugou_songs(cur, [{ 163 upsert_kugou_songs(cur, [{
......