<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Xingyuan Charts</title>
    <description>The latest articles on DEV Community by Xingyuan Charts (@gezhongjian).</description>
    <link>https://dev.to/gezhongjian</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232091%2F4c17653e-b329-49c9-ae9a-7c5fffe7edae.png</url>
      <title>DEV Community: Xingyuan Charts</title>
      <link>https://dev.to/gezhongjian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gezhongjian"/>
    <language>en</language>
    <item>
      <title>Same song, nine charts: deduplicating Chinese music rankings without shared IDs</title>
      <dc:creator>Xingyuan Charts</dc:creator>
      <pubDate>Mon, 09 Nov 2026 12:41:55 +0000</pubDate>
      <link>https://dev.to/gezhongjian/same-song-nine-charts-deduplicating-chinese-music-rankings-without-shared-ids-4keg</link>
      <guid>https://dev.to/gezhongjian/same-song-nine-charts-deduplicating-chinese-music-rankings-without-shared-ids-4keg</guid>
      <description>&lt;p&gt;Twelve public Chinese-music charts, nine platforms, two snapshots a day: roughly 570 rows every 12 hours, and &lt;strong&gt;no shared track ID anywhere&lt;/strong&gt; — the three TME properties have separate namespaces, and asking one for the other's ID silently returns a different song. So "this song is on 6 of 9 platforms" is not a join. It is a text decision, and every number downstream inherits whatever that decision got wrong.&lt;/p&gt;

&lt;p&gt;Live output, if you want to look at the result before the code: &lt;strong&gt;&lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt;&lt;/strong&gt; (charts, rank deltas, weekly diffs — read-only, no signup).&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: nine charts, zero shared IDs
&lt;/h2&gt;

&lt;p&gt;Twice a day I take a read-only snapshot of 12 public music charts across 9 platforms that publish Chinese-music charts — NetEase Cloud Music, QQ Music, Kugou, Kuwo, Qishui, Douyin, Weibo, Bilibili and Apple Music. That is roughly 570 rows per snapshot. The interesting part is not fetching the pages. It is deciding when two rows are the same song.&lt;/p&gt;

&lt;p&gt;There is no track ID shared across all nine. The three TME properties (QQ Music, Kugou, Kuwo) have completely separate namespaces; asking one for the other's ID silently returns a different song. So "this track is on 6 of 9 platforms" has to be computed from text, not from identifiers.&lt;/p&gt;

&lt;p&gt;The text is messy in predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;晴天&lt;/code&gt;, &lt;code&gt;晴天 (Live)&lt;/code&gt;, &lt;code&gt;晴天（剪辑版）&lt;/code&gt; — handled by the key below&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;晴天 - Live&lt;/code&gt; — &lt;em&gt;not&lt;/em&gt; handled: it becomes &lt;code&gt;晴天live&lt;/code&gt;, a known gap&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;七里香 feat. 杨瑞代&lt;/code&gt; vs &lt;code&gt;七里香&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;A/B&lt;/code&gt; vs &lt;code&gt;A、B&lt;/code&gt; vs &lt;code&gt;A &amp;amp; B&lt;/code&gt; for the same collaboration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@someone创作的原声&lt;/code&gt; — a Douyin sound title that is not a song title at all&lt;/li&gt;
&lt;li&gt;Chinese and English artist aliases: &lt;code&gt;周杰伦&lt;/code&gt; vs &lt;code&gt;Jay Chou&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the daily pipeline to be deterministic and auditable, not "the model thinks these are the same". Here is what I ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: one normalized key per track
&lt;/h2&gt;

&lt;p&gt;The key is &lt;code&gt;normalized title | sorted, normalized artists&lt;/code&gt;. Normalization is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;_BRACKETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\(\（\[\【].*?[\)\）\]\】]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_FEAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(?:feat|ft|featuring)\.?\s+.*$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_NON_WORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^0-9a-z\u4e00-\u9fff]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_BRACKETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# drop "(Live)", "（剪辑版）", "【...】"
&lt;/span&gt;    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_FEAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# drop "feat. X" tails
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_NON_WORD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# keep ASCII alnum + CJK only
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dedupe_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nf"&gt;normalize_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;artists&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;normalize_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;晴天 (Live)&lt;/code&gt; / &lt;code&gt;周杰伦&lt;/code&gt; and &lt;code&gt;晴天&lt;/code&gt; / &lt;code&gt;周杰伦&lt;/code&gt; collapse to &lt;code&gt;晴天|周杰伦&lt;/code&gt;. The same title by a different artist does not: the artist set is part of the key, which keeps covers apart without any extra rules.&lt;/p&gt;

&lt;p&gt;Two deliberate trade-offs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Brackets are dropped wholesale.&lt;/strong&gt; That merges a live version with the studio version, which is technically wrong. For chart aggregation it is usually the right call — charts treat them as one song — and the raw platform rows are still stored, so nothing is lost for audit. It also means &lt;code&gt;(电视剧《…》主题曲)&lt;/code&gt; disappears, which is fine for identity and bad for display. Display uses the raw title, not the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist aliases are not merged.&lt;/strong&gt; &lt;code&gt;周杰伦&lt;/code&gt; and &lt;code&gt;Jay Chou&lt;/code&gt; produce different keys, so a track can split into two identities. That undercounts cross-platform presence, but it never invents a merge. Under-counting is recoverable; a wrong merge poisons every downstream number.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Layer 2: fuzzy matching, kept offline
&lt;/h2&gt;

&lt;p&gt;There is a second scoring function in the repo for the cases the exact key cannot handle: name similarity (0.45), artist Jaccard overlap (0.30), duration closeness (0.20), album equality (0.05), with weights renormalized when a field is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name_sim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SequenceMatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;norm_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;norm_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])).&lt;/span&gt;&lt;span class="nf"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;art_sim&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;norm_artists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;norm_artists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;dur_sim&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name_sim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;art_sim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;dur_sim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;album_eq&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# original vs 65s edit are different recordings
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 20-second cap matters: a 269-second original and a 65-second clip can share a title and an artist, and should never be merged.&lt;/p&gt;

&lt;p&gt;But this scorer is &lt;strong&gt;not&lt;/strong&gt; in the daily path. Two reasons. First, 570 rows is small enough for pairwise comparison, but threshold tuning on Chinese titles is fragile: one bad threshold merges five different &lt;code&gt;泪海&lt;/code&gt; recordings, and once you have written metrics against that merge, unwinding it is real work. Second, determinism. Exact keys can be recomputed from the raw rows by anyone; a fuzzy score with a tuned threshold is an opinion. The fuzzy matcher stays offline, used to review alias candidates before adding a manual mapping.&lt;/p&gt;

&lt;p&gt;If you want the output rather than the machinery: the English charts view is &lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt; and the raw weekly diff samples are &lt;a href="https://xingyuan.beer/reports" rel="noopener noreferrer"&gt;https://xingyuan.beer/reports&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshot diffing without an event table
&lt;/h2&gt;

&lt;p&gt;Storage is SQLite. One row per &lt;code&gt;(platform, board, captured_at, rank)&lt;/code&gt;, with an idempotent upsert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;charts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dedupe_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metrics are derived per snapshot, not appended as events: &lt;code&gt;rank_delta = prev_rank - rank&lt;/code&gt;, &lt;code&gt;is_new = 1&lt;/code&gt; when the key was absent from the previous snapshot, &lt;code&gt;cross_platform =&lt;/code&gt; the number of distinct platforms carrying that key in the same snapshot. Because they are derived, recomputing a snapshot is safe and a parser fix can be replayed.&lt;/p&gt;

&lt;p&gt;The one thing that must be stable is &lt;code&gt;captured_at&lt;/code&gt;. All boards in a batch share it, because deltas are computed &lt;em&gt;between&lt;/em&gt; snapshots. If one board fails and the next run starts a new batch with a fresh timestamp, you get a partial snapshot whose deltas are wrong in a way that looks plausible. So a batch is explicit state: which boards are in, which failed, and what the shared timestamp is. An unfinished batch resumes with the same &lt;code&gt;captured_at&lt;/code&gt; for up to six hours; after that it is too stale to be useful and a new batch starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching: throttle, classify, and don't fight back
&lt;/h2&gt;

&lt;p&gt;The HTTP layer is per platform: a 0.35s minimum gap per host, two retries with exponential backoff and jitter. The important part is classification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;429&lt;/code&gt; → rate limited, do not retry; set a cooldown for that board.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;401 / 403 / 412&lt;/code&gt; → blocked, do not retry. (Bilibili in particular uses 412 for risk control.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;5xx&lt;/code&gt; → server, retry with backoff.&lt;/li&gt;
&lt;li&gt;network errors → retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrying a risk-control response just gets you blocked harder. Cooldowns live in a state file next to the database, so a restart does not forget that a board asked us to go away for a while.&lt;/p&gt;

&lt;p&gt;There is also a publication gate: a weekly diff is only generated when the last scheduled slot is complete. A partial batch is not "mostly fine" — it silently drops a board, and a diff that quietly lost Weibo is worse than a diff that is a few hours late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that actually bit me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Douyin's board is a challenge-word board, not a song chart.&lt;/strong&gt; Entries are tags people use in videos. Most are song titles, some are memes, and &lt;code&gt;@user创作的原声&lt;/code&gt; is not a title at all. Cross-platform matching for Douyin is therefore approximate by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weibo's music board is aggregated from posts&lt;/strong&gt;, not an official label chart. It is comparable as "what people post about", not as "what is charting".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"New" means new since the previous snapshot&lt;/strong&gt;, which is 12 hours earlier, not "new release". I had to rename the column in my own head before the UI stopped lying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist splitting is per adapter, and not uniform.&lt;/strong&gt; Some adapters split &lt;code&gt;A/B&lt;/code&gt; or &lt;code&gt;A、B&lt;/code&gt; into a list; Apple returns &lt;code&gt;周杰伦 &amp;amp; 五月天 阿信&lt;/code&gt; as a single string. Identical collaborations can therefore land on different keys. A shared splitter is on the list; until then the key is deliberately conservative. Also, &lt;code&gt;A feat. B&lt;/code&gt; normalizes to just &lt;code&gt;A&lt;/code&gt; — the same regex that merges live/studio variants eats the feature credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time zones.&lt;/strong&gt; Slots are 09:40 and 21:40 China time. A delta is 12 hours, not a day. Any "daily" statement has to say which two snapshots it compares.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do next
&lt;/h2&gt;

&lt;p&gt;The honest answer is that text normalization has a ceiling. Artist aliases need a curated map (there is no string metric that knows &lt;code&gt;周杰伦&lt;/code&gt; is &lt;code&gt;Jay Chou&lt;/code&gt;). The robust way to confirm "same recording" is audio fingerprinting — chroma/MFCC features plus DTW — which exists in the repo as an offline tool but is far too heavy to run on every row of every snapshot. A cheaper next step is a small alias table plus a review queue for new high-cross-platform keys.&lt;/p&gt;

&lt;p&gt;For now, the deterministic key handles the bulk of it, the fuzzy scorer stays where it cannot poison aggregates, and every number on the site is traceable back to raw rows and a snapshot timestamp.&lt;/p&gt;

&lt;p&gt;If you want to see the output: the English landing page is at &lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt;, the public Chinese dashboard is &lt;a href="https://xingyuan.beer/trends" rel="noopener noreferrer"&gt;https://xingyuan.beer/trends&lt;/a&gt;, and there are weekly diff samples at &lt;a href="https://xingyuan.beer/reports" rel="noopener noreferrer"&gt;https://xingyuan.beer/reports&lt;/a&gt;. All chart data is public; the code is not open source, but the methodology is documented at &lt;a href="https://xingyuan.beer/data" rel="noopener noreferrer"&gt;https://xingyuan.beer/data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Charts use different methodologies; rank changes are not sales, streams or revenue. Disclosure: collection runs as a scheduled job, and this write-up was assembled with AI assistance from the running code and real snapshots.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>data</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>C-Pop Chart Watch | 11.07 PM: Tianlong Babu Zhi Sudi spans 6 platforms, Qimiao Zai Shenbian climbs 38 spots</title>
      <dc:creator>Xingyuan Charts</dc:creator>
      <pubDate>Sat, 07 Nov 2026 17:10:49 +0000</pubDate>
      <link>https://dev.to/gezhongjian/c-pop-chart-watch-1107-pm-tian-long-ba-bu-zhi-su-di-spans-6-platforms-qi-miao-zai-shen-bian-climbs-38-spots-1771</link>
      <guid>https://dev.to/gezhongjian/c-pop-chart-watch-1107-pm-tian-long-ba-bu-zhi-su-di-spans-6-platforms-qi-miao-zai-shen-bian-climbs-38-spots-1771</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;In this snapshot, &lt;strong&gt;"天龙八部之宿敌" by 许嵩&lt;/strong&gt;, &lt;strong&gt;"我不难过" by 孙燕姿&lt;/strong&gt;, &lt;strong&gt;"雨爱" by 杨丞琳&lt;/strong&gt; appeared on &lt;strong&gt;6 of the 9&lt;/strong&gt; Chinese platforms we track. Cross-platform presence is a stronger signal than a single-chart rank.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data (2026-11-07 21:40 UTC+8)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Artist&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;th&gt;Best rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;天龙八部之宿敌&lt;/td&gt;
&lt;td&gt;许嵩&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;QQ Music Hot Songs #1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;我不难过&lt;/td&gt;
&lt;td&gt;孙燕姿&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;NetEase Hot Songs #2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;雨爱&lt;/td&gt;
&lt;td&gt;杨丞琳&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;QQ Music Hot Songs #11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;我好像在哪见过你&lt;/td&gt;
&lt;td&gt;薛之谦&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Qishui Hot Songs #1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;甲乙丙丁 (你我怎么两清)&lt;/td&gt;
&lt;td&gt;李佳薇&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Kuwo Hot Songs #4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Risers and new entries
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Track — Artist&lt;/th&gt;
&lt;th&gt;Platform / chart&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-chart rise&lt;/td&gt;
&lt;td&gt;奇妙在身边 — 摩登兄弟刘宇宁宋雨琦&lt;/td&gt;
&lt;td&gt;Weibo Music #11&lt;/td&gt;
&lt;td&gt;#11, +38&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-chart rise&lt;/td&gt;
&lt;td&gt;前行路上 (人生需要自强) — 徐小风, 柴东瓜&lt;/td&gt;
&lt;td&gt;Kugou Rising #11&lt;/td&gt;
&lt;td&gt;#11, +27&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-chart rise&lt;/td&gt;
&lt;td&gt;云是天空写的信 — 卡布叻_周深&lt;/td&gt;
&lt;td&gt;Weibo Music #10&lt;/td&gt;
&lt;td&gt;#10, +26&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New entry&lt;/td&gt;
&lt;td&gt;一路向北 — 周杰伦&lt;/td&gt;
&lt;td&gt;Kugou Rising #1&lt;/td&gt;
&lt;td&gt;new, #1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New entry&lt;/td&gt;
&lt;td&gt;爱而不得 (Live) — 薛之谦, 黄子弘凡&lt;/td&gt;
&lt;td&gt;QQ Music Rising #3&lt;/td&gt;
&lt;td&gt;new, #3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A rise on one chart can be an editorial slot; a new entry that already spans several platforms is a different story. Window: 2026-11-07 21:40 UTC+8, previous snapshot 09:40.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed since 09:40
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;3 tracks on 6 platforms now, 2 at 09:40.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"我不难过" by 孙燕姿 joined at 6 platforms&lt;/strong&gt; (5 at 09:40).&lt;/li&gt;
&lt;li&gt;Held the group: "天龙八部之宿敌" by 许嵩, "雨爱" by 杨丞琳.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why cross-chart beats single-chart rank
&lt;/h2&gt;

&lt;p&gt;A No.1 can be an editorial slot. The same track surviving on several platforms means it works across different recommendation systems. If you make content — covers, dance clips, edits — that is the difference between "the platform pushed it" and "listeners asked for it".&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Charts are read from &lt;strong&gt;public chart pages&lt;/strong&gt; on 9 platforms; two read-only snapshots per day, no login, no scraping evasion.&lt;/li&gt;
&lt;li&gt;Tracks are normalised by "title + primary artist" into one key, then counted across platforms.&lt;/li&gt;
&lt;li&gt;Rank change = previous rank − current rank; a positive number means a climb. "New entry" means new relative to the previous snapshot, not necessarily a new release.&lt;/li&gt;
&lt;li&gt;Start here (English): &lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Public dashboard (Chinese): &lt;a href="https://xingyuan.beer/trends" rel="noopener noreferrer"&gt;https://xingyuan.beer/trends&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Weekly picks: &lt;a href="https://xingyuan.beer/reports" rel="noopener noreferrer"&gt;https://xingyuan.beer/reports&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Public chart data only, no stream forecasts or campaign advice. Posted by an automated account; charts use different methodologies.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>cpop</category>
      <category>data</category>
      <category>china</category>
    </item>
    <item>
      <title>Xingyuan Charts: public music charts from nine Chinese platforms, twice a day</title>
      <dc:creator>Xingyuan Charts</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:40:25 +0000</pubDate>
      <link>https://dev.to/gezhongjian/xing-yuan-bang-dan-jiu-ge-ping-tai-de-yin-le-bang-dan-mei-tian-liang-ban-l16</link>
      <guid>https://dev.to/gezhongjian/xing-yuan-bang-dan-jiu-ge-ping-tai-de-yin-le-bang-dan-mei-tian-liang-ban-l16</guid>
      <description>&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;Xingyuan Charts (xingyuan.beer) takes a read-only snapshot of the &lt;strong&gt;public charts&lt;/strong&gt; on nine Chinese music platforms twice a day: NetEase Cloud Music, QQ Music, Kugou, Kuwo, Qishui, Douyin, Weibo, Bilibili and Apple Music.&lt;/p&gt;

&lt;p&gt;We do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform alignment&lt;/strong&gt; — a song sitting on six platforms at once tells you more than a No.1 on one chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rank movement&lt;/strong&gt; — two snapshots compared: who is rising, who is falling, who just entered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Q&amp;amp;A&lt;/strong&gt; — subscribers reply to any of our emails and ask "what should I make this week?"; we answer with the evidence and a concrete action (50 turns/week).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;A No.1 can be a platform's own editorial slot. A song that survives on six different recommendation algorithms is different — it is being pulled by listeners, not pushed by a playlist. That is the signal creators, labels and agencies actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to look
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start here (English): &lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Public dashboard (Chinese, read-only): &lt;a href="https://xingyuan.beer/trends" rel="noopener noreferrer"&gt;https://xingyuan.beer/trends&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Weekly cross-chart picks: &lt;a href="https://xingyuan.beer/reports" rel="noopener noreferrer"&gt;https://xingyuan.beer/reports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Methodology, data limits and refund policy: &lt;a href="https://xingyuan.beer/data" rel="noopener noreferrer"&gt;https://xingyuan.beer/data&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cadence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Snapshots: 09:40 and 21:40 (UTC+8), daily&lt;/li&gt;
&lt;li&gt;Weekly note: three cross-platform tracks plus one actionable suggestion&lt;/li&gt;
&lt;li&gt;We do not: predict streams, sell engagement, or give campaign advice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Only public chart data and rank movements are quoted. No revenue guarantees.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>cpop</category>
      <category>data</category>
      <category>china</category>
    </item>
    <item>
      <title>Only two songs charted on 6 platforms this week</title>
      <dc:creator>Xingyuan Charts</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:35:44 +0000</pubDate>
      <link>https://dev.to/gezhongjian/kua-ping-tai-tong-shi-zai-bang-6-ge-ping-tai-de-ge-ben-zhou-zhi-you-liang-shou-2026-11-07-1n8h</link>
      <guid>https://dev.to/gezhongjian/kua-ping-tai-tong-shi-zai-bang-6-ge-ping-tai-de-ge-ben-zhou-zhi-you-liang-shou-2026-11-07-1n8h</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;In this snapshot, only two tracks appeared on &lt;strong&gt;six of the nine&lt;/strong&gt; Chinese platforms we track: &lt;strong&gt;"Tian Long Ba Bu Zhi Su Di" by Xu Song&lt;/strong&gt; and &lt;strong&gt;"Yu Ai" by Rainie Yang&lt;/strong&gt;. Cross-platform presence is a stronger signal than a single-chart rank.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data (2026-11-07, 09:40 UTC+8)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Artist&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;th&gt;Best rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tian Long Ba Bu Zhi Su Di&lt;/td&gt;
&lt;td&gt;Xu Song&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Qishui Hot Songs #2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yu Ai&lt;/td&gt;
&lt;td&gt;Rainie Yang&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;QQ Music Hot Songs #12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Among this cycle's new entries, Stefanie Sun's "Wo Bu Nan Guo" reached #2 on Apple Music's Top Songs and #2 on NetEase's Hot Songs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cross-platform beats single-chart rank
&lt;/h2&gt;

&lt;p&gt;A No.1 can be an editorial slot. The same track surviving on NetEase, QQ, Kugou, Kuwo, Qishui and Douyin means it works across six different recommendation systems. If you make content — covers, dance clips, edits — that is the difference between "the platform pushed it" and "listeners asked for it".&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Charts are read from &lt;strong&gt;public chart pages&lt;/strong&gt; on nine platforms; two read-only snapshots per day, no login, no scraping evasion.&lt;/li&gt;
&lt;li&gt;Tracks are normalised by "title + primary artist" into one key, then counted across platforms.&lt;/li&gt;
&lt;li&gt;Start here (English): &lt;a href="https://xingyuan.beer/en?src=devto" rel="noopener noreferrer"&gt;https://xingyuan.beer/en?src=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Public dashboard (Chinese): &lt;a href="https://xingyuan.beer/trends" rel="noopener noreferrer"&gt;https://xingyuan.beer/trends&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Weekly picks: &lt;a href="https://xingyuan.beer/reports" rel="noopener noreferrer"&gt;https://xingyuan.beer/reports&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Public chart data only, no stream forecasts or campaign advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>cpop</category>
      <category>data</category>
      <category>china</category>
    </item>
  </channel>
</rss>
