昨天,我用自己的 YouTube Music 紀錄產生第一份活動 Recap。來源 ID、頻道、日期與月份都能統計,每張表也會交代資料覆蓋率。
今天開始處理第二個平台:Spotify。
我先完成一個匯入前檢查器。把 JSON 交給它,就能知道資料符合哪種欄位格式、播放時間是否能使用,以及哪些紀錄需要另外確認。這些結果會輸出成 JSON 和可直接閱讀的 Markdown 報告。
在資料變成排行榜之前,先把每一個數字的意思弄清楚。
今天的檢查器支援兩組串流記錄欄位格式。
第一組使用 endTime、artistName、trackName 和 msPlayed。第二組使用 ts、ms_played,以及較長的歌曲、藝人、專輯欄位名稱,另外可以帶有曲目和 Podcast 的 URI。
對照後,最容易忽略的差別是這個:
| 資料意義 | 第一組欄位 | 第二組欄位 |
|---|---|---|
| 串流結束時間 | endTime |
ts |
| 播放毫秒數 | msPlayed |
ms_played |
| 原始曲名 | trackName |
master_metadata_track_name |
| 原始藝人標籤 | artistName |
master_metadata_album_artist_name |
| 原始專輯名稱 | 本組未提供 | master_metadata_album_album_name |
如果程式只認得 msPlayed,遇到 ms_played 時,就可能把原本存在的播放時間當成缺值。
因此,檢查器會從每一列的欄位判斷格式。檔案改名不影響判斷;同一列若混入兩組格式的關鍵欄位,則會被標記為有歧義,留給後續檢查。
這是目前支援的兩組格式。遇到不認識的結構,報告會保留問題,不自行猜測欄位對應。
播放時間是這次最重要的檢查項目。
假設收到以下內容,全部都是為了測試而建立的例子:
[
{"msPlayed": 0},
{"msPlayed": 12000},
{"msPlayed": null},
{"msPlayed": "12000"},
{"msPlayed": -1},
{}
]
第一筆是明確記錄為 0 毫秒。第二筆是合法的正整數。第三筆明確填了 null,第六筆則根本沒有播放時間欄位。
第四筆雖然看起來是數字,實際上是字串;第五筆是負數,不能直接作為播放時間使用。
檢查器會把這些狀態分開:正值、零、null、缺少欄位,以及不合法的值。布林值與小數也不會被偷偷轉成播放毫秒數。
這裡特別容易出錯的是 Python 的 True。它在部分數值運算中可以被當成 1,但我們不能因此把 true 解讀為播放 1 毫秒。
同樣地,程式也不能用一個預設值,把所有缺少的播放時間都補成 0。那會讓「不知道」變成「確定沒有播放」,連資料覆蓋率都一起失真。
這個版本不會在讀到資料時,就把短播放或 skipped=true 的紀錄刪掉。
因為資料檢查要先回答的是:這筆資料記錄了什麼?欄位是否合法?
至於排行榜是否要排除特定活動,應該由後面的統計規則決定。現在先刪掉,後面連重新選擇計算方式的機會都沒有。
對播放旗標也是如此:false、null 和缺少欄位會分開記錄。字串 "false" 會被指出來,不會直接當成合法的布林值。
檢查器不會利用歌曲完整長度,也不會利用前後事件間隔,去填補播放時間。
Spotify 的串流記錄時間描述的是串流結束時間。接進統一模型時,這個語意需要留下來,不能只剩一個沒有說明的日期。
今天先在檢查報告裡記錄 stream_end_time,並檢查兩種時間表示。
對 endTime,目前支援的是分鐘精度的格式,例如:
2026-12-31 16:00
對 ts,目前支援帶有明確時區的 ISO 時間,例如:
2026-12-31T16:00:00Z
2027-01-01T00:00:00+08:00
後面兩個時間指向同一個瞬間。報告可以把它們整理成 UTC,但仍會記錄原本是分鐘、秒,還是更細的時間精度。
只有分鐘精度的資料,不能因為輸出時補上 :00,就變成真的觀察到第 0 秒。
若第二組格式缺少時區,或者日期本身不存在,檢查器會指出問題,不套用電腦所在的時區猜答案。
跨平台 Music Recap 還有另一個入口問題:串流記錄可能包含不同種類的內容。
今天的檢查器會查看曲目與 Podcast URI 的語法。合法的 spotify:track:... 歸入曲目資源,合法的 spotify:episode:... 歸入 Podcast 單集資源。
同一筆資料同時有這兩種有效 URI,就標記為衝突。沒有足夠 URI 證據的資料,則維持種類未定。
因此,只有 trackName 和 artistName 的資料,不會在這一步就被認定為已確認的音樂紀錄。
這個檢查也有明確範圍:URI 通過語法檢查,不代表已向平台確認曲目存在,更不代表已完成與 YouTube Music 的同曲辨識。
如果兩列內容完全相同,檢查器會把後面出現的列標記為疑似重複。
但它不會刪除。
尤其是只有分鐘精度的紀錄,相同時間、名稱和播放毫秒數,仍不足以讓我們斷言兩筆一定是同一個活動。
另一種情況是歌名相同、URI 不同。這也不能當成重複資料,因為可能是不同版本或不同來源項目。
今天的工具把問題位置找出來,保留後續判斷需要的資料。
這次驗證使用兩份自行建立、明確標示 is_synthetic=true 的合成資料,分別包含 6 筆與 10 筆紀錄。
我刻意放入零毫秒、未知時間長度、缺欄位、字串型數字、負數、缺時區、Podcast、URI 衝突與重複列,讓結果可以逐筆核對。
| 檢查結果 | 第一組格式 | 第二組格式 |
|---|---|---|
| 輸入紀錄 | 6 筆 | 10 筆 |
| 本版檢查通過 | 0 筆 | 3 筆 |
| 需要進一步確認 | 4 筆 | 3 筆 |
| 有明確欄位錯誤 | 2 筆 | 4 筆 |
| 合法數值型播放時間 | 2/6 | 8/10 |
| 可解析結束時間 | 6/6 | 9/10 |
第一組沒有任何整列直接通過,是因為這份測試資料沒有能確認內容種類的 URI。四筆沒有明確格式錯誤的紀錄,仍會留在「需要進一步確認」。另外兩筆則含有不合法的播放時間。
這也說明「欄位覆蓋率」與「整列通過率」要分開看。某列的播放時間可以是合法整數,但它的日期可能錯了,整列仍不能直接通過。
所有狀態都能對帳回輸入總數。報告會列出問題代碼與從 1 起算的列號,例如哪一列的播放毫秒數不合法,方便回到原始 JSON 核對。
兩份測試檔故意帶有錯誤,因此工具應該回報需要處理。驗證成功的意思,是它找到了預先放入的問題,而且沒有把正常值改壞。
今天由 ChatGPT 接續專案,完成格式辨識、欄位檢查、報告輸出與測試,沒有另外啟動 Codex 工作任務。
第一次測試時,有一個預期答案寫錯:把第一組資料的合法數值型播放時間填成 4 筆。
重新逐筆核對後,正確答案其實是 2 筆,也就是一筆正值與一筆明確的零。null 和缺少欄位都不應被算進去。
我修正的是測試的預期答案,沒有為了讓測試通過,就把未知值改成已知值。
最後,18 項針對資料語意與檔案輸出的測試通過。兩份合成資料也實際走完命令列檢查、JSON/Markdown 輸出與重新讀取,20 項對帳全部相符。
現在專案多了一個可以實際執行的 Spotify 匯入前檢查器。
交給它一份支援的串流記錄 JSON,就能取得格式判斷、播放時間狀態、時間精度、內容種類、疑似重複列,以及需要回頭處理的問題位置。
spotify_audit.json 保存結構化結果,spotify_audit.md 讓人直接閱讀。檢查過程保留原始檔,也沒有生成歌曲排行或任意丟掉紀錄。
下一步,就是把這些已經說清楚的欄位規則接到 SpotifyImporter,讓 Spotify 紀錄進入統一的 ListeningEvent,同時保留平台原本的時間與資料語意。
另外回覆前一篇的留言
Q:
把榜內份額與資料覆蓋率分開,還特別標出 1 月、9 月是部分月份,這讓 Recap 不會把漂亮數字講過頭。接上 Spotify 後,跨平台同曲辨識會怎麼驗收?你會優先用 ISRC,還是名稱、藝人與時長的組合規則?
A:
謝謝!跨平台同曲辨識目前的方向會是「ISRC 優先,但不把 ISRC 當唯一條件」。
Spotify 如果有提供 ISRC,它會是很強的識別依據;但 YouTube Music 的活動匯出本身不一定有 ISRC,所以不能假設兩邊永遠都能直接對。
沒有 ISRC 時,才會進到第二層,比對正規化後的歌曲名稱、藝人,以及之後能取得的歌曲版本資訊。時長也可以當輔助證據,但這裡指的是歌曲本身的媒體長度,不是這次 Takeout 缺少的「實際播放秒數」,兩者會分開保存。
我也不打算只因為名稱很像就直接合併,像 Live、Remix、Acoustic、翻唱或不同版本都可能要保留成不同 Track。之後會把它做成有證據等級的 matching 流程。
Spotify 的真實匯出格式會先在之後,等兩邊實際欄位都看過之後,再把 Track Identity 規則正式定下來。