Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
沈秋雨
/
import_hk_songs
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit
55c6e4fb
...
55c6e4fb71cc09df0cb5d43383bbb10b754ab5ca
authored
2026-07-13 14:22:42 +0800
by
沈秋雨
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
fix(etl_to_crawler): 解决同批次歌曲UUID冲突及关联关系处理问题
1 parent
c41b71ef
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
143 additions
and
6 deletions
etl_to_crawler/runner.py
tests/test_runner.py
etl_to_crawler/runner.py
View file @
55c6e4f
...
...
@@ -442,7 +442,8 @@ def _prepare_qq_payload(hk_row: dict, pr: dict, bucket, base_url, sp: dict, sing
'singers'
:
singer_rows
,
'albums'
:
album_rows
,
'songs'
:
[
song_row
],
'singer_songs'
:
[(
sg
[
'singer_id'
],
song_uuid
)
for
sg
in
singer_list
],
# 关联阶段统一按平台歌曲 ID 解析数据库中的实际 UUID,兼容 songs 冲突跳过。
'singer_songs'
:
[(
sg
[
'singer_id'
],
platform_song_id
)
for
sg
in
singer_list
],
'singer_albums'
:
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
]
if
album_id
else
[],
}
...
...
@@ -554,7 +555,7 @@ def _prepare_kugou_payload(hk_row: dict, pr: dict, bucket, base_url, sp: dict, s
'singers'
:
singer_rows
,
'albums'
:
album_rows
,
'songs'
:
[
song_row
],
'singer_songs'
:
[(
sg
[
'singer_id'
],
song_
uu
id
)
for
sg
in
singer_list
],
'singer_songs'
:
[(
sg
[
'singer_id'
],
song_id
)
for
sg
in
singer_list
],
'singer_albums'
:
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
]
if
album_id
else
[],
}
...
...
@@ -678,7 +679,7 @@ def _prepare_netease_payload(hk_row: dict, pr: dict, bucket, base_url, sp: dict,
'singers'
:
singer_rows
,
'albums'
:
album_rows
,
'songs'
:
[
song_row
],
'singer_songs'
:
[(
sg
[
'singer_id'
],
song_
uu
id
)
for
sg
in
singer_list
],
'singer_songs'
:
[(
sg
[
'singer_id'
],
song_id
)
for
sg
in
singer_list
],
'singer_albums'
:
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
]
if
album_id
else
[],
}
...
...
@@ -731,6 +732,57 @@ def _extend_platform_payload(target: dict, payload: dict) -> None:
target
[
'singer_albums'
]
.
extend
(
payload
.
get
(
'singer_albums'
,
[]))
_CRAWLER_SONG_TABLES
=
{
'crawler_qqmusic_songs'
,
'crawler_kugou_songs'
,
'crawler_netease_songs'
,
}
def
_dedupe_songs_by_platform_id
(
songs
:
list
[
dict
])
->
list
[
dict
]:
"""同批相同平台歌曲只尝试插入一次,保留首次出现的数据。"""
seen
=
set
()
deduped
=
[]
for
song
in
songs
:
platform_song_id
=
int
(
song
[
'platform_song_id'
])
if
platform_song_id
not
in
seen
:
seen
.
add
(
platform_song_id
)
deduped
.
append
(
song
)
return
deduped
def
_resolve_singer_song_pairs
(
cur
,
songs_table
:
str
,
platform_pairs
:
list
[
tuple
[
int
,
int
]],
)
->
list
[
tuple
[
int
,
str
]]:
"""将 (singer_id, platform_song_id) 解析为关联表需要的实际歌曲 UUID。"""
if
not
platform_pairs
:
return
[]
if
songs_table
not
in
_CRAWLER_SONG_TABLES
:
raise
ValueError
(
f
'unsupported crawler songs table: {songs_table}'
)
platform_song_ids
=
sorted
({
int
(
platform_song_id
)
for
_
,
platform_song_id
in
platform_pairs
})
placeholders
=
', '
.
join
([
'
%
s'
]
*
len
(
platform_song_ids
))
cur
.
execute
(
f
'SELECT platform_song_id, id FROM {songs_table} '
f
'WHERE platform_song_id IN ({placeholders})'
,
tuple
(
platform_song_ids
),
)
actual_ids
=
{
int
(
row
[
0
]):
str
(
row
[
1
])
for
row
in
cur
.
fetchall
()}
missing
=
sorted
(
set
(
platform_song_ids
)
-
actual_ids
.
keys
())
if
missing
:
raise
RuntimeError
(
f
'cannot resolve actual song UUIDs from {songs_table}: platform_song_ids={missing}'
)
# dict 保序去重,同一歌手与同一实际歌曲只写一次关系。
return
list
(
dict
.
fromkeys
(
(
int
(
singer_id
),
actual_ids
[
int
(
platform_song_id
)])
for
singer_id
,
platform_song_id
in
platform_pairs
))
def
_write_import_payloads
(
pg_cur
,
payloads
:
list
[
dict
])
->
list
[
dict
]:
if
not
payloads
:
return
[]
...
...
@@ -747,24 +799,36 @@ def _write_import_payloads(pg_cur, payloads: list[dict]) -> list[dict]:
imported
.
append
(
payload
[
'result'
])
qq
=
grouped
[
PLATFORM_QQ
]
qq
[
'songs'
]
=
_dedupe_songs_by_platform_id
(
qq
[
'songs'
])
upsert_qq_singers
(
pg_cur
,
qq
[
'singers'
])
upsert_qq_albums
(
pg_cur
,
qq
[
'albums'
])
upsert_qq_songs
(
pg_cur
,
qq
[
'songs'
])
upsert_qq_singer_songs
(
pg_cur
,
qq
[
'singer_songs'
])
qq_singer_songs
=
_resolve_singer_song_pairs
(
pg_cur
,
'crawler_qqmusic_songs'
,
qq
[
'singer_songs'
],
)
upsert_qq_singer_songs
(
pg_cur
,
qq_singer_songs
)
upsert_qq_singer_albums
(
pg_cur
,
qq
[
'singer_albums'
])
kugou
=
grouped
[
PLATFORM_KUGOU
]
kugou
[
'songs'
]
=
_dedupe_songs_by_platform_id
(
kugou
[
'songs'
])
upsert_kugou_singers
(
pg_cur
,
kugou
[
'singers'
])
upsert_kugou_albums
(
pg_cur
,
kugou
[
'albums'
])
upsert_kugou_songs
(
pg_cur
,
kugou
[
'songs'
])
upsert_kugou_singer_songs
(
pg_cur
,
kugou
[
'singer_songs'
])
kugou_singer_songs
=
_resolve_singer_song_pairs
(
pg_cur
,
'crawler_kugou_songs'
,
kugou
[
'singer_songs'
],
)
upsert_kugou_singer_songs
(
pg_cur
,
kugou_singer_songs
)
upsert_kugou_singer_albums
(
pg_cur
,
kugou
[
'singer_albums'
])
netease
=
grouped
[
PLATFORM_NETEASE
]
netease
[
'songs'
]
=
_dedupe_songs_by_platform_id
(
netease
[
'songs'
])
upsert_netease_singers
(
pg_cur
,
netease
[
'singers'
])
upsert_netease_albums
(
pg_cur
,
netease
[
'albums'
])
upsert_netease_songs
(
pg_cur
,
netease
[
'songs'
])
upsert_netease_singer_songs
(
pg_cur
,
netease
[
'singer_songs'
])
netease_singer_songs
=
_resolve_singer_song_pairs
(
pg_cur
,
'crawler_netease_songs'
,
netease
[
'singer_songs'
],
)
upsert_netease_singer_songs
(
pg_cur
,
netease_singer_songs
)
upsert_netease_singer_albums
(
pg_cur
,
netease
[
'singer_albums'
])
upsert_yinyan_song_records
(
pg_cur
,
yinyan_records
)
...
...
tests/test_runner.py
View file @
55c6e4f
...
...
@@ -241,6 +241,79 @@ def test_records2_selection_accepts_current_record_with_empty_cover_and_lyric():
assert
reason
==
'ok'
@pytest.mark.parametrize
(
(
'platform'
,
'songs_table'
,
'song_upsert_name'
,
'relation_upsert_name'
),
[
(
runner
.
PLATFORM_QQ
,
'crawler_qqmusic_songs'
,
'upsert_qq_songs'
,
'upsert_qq_singer_songs'
),
(
runner
.
PLATFORM_KUGOU
,
'crawler_kugou_songs'
,
'upsert_kugou_songs'
,
'upsert_kugou_singer_songs'
),
(
runner
.
PLATFORM_NETEASE
,
'crawler_netease_songs'
,
'upsert_netease_songs'
,
'upsert_netease_singer_songs'
),
],
)
def
test_write_import_payloads_resolves_existing_song_uuid_for_all_platforms
(
monkeypatch
,
platform
,
songs_table
,
song_upsert_name
,
relation_upsert_name
,
):
cur
=
MagicMock
()
cur
.
fetchall
.
return_value
=
[(
200
,
'existing-song-uuid'
)]
song_upsert
=
MagicMock
()
relation_upsert
=
MagicMock
()
yinyan_upsert
=
MagicMock
()
monkeypatch
.
setattr
(
runner
,
song_upsert_name
,
song_upsert
)
monkeypatch
.
setattr
(
runner
,
relation_upsert_name
,
relation_upsert
)
monkeypatch
.
setattr
(
runner
,
'upsert_yinyan_song_records'
,
yinyan_upsert
)
runner
.
_write_import_payloads
(
cur
,
[{
'platform'
:
platform
,
'platform_song_id'
:
200
,
'result'
:
{
'platform_song_id'
:
200
},
'yinyan_record'
:
{
'song_id'
:
10
,
'record_id'
:
20
,
'platform'
:
platform
,
'platform_song_id'
:
200
,
},
'singers'
:
[],
'albums'
:
[],
'songs'
:
[
{
'song_uuid'
:
'temporary-uuid-1'
,
'platform_song_id'
:
200
},
{
'song_uuid'
:
'temporary-uuid-2'
,
'platform_song_id'
:
200
},
],
'singer_songs'
:
[(
11
,
200
),
(
11
,
200
),
(
12
,
200
)],
'singer_albums'
:
[],
}])
# 同批平台歌曲去重,且全部歌手关系使用数据库实际保留的歌曲 UUID。
assert
len
(
song_upsert
.
call_args
.
args
[
1
])
==
1
relation_upsert
.
assert_called_once_with
(
cur
,
[(
11
,
'existing-song-uuid'
),
(
12
,
'existing-song-uuid'
)],
)
sql
,
params
=
cur
.
execute
.
call_args
.
args
assert
f
'FROM {songs_table}'
in
sql
assert
params
==
(
200
,)
yinyan_upsert
.
assert_called_once
()
def
test_write_import_payloads_does_not_mark_yinyan_success_when_song_uuid_is_missing
(
monkeypatch
):
cur
=
MagicMock
()
cur
.
fetchall
.
return_value
=
[]
yinyan_upsert
=
MagicMock
()
monkeypatch
.
setattr
(
runner
,
'upsert_yinyan_song_records'
,
yinyan_upsert
)
with
pytest
.
raises
(
RuntimeError
,
match
=
r'platform_song_ids=\[200\]'
):
runner
.
_write_import_payloads
(
cur
,
[{
'platform'
:
runner
.
PLATFORM_KUGOU
,
'platform_song_id'
:
200
,
'result'
:
{
'platform_song_id'
:
200
},
'yinyan_record'
:
{
'song_id'
:
10
,
'record_id'
:
20
,
'platform'
:
runner
.
PLATFORM_KUGOU
,
'platform_song_id'
:
200
,
},
'singers'
:
[],
'albums'
:
[],
'songs'
:
[{
'song_uuid'
:
'temporary-uuid'
,
'platform_song_id'
:
200
}],
'singer_songs'
:
[(
11
,
200
)],
'singer_albums'
:
[],
}])
yinyan_upsert
.
assert_not_called
()
def
test_run_imports_only_pending_yinyan_platform_record
(
monkeypatch
):
pg_conn
=
_PgConnection
()
prepare
=
MagicMock
(
return_value
=
{
...
...
Please
register
or
sign in
to post a comment