Xây dựng trình phát nhạc trên trình duyệt cho AI Playlist: Bài học từ Vanilla JS

06 tháng 4, 2026·10 phút đọc

PlaylistBridge ra đời để giải quyết vấn đề sao chép danh sách bài hát từ AI thủ công. Ứng dụng được viết hoàn toàn bằng JavaScript thuần, tích hợp Netlify và Firebase, mang đến trải nghiệm nghe nhạc mượt mà mà không cần cài đặt hay đăng nhập.

Xây dựng trình phát nhạc trên trình duyệt cho AI Playlist: Bài học từ Vanilla JS

Ở tuổi 17. JavaScript thuần. Không framework. Không backend server. Đã sẵn sàng tung ra thị trường.

Vấn đề chưa ai giải quyết

Mỗi tuần, hàng nghìn người yêu cầu ChatGPT, Claude hoặc Gemini tạo danh sách phát. AI vui vẻ trả lại một danh sách 10 bài hát tuyệt đẹp. Và sau đó... mọi thứ dừng lại. Họ bị kẹt trong việc phải sao chép và dán từng bài hát vào Spotify hoặc YouTube một cách thủ công.

Thật phiền phức. Vì vậy, tôi đã khắc phục nó.

PlaylistBridge (playlistbridge.netlify.app) chuyển đổi mọi danh sách bài hát do AI tạo thành các liên kết có thể phát ngay lập tức. Không cần đăng nhập. Không cần cài đặt ứng dụng. Chỉ cần dán và phát.

Tôi bắt đầu với một ý tưởng đơn giản. Trong vài tháng qua, nó đã trở thành một thứ tôi không mong đợi — một trình phát nhạc hoàn chỉnh trên trình duyệt với chế độ video, danh sách phát cộng đồng, thẻ chia sẻ và bộ đệm bài hát được hỗ trợ bởi Firestore. Tất cả đều bằng JavaScript thuần.

Dưới đây là câu chuyện về cách tôi xây dựng nó và những bài học tôi rút ra được.

Công nghệ sử dụng (Và lý do tôi chọn)

  • Frontend: HTML, CSS, JavaScript thuần — không React, không Vue, không gì cả
  • Backend: Netlify Functions (serverless)
  • Cơ sở dữ liệu: Firebase Firestore
  • Hosting: Tầng miễn phí của Netlify
  • Metadata: iTunes Search API (miễn phí, không cần khóa)
  • Phát lại: YouTube IFrame API

Tôi có chủ ý chọn JavaScript thuần. Không có bước build (build step), không cơn ác mộng về dependency, không chi phí khung framework (framework overhead). Mọi tệp đều có thể chỉnh sửa và triển khai trực tiếp. Khi có sự cố, tôi biết chính xác phải nhìn vào đâu.

Sự đánh đổi là việc thao tác DOM dài dòng hơn — nhưng tôi đã giải quyết vấn đề này bằng cách tạo một lớp hiển thị ui.js tách biệt tất cả trạng thái trực quan khỏi logic nghiệp vụ. script.js không bao giờ chạm vào DOM trực tiếp. window.UI.* xử lý mọi thứ liên quan đến giao diện.

Tính năng cốt lõi: Khớp Metadata

Khi người dùng dán danh sách bài hát, những điều sau sẽ xảy ra:

  1. Parser xóa bỏ số thứ tự, dấu gạch ngang và khoảng trắng
  2. Mỗi truy vấn bài hát sẽ truy cập vào Netlify function của tôi, nơi đóng vai trò proxy cho iTunes Search API
  3. iTunes trả về tên bài hát thực, nghệ sĩ và URL ảnh bìa album
  4. Các thẻ (cards) được kết xuất tiến bộ khi mỗi bài hát được giải quyết — không cần chờ tất cả bài hát xong

Việc kết xuất tiến bộ (progressive rendering) rất quan trọng. Nếu tôi đợi cả 20 bài hát giải quyết xong trước khi hiển thị bất cứ thứ gì, người dùng sẽ nghĩ ứng dụng bị lỗi. Progressive rendering làm cho nó có cảm giác tức thì.

Một trường hợp ngoại lệ khó khăn: iTunes API đôi khi trả về sai bài hát nếu truy vấn mơ hồ. Tôi đã giải quyết điều này bằng cách xây dựng truy vấn dưới dạng "Tên nghệ sĩ Tên bài hát" từ đầu vào đã phân tích cú pháp, điều này cải thiện đáng kể độ chính xác của việc khớp.

InstantPlay: Tính năng thay đổi mọi thứ

Ứng dụng ban đầu chỉ cung cấp các liên kết. Người dùng phải nhấp 20 lần để mở 20 bài hát. Vẫn tốt hơn là không có gì, nhưng nó chưa phải là tuyệt vời.

Vì vậy, tôi đã xây dựng InstantPlay — một trình phát nhạc trên trình duyệt sử dụng YouTube IFrame API.

Quy trình:

Người dùng nhấp Play → Kiểm tra bộ nhớ đệm trong bộ nhớ (in-memory) → Kiểm tra bộ sưu tập bài hát Firestore → Gọi /.netlify/functions/ytsearch   → Thử API Invidious (3 phiên bản, timeout 3s mỗi cái)   → Fallback: Cạo dữ liệu HTML YouTube → Tải trình phát YouTube IFrame → Tự động chuyển sang bài tiếp theo khi kết thúc • Tải trước ID bài hát tiếp theo ở chế độ nền

Chiến lược Caching (Bộ đệm)

Tôi đã xây dựng ba lớp lưu trữ đệm cho ID video:

  • Lớp 1 — Đối tượng JS trong bộ nhớ: Độ trễ bằng không, bị mất khi tải lại trang.
  • Lớp 2 — sessionStorage: Tồn tại sau khi tải lại trang trong cùng một phiên trình duyệt.
  • Lớp 3 — Bộ sưu tập songs của Firestore: Chéo giữa người dùng, vĩnh viễn. ID tài liệu là truy vấn đã được chuẩn hóa. Người dùng đầu tiên phát bài hát sẽ chịu chi phí API. Mọi người dùng sau sẽ lấy nó từ Firestore ngay lập tức.

Điều này có nghĩa là khi ứng dụng phát triển, các lệnh gọi hàm Netlify cho ID video thực sự sẽ giảm theo thời gian khi bộ đệm được lấp đầy.

Vấn đề YouTube ToS

Đây là điều tôi phải suy nghĩ kỹ: Điều khoản dịch vụ của YouTube IFrame API explicitly cấm tạo trải nghiệm "chỉ âm thanh" (audio-only). Trình phát của tôi đang ẩn iframe bằng height: 0.

Giải pháp của tôi: một trình phát toàn màn hình có thể mở rộng với công tắc chuyển đổi Âm thanh/Video. Ở chế độ Âm thanh, người dùng thấy ảnh bìa album. Ở chế độ Video, họ thấy video YouTube thực tế. Iframe ẩn luôn hiện diện cho âm thanh — nhưng người dùng có thể chuyển sang chế độ video để xem nội dung trực quan, giữ cho trải nghiệm tuân thủ ToS.

Trình phát mở rộng

Đây là phần UI phức tạp nhất.

Khi người dùng nhấn vào thanh nhỏ ở dưới cùng, nó sẽ trượt lên thành trình phát toàn màn hình — giống chính chế độ xem mở rộng của YouTube Music. Phía sau hậu trường:

  • Nền trở thành phiên bản mờ của ảnh bìa album hiện tại
  • Ảnh bìa album được hiển thị to và ở giữa với một ánh sáng nhẹ
  • Thanh tiến trình, điều khiển và hàng đợi đều hiển thị
  • Chuyển sang chế độ Video sẽ chèn một iframe nhúng YouTube tiêu chuẩn tại dấu thời gian hiện tại

Lỗi khó chịu nhất: khi chế độ Video chèn iframe của chính nó, trình phát YT API ẩn vẫn đang chạy — hai nguồn âm thanh phát đồng thời. Giải pháp là thực hiện mute()pauseVideo() trên trình phát API khi chuyển sang chế độ video, sau đó unMute()playVideo() khi chuyển lại chế độ âm thanh.

Danh sách phát cộng đồng

Tôi muốn người dùng chia sẻ những gì họ đã xây dựng. Trang cộng đồng hiển thị danh sách phát được xuất bản bởi những người dùng khác — mỗi thẻ có một khổ ảnh 2×2, xem trước bài hát, số lượt phát và nút "Mở & Phát".

Một tối ưu hóa chính: thay vì lấy ảnh bìa album từ iTunes mỗi khi tải trang cộng đồng (sẽ là 48 lệnh gọi API cho 12 thẻ), tôi lưu mảng artUrls vào Firestore tại thời điểm xuất bản. Trang cộng đồng đọc chúng trực tiếp — bằng không lệnh gọi API.

Đối với tiêu đề danh sách phát, tôi tự động tạo chúng: "The Weeknd, Taylor Swift + 3 bài khác". Không cần đầu vào của người dùng — việc xuất bản không ma sát (frictionless) có nghĩa là nhiều người hơn thực sự xuất bản.

Thẻ chia sẻ

Sau khi tạo danh sách phát, người dùng có thể chia sẻ một thẻ hình ảnh đẹp mắt — giống như Spotify Wrapped cho danh sách phát của họ.

Được xây dựng hoàn toàn bằng HTML5 Canvas:

  • Ảnh bìa album đầu tiên trở thành nền mờ
  • Khổ ảnh 2×2 ở phía trên
  • Danh sách bài hát với hình thu nhỏ bên dưới
  • Thương hiệu PlaylistBridge + nút kêu gọi hành động (CTA) ở dưới cùng

Thách thức CORS: Canvas bị ô nhiễm khi vẽ hình ảnh cross-origin. Giải pháp là thêm crossorigin="anonymous" vào các phần tử <img> tại thời điểm kết xuất thẻ, sau đó đọc các phần tử DOM đó trực tiếp trong Canvas — không cần lấy lại, không có lệnh gọi API bổ sung.

Trên thiết bị di động, nó kích hoạt bảng chia sẻ gốc. Trên máy tính để bàn, nó tải xuống dưới dạng PNG.

SEO thực sự có hiệu quả

Tôi đang nhận được 100% lưu lượng truy cập nước ngoài (Mỹ, Anh, Úc) với quảng cáo trả phí bằng không.

Những gì có hiệu quả:

  • Nhiều trang đích (landing page) dành riêng nhắm đến từ khóa dài (long-tail keywords) (/chatgpt-to-spotify, /text-to-youtube-music, v.v.)
  • Schema JSON-LD cho WebApplicationFAQPage
  • llms.txt — một tệp văn bản thuần giải thích công cụ cho các mô hình AI. Đây là lý do ChatGPT và Gemini đã giới thiệu PlaylistBridge cho người dùng một cách tự nhiên.
  • alternateName: "Playlist Bridge" trong schema để bắt được biến thể tìm kiếm hai từ.

Những gì không có hiệu quả: Reddit. Mọi bài đăng đều bị xóa bất kể tuổi tài khoản hay chất lượng nội dung. Đã chuyển sang hướng khác.

Con số

  • 319+ danh sách phát được tạo (theo dõi qua Firestore)
  • 100% lưu lượng truy cập nước ngoài — Mỹ, Anh, Úc, Châu Âu
  • 10 ngày để đạt 100 khách truy cập sau khi thiết kế lại (so với 2 tháng trước đó)
  • 777 lượt gọi hàm Netlify trong một ngày đỉnh điểm
  • 0 quảng cáo trả phí

Những gì tôi sẽ làm khác đi

Sử dụng hệ thống thiết kế (design system) từ ngày đầu tiên. Tôi đã dành hàng giờ để sửa chữa khoảng cách không nhất quán vì tôi không thiết lập các biến CSS đúng cách ngay từ đầu. Bây giờ tôi có một hệ thống thiết kế đầy đủ — nhưng việc áp dụng lại nó (retrofitting) rất đau đớn.

Xây dựng lớp lưu đệm (caching layer) sớm hơn. Tôi chỉ thêm bộ đệm bài hát Firestore sau khi nhận thấy các lệnh gọi hàm Netlify tăng vọt. Nó lẽ ra phải có ở đó từ ngày đầu tiên.

Kiểm tra trên thiết bị thực sớm hơn. Rất nhiều lỗi chỉ xuất hiện trên thiết bị di động — bố cục trình phát mở rộng, điều khiển âm lượng bị cắt, hạn chế phát nền. Trình giả lập (emulators) nói dối.

Tiếp theo là gì?

  • Spotify OAuth — tạo danh sách phát thực trong tài khoản người dùng. Đây là tính năng người dùng thực sự muốn.
  • Trang Tâm trạng/Phong cách (Mood/Vibes) — duyệt danh sách phát cộng đồng theo thể loại (Gym, Lãng mạn, Buồn, Đêm khuya)
  • AdSense — lưu lượng truy cập nước ngoài khiến điều này khả thi ngay bây giờ

Hãy thử

playlistbridge.netlify.app

Dán bất kỳ danh sách phát AI nào. Nhấn phát. Nó hoạt động ngay.

Nếu bạn đã xây dựng một cái gì đó tương tự hoặc có suy nghĩ về các quyết định kiến trúc, tôi rất muốn nghe chúng trong phần bình luận. Đặc biệt quan tâm đến việc những người khác đã xử lý câu hỏi tuân thủ YouTube IFrame ToS như thế nào.

Xây dựng bằng JavaScript thuần, Firebase và Netlify. Không vốn đầu tư mạo hiểm. Không nhóm. Chỉ là tôi, 17 tuổi, tự tìm hiểu.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗