内部データを用いた取得テスト
This commit is contained in:
commit
a05af1a208
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
||||
41
esbuild.config.mjs
Normal file
41
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import * as esbuild from "esbuild";
|
||||
import { cpSync, mkdirSync } from "fs";
|
||||
|
||||
const common = {
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
target: "es2020",
|
||||
};
|
||||
|
||||
async function build(browser) {
|
||||
const outdir = `dist/${browser}`;
|
||||
mkdirSync(outdir, { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
esbuild.build({
|
||||
...common,
|
||||
entryPoints: ["src/content/index.ts"],
|
||||
outfile: `${outdir}/content/index.js`,
|
||||
format: "iife",
|
||||
}),
|
||||
esbuild.build({
|
||||
...common,
|
||||
entryPoints: ["src/background/service-worker.ts"],
|
||||
outfile: `${outdir}/background/service-worker.js`,
|
||||
format: "iife",
|
||||
}),
|
||||
esbuild.build({
|
||||
...common,
|
||||
entryPoints: ["src/injected/page-script.ts"],
|
||||
outfile: `${outdir}/injected/page-script.js`,
|
||||
format: "iife",
|
||||
}),
|
||||
]);
|
||||
|
||||
cpSync(`manifest.${browser}.json`, `${outdir}/manifest.json`);
|
||||
}
|
||||
|
||||
await build("chrome");
|
||||
await build("firefox");
|
||||
|
||||
console.log("Build complete.");
|
||||
23
manifest.chrome.json
Normal file
23
manifest.chrome.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "YT Playlist Features",
|
||||
"version": "0.1.0",
|
||||
"description": "Extract and work with YouTube playlist data",
|
||||
"permissions": ["storage"],
|
||||
"background": {
|
||||
"service_worker": "background/service-worker.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://www.youtube.com/*"],
|
||||
"js": ["content/index.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injected/page-script.js"],
|
||||
"matches": ["*://www.youtube.com/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
29
manifest.firefox.json
Normal file
29
manifest.firefox.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "YT Playlist Features",
|
||||
"version": "0.1.0",
|
||||
"description": "Extract and work with YouTube playlist data",
|
||||
"permissions": ["storage"],
|
||||
"background": {
|
||||
"scripts": ["background/service-worker.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://www.youtube.com/*"],
|
||||
"js": ["content/index.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injected/page-script.js"],
|
||||
"matches": ["*://www.youtube.com/*"]
|
||||
}
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "yt-playlist-features@example.com",
|
||||
"strict_min_version": "133.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
531
package-lock.json
generated
Normal file
531
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
{
|
||||
"name": "yt-playlist-features",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yt-playlist-features",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webextension-polyfill": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.12.5.tgz",
|
||||
"integrity": "sha512-uKSAv6LgcVdINmxXMKBuVIcg/2m5JZugoZO8x20g7j2bXJkPIl/lVGQcDlbV+aXAiTyXT2RA5U5mI4IGCDMQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/webextension-polyfill": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz",
|
||||
"integrity": "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==",
|
||||
"license": "MPL-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
package.json
Normal file
18
package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "yt-playlist-features",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
}
|
||||
}
|
||||
27
src/background/service-worker.ts
Normal file
27
src/background/service-worker.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import browser from "webextension-polyfill";
|
||||
import type { Message } from "../shared/messages";
|
||||
import { savePlaylist, getPlaylist } from "../shared/storage";
|
||||
|
||||
const LOG_PREFIX = "[yt-playlist-features:bg]";
|
||||
|
||||
browser.runtime.onMessage.addListener(
|
||||
async (message: unknown, _sender: browser.Runtime.MessageSender) => {
|
||||
const msg = message as Message;
|
||||
|
||||
if (msg.type === "PLAYLIST_EXTRACTED") {
|
||||
console.log(
|
||||
LOG_PREFIX,
|
||||
`Saving playlist: "${msg.data.metadata.title}" (${msg.data.extractedCount} videos)`,
|
||||
);
|
||||
await savePlaylist(msg.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "GET_PLAYLIST") {
|
||||
const data = await getPlaylist(msg.playlistId);
|
||||
return { type: "PLAYLIST_RESPONSE", data } satisfies Message;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
console.log(LOG_PREFIX, "Service worker started.");
|
||||
220
src/content/extractor.ts
Normal file
220
src/content/extractor.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import type {
|
||||
PlaylistData,
|
||||
PlaylistMetadata,
|
||||
PlaylistVideo,
|
||||
Thumbnail,
|
||||
} from "../types/playlist";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
const LOG = "[yt-playlist-features]";
|
||||
|
||||
export function parsePlaylistData(raw: any): PlaylistData | null {
|
||||
try {
|
||||
const metadata = extractMetadata(raw);
|
||||
if (!metadata) {
|
||||
console.warn(LOG, "Could not extract metadata");
|
||||
return null;
|
||||
}
|
||||
|
||||
const videoListContents = findVideoListContents(raw);
|
||||
if (!videoListContents) {
|
||||
console.warn(LOG, "Could not find video list");
|
||||
return null;
|
||||
}
|
||||
|
||||
const videos: PlaylistVideo[] = [];
|
||||
let hasMore = false;
|
||||
|
||||
for (const item of videoListContents) {
|
||||
if (item.playlistVideoRenderer) {
|
||||
const video = parseVideo(item.playlistVideoRenderer);
|
||||
if (video) videos.push(video);
|
||||
}
|
||||
if (item.continuationItemRenderer) {
|
||||
hasMore = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metadata,
|
||||
videos,
|
||||
extractedAt: new Date().toISOString(),
|
||||
isComplete: !hasMore,
|
||||
extractedCount: videos.length,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(LOG, "Failed to parse playlist data:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractMetadata(raw: any): PlaylistMetadata | null {
|
||||
// Extract playlist ID from URL as fallback
|
||||
const playlistId = extractPlaylistId(raw);
|
||||
if (!playlistId) return null;
|
||||
|
||||
// Title from metadata.playlistMetadataRenderer
|
||||
const metadataRenderer = raw?.metadata?.playlistMetadataRenderer;
|
||||
const title = metadataRenderer?.title ?? "";
|
||||
|
||||
// Sidebar has primary and secondary info
|
||||
const sidebarItems =
|
||||
raw?.sidebar?.playlistSidebarRenderer?.items ?? [];
|
||||
const primaryInfo = sidebarItems[0]?.playlistSidebarPrimaryInfoRenderer;
|
||||
const secondaryInfo = sidebarItems[1]?.playlistSidebarSecondaryInfoRenderer;
|
||||
|
||||
// Stats from primary info (e.g. "87 本の動画", "視聴回数 1,234 回", "最終更新日...")
|
||||
const stats = primaryInfo?.stats ?? [];
|
||||
const videoCount = parseVideoCount(stats[0]);
|
||||
const viewCountText = extractText(stats[1]) || null;
|
||||
const lastUpdatedText = extractText(stats[2]) || null;
|
||||
|
||||
// Description from pageHeaderViewModel or primary info
|
||||
const pageHeaderVM =
|
||||
raw?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
|
||||
const description =
|
||||
extractText(pageHeaderVM?.description?.descriptionPreviewViewModel?.description) ||
|
||||
extractText(primaryInfo?.description) ||
|
||||
"";
|
||||
|
||||
// Owner from secondary info
|
||||
const ownerRenderer =
|
||||
secondaryInfo?.videoOwner?.videoOwnerRenderer;
|
||||
const ownerRun = ownerRenderer?.title?.runs?.[0];
|
||||
const ownerEndpoint =
|
||||
ownerRun?.navigationEndpoint?.browseEndpoint;
|
||||
|
||||
// Thumbnails from pageHeaderViewModel heroImage or primary info
|
||||
const thumbnails = extractPlaylistThumbnails(pageHeaderVM, primaryInfo);
|
||||
|
||||
return {
|
||||
playlistId,
|
||||
title,
|
||||
description,
|
||||
videoCount,
|
||||
totalDurationText: null, // not available in current structure
|
||||
viewCountText,
|
||||
lastUpdatedText,
|
||||
thumbnails,
|
||||
owner: {
|
||||
name: ownerRun?.text ?? "",
|
||||
channelId: ownerEndpoint?.browseId ?? "",
|
||||
url: ownerRun?.navigationEndpoint?.commandMetadata
|
||||
?.webCommandMetadata?.url ?? "",
|
||||
},
|
||||
privacy: "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
function extractPlaylistId(raw: any): string | null {
|
||||
// Try microformat
|
||||
const microformat =
|
||||
raw?.microformat?.microformatDataRenderer?.urlCanonical;
|
||||
if (microformat) {
|
||||
const match = microformat.match(/[?&]list=([^&]+)/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
// Try from appindexing link
|
||||
const appLink =
|
||||
raw?.metadata?.playlistMetadataRenderer?.androidAppindexingLink;
|
||||
if (appLink) {
|
||||
const match = appLink.match(/[?&]list=([^&]+)/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
// Fallback to URL
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get("list");
|
||||
}
|
||||
|
||||
function findVideoListContents(raw: any): any[] | null {
|
||||
const tabs =
|
||||
raw?.contents?.twoColumnBrowseResultsRenderer?.tabs;
|
||||
if (!tabs?.length) return null;
|
||||
|
||||
const tabContent = tabs[0]?.tabRenderer?.content;
|
||||
const sectionContents =
|
||||
tabContent?.sectionListRenderer?.contents;
|
||||
if (!sectionContents?.length) return null;
|
||||
|
||||
const itemSection =
|
||||
sectionContents[0]?.itemSectionRenderer?.contents;
|
||||
if (!itemSection?.length) return null;
|
||||
|
||||
return itemSection[0]?.playlistVideoListRenderer?.contents ?? null;
|
||||
}
|
||||
|
||||
function parseVideo(renderer: any): PlaylistVideo | null {
|
||||
const videoId = renderer.videoId;
|
||||
if (!videoId) return null;
|
||||
|
||||
const bylineRun = renderer.shortBylineText?.runs?.[0];
|
||||
const bylineEndpoint =
|
||||
bylineRun?.navigationEndpoint?.browseEndpoint;
|
||||
|
||||
const lengthSeconds = renderer.lengthSeconds
|
||||
? parseInt(renderer.lengthSeconds, 10)
|
||||
: null;
|
||||
|
||||
return {
|
||||
videoId,
|
||||
title: extractText(renderer.title),
|
||||
index: parseInt(extractText(renderer.index) || "0", 10),
|
||||
durationSeconds: lengthSeconds,
|
||||
durationText: extractText(renderer.lengthText) || null,
|
||||
thumbnails: renderer.thumbnail?.thumbnails ?? [],
|
||||
channel: {
|
||||
name: bylineRun?.text ?? "",
|
||||
channelId: bylineEndpoint?.browseId ?? "",
|
||||
url: bylineRun?.navigationEndpoint?.commandMetadata
|
||||
?.webCommandMetadata?.url ?? "",
|
||||
},
|
||||
isPlayable: renderer.isPlayable !== false,
|
||||
isLive:
|
||||
renderer.badges?.some(
|
||||
(b: any) =>
|
||||
b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW",
|
||||
) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function extractText(textObj: any): string {
|
||||
if (!textObj) return "";
|
||||
if (typeof textObj === "string") return textObj;
|
||||
if (textObj.simpleText) return textObj.simpleText;
|
||||
if (textObj.runs) {
|
||||
return textObj.runs.map((r: any) => r.text).join("");
|
||||
}
|
||||
if (textObj.content) return extractText(textObj.content);
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractPlaylistThumbnails(
|
||||
pageHeaderVM: any,
|
||||
primaryInfo: any,
|
||||
): Thumbnail[] {
|
||||
// Try heroImage from pageHeaderViewModel
|
||||
const heroThumbnails =
|
||||
pageHeaderVM?.heroImage?.contentPreviewImageViewModel?.image?.sources;
|
||||
if (heroThumbnails?.length) {
|
||||
return heroThumbnails.map((t: any) => ({
|
||||
url: t.url ?? "",
|
||||
width: t.width ?? 0,
|
||||
height: t.height ?? 0,
|
||||
}));
|
||||
}
|
||||
// Fallback to primary info thumbnail
|
||||
return (
|
||||
primaryInfo?.thumbnailRenderer?.playlistVideoThumbnailRenderer?.thumbnail
|
||||
?.thumbnails ??
|
||||
primaryInfo?.thumbnailRenderer?.playlistCustomThumbnailRenderer?.thumbnail
|
||||
?.thumbnails ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
function parseVideoCount(textObj: any): number {
|
||||
const text = extractText(textObj);
|
||||
const match = text.replace(/,/g, "").match(/(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
61
src/content/index.ts
Normal file
61
src/content/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import browser from "webextension-polyfill";
|
||||
import { onPlaylistPageReady, getPlaylistId } from "./navigation";
|
||||
import { parsePlaylistData } from "./extractor";
|
||||
import type { Message } from "../shared/messages";
|
||||
|
||||
const LOG_PREFIX = "[yt-playlist-features]";
|
||||
|
||||
// Track the last extracted playlist ID to avoid duplicate extractions
|
||||
let lastExtractedId: string | null = null;
|
||||
|
||||
function injectPageScript(): void {
|
||||
const script = document.createElement("script");
|
||||
script.src = browser.runtime.getURL("injected/page-script.js");
|
||||
script.onload = () => script.remove();
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
}
|
||||
|
||||
function handlePlaylistData(event: Event): void {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (!detail) return;
|
||||
|
||||
let raw: any;
|
||||
try {
|
||||
raw = JSON.parse(detail);
|
||||
} catch {
|
||||
console.error(LOG_PREFIX, "Failed to parse ytInitialData JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
const playlistData = parsePlaylistData(raw);
|
||||
if (!playlistData) {
|
||||
console.warn(LOG_PREFIX, "Could not extract playlist data from ytInitialData");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
LOG_PREFIX,
|
||||
`Extracted playlist: "${playlistData.metadata.title}" (${playlistData.extractedCount} videos)`,
|
||||
);
|
||||
|
||||
// Send to background service worker for storage
|
||||
const message: Message = { type: "PLAYLIST_EXTRACTED", data: playlistData };
|
||||
browser.runtime.sendMessage(message).catch((err) => {
|
||||
console.error(LOG_PREFIX, "Failed to send playlist data to background:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for data from the injected page script
|
||||
document.addEventListener("__yt_playlist_ext_data", handlePlaylistData);
|
||||
|
||||
// Detect playlist page navigation and trigger extraction
|
||||
onPlaylistPageReady(() => {
|
||||
const playlistId = getPlaylistId();
|
||||
if (!playlistId || playlistId === lastExtractedId) return;
|
||||
lastExtractedId = playlistId;
|
||||
|
||||
// Small delay to ensure ytInitialData is updated after SPA navigation
|
||||
setTimeout(() => {
|
||||
injectPageScript();
|
||||
}, 100);
|
||||
});
|
||||
38
src/content/navigation.ts
Normal file
38
src/content/navigation.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
type NavigationCallback = () => void;
|
||||
|
||||
export function onPlaylistPageReady(callback: NavigationCallback): void {
|
||||
// Check current page immediately
|
||||
if (isPlaylistPage()) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// YouTube's SPA navigation fires this custom event on completion
|
||||
document.addEventListener("yt-navigate-finish", () => {
|
||||
if (isPlaylistPage()) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback: watch <title> changes as a signal of navigation
|
||||
const titleEl = document.querySelector("title");
|
||||
if (titleEl) {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (isPlaylistPage()) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
observer.observe(titleEl, { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
function isPlaylistPage(): boolean {
|
||||
const url = new URL(window.location.href);
|
||||
return (
|
||||
url.pathname === "/playlist" && url.searchParams.has("list")
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlaylistId(): string | null {
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get("list");
|
||||
}
|
||||
11
src/injected/page-script.ts
Normal file
11
src/injected/page-script.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Runs in the page's JS context (not isolated world).
|
||||
// Reads ytInitialData and sends it to the content script via CustomEvent.
|
||||
|
||||
const data = (window as any).ytInitialData;
|
||||
if (data) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("__yt_playlist_ext_data", {
|
||||
detail: JSON.stringify(data),
|
||||
}),
|
||||
);
|
||||
}
|
||||
6
src/shared/messages.ts
Normal file
6
src/shared/messages.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { PlaylistData } from "../types/playlist";
|
||||
|
||||
export type Message =
|
||||
| { type: "PLAYLIST_EXTRACTED"; data: PlaylistData }
|
||||
| { type: "GET_PLAYLIST"; playlistId: string }
|
||||
| { type: "PLAYLIST_RESPONSE"; data: PlaylistData | null };
|
||||
25
src/shared/storage.ts
Normal file
25
src/shared/storage.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import browser from "webextension-polyfill";
|
||||
import type { PlaylistData } from "../types/playlist";
|
||||
|
||||
const STORAGE_PREFIX = "playlist:";
|
||||
|
||||
export async function savePlaylist(data: PlaylistData): Promise<void> {
|
||||
await browser.storage.local.set({
|
||||
[STORAGE_PREFIX + data.metadata.playlistId]: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPlaylist(
|
||||
playlistId: string,
|
||||
): Promise<PlaylistData | null> {
|
||||
const key = STORAGE_PREFIX + playlistId;
|
||||
const result = await browser.storage.local.get(key);
|
||||
return (result[key] as PlaylistData) ?? null;
|
||||
}
|
||||
|
||||
export async function getAllPlaylists(): Promise<PlaylistData[]> {
|
||||
const all = await browser.storage.local.get(null);
|
||||
return Object.entries(all)
|
||||
.filter(([key]) => key.startsWith(STORAGE_PREFIX))
|
||||
.map(([, value]) => value as PlaylistData);
|
||||
}
|
||||
51
src/types/playlist.ts
Normal file
51
src/types/playlist.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export interface Thumbnail {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface PlaylistVideo {
|
||||
videoId: string;
|
||||
title: string;
|
||||
/** 0-indexed position in the playlist */
|
||||
index: number;
|
||||
/** Duration in seconds; null if live or unknown */
|
||||
durationSeconds: number | null;
|
||||
/** Human-readable duration, e.g. "12:34" */
|
||||
durationText: string | null;
|
||||
thumbnails: Thumbnail[];
|
||||
channel: {
|
||||
name: string;
|
||||
channelId: string;
|
||||
url: string;
|
||||
};
|
||||
isPlayable: boolean;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
export interface PlaylistMetadata {
|
||||
playlistId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
videoCount: number;
|
||||
totalDurationText: string | null;
|
||||
viewCountText: string | null;
|
||||
lastUpdatedText: string | null;
|
||||
thumbnails: Thumbnail[];
|
||||
owner: {
|
||||
name: string;
|
||||
channelId: string;
|
||||
url: string;
|
||||
};
|
||||
privacy: "public" | "unlisted" | "private" | "unknown";
|
||||
}
|
||||
|
||||
export interface PlaylistData {
|
||||
metadata: PlaylistMetadata;
|
||||
videos: PlaylistVideo[];
|
||||
/** ISO timestamp of extraction */
|
||||
extractedAt: string;
|
||||
/** Whether all videos were loaded or only the first page */
|
||||
isComplete: boolean;
|
||||
extractedCount: number;
|
||||
}
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user