5 Commits
Author SHA1 Message Date
Hare 668574341e Upgrade dependencies version 2026-04-09 03:05:38 +09:00
Hare 9c4ea9dd4d Bump to 0.1.1 2026-04-09 03:04:17 +09:00
Hare 38dbd504d6 初回ロードのマウントポイント待機 2026-04-09 03:01:58 +09:00
Hare b23de0741a 順次表示の実装 2026-04-09 03:00:56 +09:00
Hare 65e2f28f8e SPA遷移の修正 2026-04-09 02:46:06 +09:00
10 changed files with 344 additions and 177 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "YT Playlist Features",
"version": "0.1.0",
"version": "0.1.1",
"description": "Extract and work with YouTube playlist data",
"permissions": [],
"background": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "YT Playlist Features",
"version": "0.1.0",
"version": "0.1.1",
"description": "Extract and work with YouTube playlist data",
"permissions": [],
"background": {
+115 -115
View File
@@ -1,25 +1,25 @@
{
"name": "yt-playlist-features",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yt-playlist-features",
"version": "0.1.0",
"version": "0.1.1",
"dependencies": {
"webextension-polyfill": "^0.12.0"
},
"devDependencies": {
"@types/webextension-polyfill": "^0.12.1",
"esbuild": "^0.25.0",
"typescript": "^5.7.0"
"@types/webextension-polyfill": "^0.12.5",
"esbuild": "^0.28.0",
"typescript": "^6.0.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
@@ -34,9 +34,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
@@ -51,9 +51,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
@@ -68,9 +68,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
@@ -85,9 +85,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
@@ -102,9 +102,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
@@ -119,9 +119,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
@@ -136,9 +136,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
@@ -153,9 +153,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
@@ -170,9 +170,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
@@ -187,9 +187,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
@@ -204,9 +204,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
@@ -221,9 +221,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
@@ -238,9 +238,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
@@ -255,9 +255,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
@@ -272,9 +272,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
@@ -289,9 +289,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
@@ -306,9 +306,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
@@ -323,9 +323,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
@@ -340,9 +340,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
@@ -357,9 +357,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
@@ -374,9 +374,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
@@ -391,9 +391,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
@@ -408,9 +408,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
@@ -425,9 +425,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
@@ -442,9 +442,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
@@ -466,9 +466,9 @@
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -479,38 +479,38 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "yt-playlist-features",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"type": "module",
"scripts": {
@@ -9,9 +9,9 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/webextension-polyfill": "^0.12.1",
"esbuild": "^0.25.0",
"typescript": "^5.7.0"
"@types/webextension-polyfill": "^0.12.5",
"esbuild": "^0.28.0",
"typescript": "^6.0.2"
},
"dependencies": {
"webextension-polyfill": "^0.12.0"
+2
View File
@@ -175,6 +175,8 @@ function parseVideo(renderer: any): PlaylistVideo | null {
(b: any) =>
b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW",
) ?? false,
addedBy: null,
voteCount: null,
};
}
+51 -2
View File
@@ -3,7 +3,7 @@ import { onPlaylistPageReady, getPlaylistId } from "./navigation";
import { parseInitialData, buildPlaylistData } from "./extractor";
import type { PlaylistVideo } from "../types/playlist";
import type { Message } from "../shared/messages";
import { mountTable } from "./ui/lifecycle";
import { mountTable, appendToTable, setTableComplete } from "./ui/lifecycle";
const LOG = "[yt-playlist-features]";
@@ -31,6 +31,9 @@ function handlePlaylistData(event: Event): void {
return;
}
// Skip append messages — handled separately
if (payload.type === "append") return;
if (payload.error) {
console.warn(LOG, "Page script error:", payload.error);
return;
@@ -75,7 +78,7 @@ function handlePlaylistData(event: Event): void {
} satisfies PlaylistVideo;
});
const playlistData = buildPlaylistData(metadata, videos, true);
const playlistData = buildPlaylistData(metadata, videos, payload.isComplete !== false);
console.log(
LOG,
@@ -90,8 +93,54 @@ function handlePlaylistData(event: Event): void {
});
}
function handlePlaylistAppend(event: Event): void {
const detail = (event as CustomEvent).detail;
if (!detail) return;
let payload: any;
try {
payload = JSON.parse(detail);
} catch {
return;
}
if (payload.type === "append") {
const newVideos: PlaylistVideo[] = payload.videos.map((v: any) => {
const bylineRun = v.shortBylineText?.runs?.[0];
const bylineEndpoint = bylineRun?.navigationEndpoint?.browseEndpoint;
return {
videoId: v.videoId,
title: v.title,
index: v.index,
durationSeconds: v.lengthSeconds,
durationText: v.lengthText || null,
thumbnails: v.thumbnails,
channel: {
name: bylineRun?.text ?? "",
channelId: bylineEndpoint?.browseId ?? "",
url: bylineRun?.navigationEndpoint?.commandMetadata
?.webCommandMetadata?.url ?? "",
},
isPlayable: v.isPlayable,
isLive:
v.badges?.some(
(b: any) =>
b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW",
) ?? false,
addedBy: v.addedBy ?? null,
voteCount: v.voteCount ?? null,
} satisfies PlaylistVideo;
});
appendToTable(newVideos);
if (payload.isComplete) {
setTableComplete(payload.totalCount);
}
}
}
// Listen for data from the injected page script
document.addEventListener("__yt_playlist_ext_data", handlePlaylistData);
document.addEventListener("__yt_playlist_ext_data", handlePlaylistAppend);
// Detect playlist page navigation and trigger extraction
onPlaylistPageReady(() => {
+4 -1
View File
@@ -9,7 +9,8 @@ type MessageKey =
| "filterChannel"
| "filterAddedBy"
| "badgeLive"
| "headerVideos";
| "headerVideos"
| "headerLoading";
const messages: Record<string, Record<MessageKey, string>> = {
ja: {
@@ -24,6 +25,7 @@ const messages: Record<string, Record<MessageKey, string>> = {
filterAddedBy: "追加者...",
badgeLive: "ライブ",
headerVideos: "本の動画",
headerLoading: "読み込み中…",
},
en: {
colIndex: "#",
@@ -37,6 +39,7 @@ const messages: Record<string, Record<MessageKey, string>> = {
filterAddedBy: "Added by...",
badgeLive: "LIVE",
headerVideos: "videos",
headerLoading: "loading…",
},
};
+52 -14
View File
@@ -1,21 +1,16 @@
import type { PlaylistData } from "../../types/playlist";
import type { PlaylistData, PlaylistVideo } from "../../types/playlist";
import { injectStyles } from "./styles";
import { renderPlaylistTable } from "./table-renderer";
import { renderPlaylistTable, type PlaylistTableHandle } from "./table-renderer";
const CONTAINER_ID = "ytpf-playlist-table";
export function mountTable(data: PlaylistData): void {
unmountTable();
injectStyles();
const el = renderPlaylistTable(data);
el.id = CONTAINER_ID;
let currentHandle: PlaylistTableHandle | null = null;
function findAnchor(): { parent: Element; before: Element | null } | null {
// Primary: before ytd-playlist-video-list-renderer (sibling)
const videoList = document.querySelector("ytd-playlist-video-list-renderer");
if (videoList?.parentElement) {
videoList.parentElement.insertBefore(el, videoList);
return;
return { parent: videoList.parentElement, before: videoList };
}
// Fallback: end of ytd-section-list-renderer > #contents
@@ -23,8 +18,7 @@ export function mountTable(data: PlaylistData): void {
"ytd-section-list-renderer > #contents",
);
if (sectionContents) {
sectionContents.appendChild(el);
return;
return { parent: sectionContents, before: null };
}
// Last resort: #primary
@@ -32,14 +26,58 @@ export function mountTable(data: PlaylistData): void {
"ytd-two-column-browse-results-renderer > #primary",
);
if (primary) {
primary.appendChild(el);
return { parent: primary, before: null };
}
return null;
}
function insertAt(el: HTMLElement, anchor: { parent: Element; before: Element | null }): void {
anchor.parent.insertBefore(el, anchor.before);
}
let pendingObserver: MutationObserver | null = null;
export function mountTable(data: PlaylistData): void {
unmountTable();
injectStyles();
currentHandle = renderPlaylistTable(data);
const el = currentHandle.element;
el.id = CONTAINER_ID;
const anchor = findAnchor();
if (anchor) {
insertAt(el, anchor);
return;
}
console.warn("[yt-playlist-features] Could not find anchor to mount table");
// DOM not ready yet — wait for the anchor element to appear
pendingObserver = new MutationObserver(() => {
const a = findAnchor();
if (a) {
pendingObserver!.disconnect();
pendingObserver = null;
insertAt(el, a);
}
});
pendingObserver.observe(document.body, { childList: true, subtree: true });
}
export function appendToTable(newVideos: PlaylistVideo[]): void {
currentHandle?.appendVideos(newVideos);
}
export function setTableComplete(extractedCount: number): void {
currentHandle?.setComplete(extractedCount);
}
export function unmountTable(): void {
if (pendingObserver) {
pendingObserver.disconnect();
pendingObserver = null;
}
currentHandle = null;
document.getElementById(CONTAINER_ID)?.remove();
}
+56 -5
View File
@@ -25,13 +25,15 @@ function getAllColumns(): Column[] {
interface TagInput {
container: HTMLElement;
getTags(): string[];
addCandidates(names: string[]): void;
}
function createTagInput(
placeholder: string,
candidates: string[],
initialCandidates: string[],
onChange: () => void,
): TagInput {
const candidates = [...initialCandidates];
const tags: string[] = [];
const container = document.createElement("div");
@@ -185,7 +187,16 @@ function createTagInput(
container.addEventListener("click", () => input.focus());
container.append(input, dropdown);
return { container, getTags: () => [...tags] };
return {
container,
getTags: () => [...tags],
addCandidates(names: string[]) {
for (const name of names) {
if (!candidates.includes(name)) candidates.push(name);
}
candidates.sort();
},
};
}
function compareFn(key: SortKey, dir: SortDir) {
@@ -278,7 +289,13 @@ function buildRow(
return tr;
}
export function renderPlaylistTable(data: PlaylistData): HTMLElement {
export interface PlaylistTableHandle {
element: HTMLElement;
appendVideos(newVideos: PlaylistVideo[]): void;
setComplete(extractedCount: number): void;
}
export function renderPlaylistTable(data: PlaylistData): PlaylistTableHandle {
const isCollab = data.videos.some(
(v) => v.addedBy != null || v.voteCount != null,
);
@@ -291,9 +308,17 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
const header = document.createElement("div");
header.className = "ytpf-header";
let extractedCount = data.extractedCount;
let complete = data.isComplete;
const titleSpan = document.createElement("span");
titleSpan.className = "ytpf-header-title";
titleSpan.textContent = `${data.metadata.title}${data.extractedCount} ${t("headerVideos")}`;
function updateHeader() {
const status = complete ? "" : ` (${t("headerLoading")})`;
titleSpan.textContent = `${data.metadata.title}${extractedCount} ${t("headerVideos")}${status}`;
}
updateHeader();
const metaSpan = document.createElement("span");
metaSpan.className = "ytpf-header-meta";
@@ -445,5 +470,31 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
toggle.classList.toggle("ytpf-toggle--collapsed");
});
return wrapper;
function appendVideos(newVideos: PlaylistVideo[]) {
videos.push(...newVideos);
extractedCount += newVideos.length;
// Update filter candidates
channelTagInput.addCandidates(newVideos.map((v) => v.channel.name));
if (addedByTagInput) {
addedByTagInput.addCandidates(
newVideos.map((v) => v.addedBy).filter((n): n is string => n != null),
);
}
// Re-sort if a sort is active
if (sortKey) {
videos.sort(compareFn(sortKey, sortDir));
}
renderRows();
updateHeader();
}
function setComplete(count: number) {
extractedCount = count;
complete = true;
updateHeader();
}
return { element: wrapper, appendVideos, setComplete };
}
+58 -34
View File
@@ -16,6 +16,7 @@
w.__yt_playlist_ext_injected = true;
let extractingUrl: string | null = null;
let isInitialLoad = true;
let collabCache: Map<string, string> = new Map();
let collabCachePlaylistId: string | null = null;
@@ -64,12 +65,13 @@
const { cfg, apiKey, baseContext } = getConfig();
const authHeaders = buildAuthHeaders(cfg, baseContext);
// Try ytInitialData first (works on initial page load)
let initialData = w.ytInitialData;
// Try ytInitialData only on initial page load.
// On SPA navigation ytInitialData is stale (still holds the previous page's data),
// so we must always fetch fresh data via the browse API.
let initialData = isInitialLoad ? w.ytInitialData : null;
let videoList = initialData ? findVideoListContents(initialData) : null;
isInitialLoad = false;
// If ytInitialData doesn't have the video list (SPA navigation),
// fetch the playlist data ourselves via the browse API
if (!videoList) {
const playlistId = getPlaylistId();
if (!playlistId) {
@@ -94,13 +96,13 @@
}
}
const allRenderers: any[] = [];
const firstPageRenderers: any[] = [];
let continuation: string | null = null;
let continuationClickTracking: string | null = null;
for (const item of videoList) {
if (item.playlistVideoRenderer) {
allRenderers.push(item.playlistVideoRenderer);
firstPageRenderers.push(item.playlistVideoRenderer);
}
if (item.continuationItemRenderer) {
const extracted = extractContinuationInfo(item.continuationItemRenderer);
@@ -109,7 +111,40 @@
}
}
// Fetch remaining pages
// Fetch collaborator names for collaborative playlists
const avatarToName = await fetchCollaboratorMap(initialData, cfg, baseContext, authHeaders);
function mapRenderers(renderers: any[], startIndex: number) {
return renderers.map((d, i) => ({
videoId: d.videoId,
title: textOf(d.title),
index: startIndex + i,
lengthSeconds: d.lengthSeconds ? parseInt(d.lengthSeconds, 10) : null,
lengthText: textOf(d.lengthText),
thumbnails: d.thumbnail?.thumbnails ?? [],
shortBylineText: d.shortBylineText,
isPlayable: d.isPlayable !== false,
badges: d.badges ?? [],
voteCount: typeof d.voteCount === "number" ? d.voteCount : null,
addedBy: resolveAddedBy(d, avatarToName),
}));
}
// Send first page immediately
const hasContinuation = !!continuation;
const firstPageVideos = mapRenderers(firstPageRenderers, 0);
console.log(LOG, `First page: ${firstPageVideos.length} videos (has more: ${hasContinuation})`);
sendResult({
videos: firstPageVideos,
initialData,
innertubeApiKey: apiKey,
innertubeContext: baseContext,
isComplete: !hasContinuation,
});
// Fetch remaining pages lazily and send incremental updates
let totalCount = firstPageVideos.length;
let page = 1;
while (continuation && page < 50) {
try {
@@ -132,12 +167,13 @@
continuation = null;
continuationClickTracking = null;
const pageRenderers: any[] = [];
for (const action of data.onResponseReceivedActions ?? []) {
const items = action?.appendContinuationItemsAction?.continuationItems;
if (!items) continue;
for (const item of items) {
if (item.playlistVideoRenderer) {
allRenderers.push(item.playlistVideoRenderer);
pageRenderers.push(item.playlistVideoRenderer);
}
if (item.continuationItemRenderer) {
const extracted = extractContinuationInfo(item.continuationItemRenderer);
@@ -146,38 +182,26 @@
}
}
}
const pageVideos = mapRenderers(pageRenderers, totalCount);
totalCount += pageVideos.length;
console.log(LOG, `Continuation page ${page}: +${pageVideos.length} videos (total: ${totalCount})`);
sendResult({
type: "append",
videos: pageVideos,
isComplete: !continuation,
totalCount,
});
page++;
} catch (e) {
console.error(LOG, `Continuation page ${page} failed:`, e);
// Send completion even on error so the loading indicator goes away
sendResult({ type: "append", videos: [], isComplete: true, totalCount });
break;
}
}
// Fetch collaborator names for collaborative playlists
const avatarToName = await fetchCollaboratorMap(initialData, cfg, baseContext, authHeaders);
const videos = allRenderers.map((d, i) => ({
videoId: d.videoId,
title: textOf(d.title),
index: i,
lengthSeconds: d.lengthSeconds ? parseInt(d.lengthSeconds, 10) : null,
lengthText: textOf(d.lengthText),
thumbnails: d.thumbnail?.thumbnails ?? [],
shortBylineText: d.shortBylineText,
isPlayable: d.isPlayable !== false,
badges: d.badges ?? [],
voteCount: typeof d.voteCount === "number" ? d.voteCount : null,
addedBy: resolveAddedBy(d, avatarToName),
}));
console.log(LOG, `Extraction: ${videos.length} videos (${page - 1} continuation pages)`);
sendResult({
videos,
initialData,
innertubeApiKey: apiKey,
innertubeContext: baseContext,
});
}
// --- helpers ---