From 86a1adee2cd902f2d6a0fd698eee5958e268f958 Mon Sep 17 00:00:00 2001 From: ntki72 Date: Wed, 25 Dec 2024 21:24:28 +0900 Subject: [PATCH] =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2=E3=81=AE?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E3=81=A8=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=81=AE=E9=85=8D=E4=BF=A1=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=8C=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .sequelizerc | 8 + config/config.js | 8 +- docker-compose.yml | 2 + models/channel.js | 24 - models/index.js | 43 -- models/liveschedule.js | 26 - package-lock.json | 446 ++++++++++++------ package.json | 11 +- public/thumbnails/EdJHvSsaoVc.jpg | Bin 0 -> 34142 bytes src/app.ts | 75 ++- src/config/env.ts | 15 +- .../20241225032432-create-channel.js | 0 .../20241225032605-create-live-schedule.js | 2 +- ...41225081006-add-videoId-to-liveSchedule.js | 12 + ...25084215-add-channel-handle-to-channels.js | 12 + src/models/channel.ts | 65 +++ src/models/index.ts | 16 +- src/models/liveschedule.ts | 70 +++ src/routes/admin.ts | 143 ++++++ src/routes/api.ts | 75 +++ src/services/liveScheduleService.ts | 57 +++ src/services/updateLiveSchedules.ts | 17 + src/utils/downloadThumbnail.ts | 32 ++ src/utils/migrate.ts | 25 + src/utils/youtube.ts | 54 +++ views/admin/channels.ejs | 44 ++ views/admin/edit-channel.ejs | 23 + views/admin/index.ejs | 12 + views/admin/new-channel.ejs | 25 + views/admin/test-result.ejs | 27 ++ wait-for-it.sh | 1 - 31 files changed, 1104 insertions(+), 266 deletions(-) create mode 100644 .sequelizerc delete mode 100644 models/channel.js delete mode 100644 models/index.js delete mode 100644 models/liveschedule.js create mode 100644 public/thumbnails/EdJHvSsaoVc.jpg rename {migrations => src/migrations}/20241225032432-create-channel.js (100%) rename {migrations => src/migrations}/20241225032605-create-live-schedule.js (97%) create mode 100644 src/migrations/20241225081006-add-videoId-to-liveSchedule.js create mode 100644 src/migrations/20241225084215-add-channel-handle-to-channels.js create mode 100644 src/models/channel.ts create mode 100644 src/models/liveschedule.ts create mode 100644 src/routes/api.ts create mode 100644 src/services/liveScheduleService.ts create mode 100644 src/services/updateLiveSchedules.ts create mode 100644 src/utils/downloadThumbnail.ts create mode 100644 src/utils/migrate.ts create mode 100644 src/utils/youtube.ts create mode 100644 views/admin/channels.ejs create mode 100644 views/admin/edit-channel.ejs create mode 100644 views/admin/index.ejs create mode 100644 views/admin/new-channel.ejs create mode 100644 views/admin/test-result.ejs diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 0000000..5eade1c --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,8 @@ +const path = require('path'); + +module.exports = { + 'config': path.resolve('config/config.js'), + 'models-path': path.resolve('src/models'), + 'migrations-path': path.resolve('src/migrations'), + 'seeders-path': path.resolve('src/seeders'), +}; diff --git a/config/config.js b/config/config.js index c76bae9..80eaede 100644 --- a/config/config.js +++ b/config/config.js @@ -3,9 +3,13 @@ require('dotenv').config(); module.exports = { development: { username: process.env.DB_USER, - password: process.env.DB_PASSWORD, + password: process.env.DB_PASS, database: process.env.DB_NAME, host: process.env.DB_HOST, - dialect: "mysql" + dialect: "mysql", + migrationStorage: "sequelize", // マイグレーションの保存形式 + seederStorage: "sequelize", // シードデータの保存形式 + migrationStoragePath: "src/migrations", // マイグレーションのディレクトリ + seederStoragePath: "src/seeders" // シードデータのディレクトリ } }; diff --git a/docker-compose.yml b/docker-compose.yml index 28bcb2a..f61f35a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: DB_USER: ${DB_USER} DB_PASS: ${DB_PASS} DB_HOST: db + volumes: + - ./public/thumbnails:/app/public/thumbnails db: image: mysql:8.0 volumes: diff --git a/models/channel.js b/models/channel.js deleted file mode 100644 index 5fcac35..0000000 --- a/models/channel.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -const { - Model -} = require('sequelize'); -module.exports = (sequelize, DataTypes) => { - class Channel extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ - static associate(models) { - // define association here - } - } - Channel.init({ - name: DataTypes.STRING, - youtube_id: DataTypes.STRING - }, { - sequelize, - modelName: 'Channel', - }); - return Channel; -}; \ No newline at end of file diff --git a/models/index.js b/models/index.js deleted file mode 100644 index 024200e..0000000 --- a/models/index.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const Sequelize = require('sequelize'); -const process = require('process'); -const basename = path.basename(__filename); -const env = process.env.NODE_ENV || 'development'; -const config = require(__dirname + '/../config/config.json')[env]; -const db = {}; - -let sequelize; -if (config.use_env_variable) { - sequelize = new Sequelize(process.env[config.use_env_variable], config); -} else { - sequelize = new Sequelize(config.database, config.username, config.password, config); -} - -fs - .readdirSync(__dirname) - .filter(file => { - return ( - file.indexOf('.') !== 0 && - file !== basename && - file.slice(-3) === '.js' && - file.indexOf('.test.js') === -1 - ); - }) - .forEach(file => { - const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); - db[model.name] = model; - }); - -Object.keys(db).forEach(modelName => { - if (db[modelName].associate) { - db[modelName].associate(db); - } -}); - -db.sequelize = sequelize; -db.Sequelize = Sequelize; - -module.exports = db; diff --git a/models/liveschedule.js b/models/liveschedule.js deleted file mode 100644 index 3baa206..0000000 --- a/models/liveschedule.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -const { - Model -} = require('sequelize'); -module.exports = (sequelize, DataTypes) => { - class LiveSchedule extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ - static associate(models) { - // define association here - } - } - LiveSchedule.init({ - channelId: DataTypes.INTEGER, - title: DataTypes.STRING, - start_time: DataTypes.DATE, - thumbnail_url: DataTypes.STRING - }, { - sequelize, - modelName: 'LiveSchedule', - }); - return LiveSchedule; -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ba4e444..7d4b4ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,22 @@ "name": "youtube_live_calendar", "version": "1.0.0", "dependencies": { + "axios": "^1.7.9", "dotenv": "^16.4.7", + "ejs": "^3.1.10", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "method-override": "^3.0.0", "mysql2": "^3.3.3", - "node-cron": "^3.0.0", - "sequelize": "^6.32.1" + "node-cron": "^3.0.3", + "sequelize": "^6.32.1", + "sequelize-cli": "^6.6.2" }, "devDependencies": { "@types/express": "^4.17.17", + "@types/method-override": "^3.0.0", "@types/node": "^20.4.2", - "sequelize-cli": "^6.6.2", + "@types/node-cron": "^3.0.11", "typescript": "^5.2.2" } }, @@ -26,7 +31,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -44,14 +48,12 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -121,6 +123,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-7XFHR6j7JljprBpzzRZatakUXm1kEGAM3PL/GSsGRHtDvOAKYCdmnXX/5YSl1eQrpJymGs9tRekSWEGaG+Ntjw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -143,6 +155,13 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -190,7 +209,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -213,7 +231,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -223,13 +240,15 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -241,11 +260,22 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 4.0.0" @@ -260,11 +290,21 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -289,7 +329,6 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -317,13 +356,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/bytes": { @@ -364,11 +403,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/cli-color": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "dev": true, "license": "ISC", "dependencies": { "d": "^1.0.1", @@ -385,7 +439,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -397,40 +450,21 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -445,7 +479,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -458,7 +491,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -476,7 +508,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -489,24 +520,39 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -553,7 +599,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -568,7 +613,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dev": true, "license": "ISC", "dependencies": { "es5-ext": "^0.10.64", @@ -587,6 +631,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -651,14 +704,12 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", - "dev": true, "license": "MIT", "dependencies": { "@one-ini/wasm": "0.1.1", @@ -673,17 +724,55 @@ "node": ">=14" } }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -729,7 +818,6 @@ "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "dev": true, "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -746,7 +834,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dev": true, "license": "MIT", "dependencies": { "d": "1", @@ -758,7 +845,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dev": true, "license": "ISC", "dependencies": { "d": "^1.0.2", @@ -772,7 +858,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, "license": "ISC", "dependencies": { "d": "1", @@ -785,7 +870,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -801,7 +885,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, "license": "ISC", "dependencies": { "d": "^1.0.1", @@ -826,7 +909,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, "license": "MIT", "dependencies": { "d": "1", @@ -892,12 +974,41 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, "license": "ISC", "dependencies": { "type": "^2.7.2" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -916,11 +1027,30 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -933,6 +1063,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -955,7 +1099,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -989,7 +1132,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -1023,7 +1165,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -1040,11 +1181,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -1072,9 +1221,17 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1146,7 +1303,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, "license": "ISC" }, "node_modules/ipaddr.js": { @@ -1162,7 +1318,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -1178,7 +1333,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1188,7 +1342,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, "license": "MIT" }, "node_modules/is-property": { @@ -1201,14 +1354,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -1220,11 +1371,28 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", - "dev": true, "license": "MIT", "dependencies": { "config-chain": "^1.1.13", @@ -1246,7 +1414,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -1256,7 +1423,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -1290,7 +1456,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", - "dev": true, "license": "MIT", "dependencies": { "es5-ext": "~0.10.2" @@ -1333,7 +1498,6 @@ "version": "0.4.17", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", - "dev": true, "license": "ISC", "dependencies": { "d": "^1.0.2", @@ -1358,6 +1522,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "license": "MIT", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1401,26 +1589,21 @@ } }, "node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -1510,7 +1693,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true, "license": "ISC" }, "node_modules/node-cron": { @@ -1529,7 +1711,6 @@ "version": "7.2.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, "license": "ISC", "dependencies": { "abbrev": "^2.0.0" @@ -1569,7 +1750,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parseurl": { @@ -1585,7 +1765,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1595,14 +1774,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -1619,7 +1796,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -1638,7 +1814,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, "license": "ISC" }, "node_modules/proxy-addr": { @@ -1654,6 +1829,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1697,7 +1878,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1707,7 +1887,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -1878,7 +2057,6 @@ "version": "6.6.2", "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.2.tgz", "integrity": "sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg==", - "dev": true, "license": "MIT", "dependencies": { "cli-color": "^2.0.3", @@ -1954,7 +2132,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -1967,7 +2144,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2049,7 +2225,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2080,7 +2255,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2099,7 +2273,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2114,7 +2287,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2124,14 +2296,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2144,7 +2314,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -2161,7 +2330,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2174,17 +2342,27 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2197,7 +2375,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", - "dev": true, "license": "ISC", "dependencies": { "es5-ext": "^0.10.64", @@ -2226,7 +2403,6 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true, "license": "ISC" }, "node_modules/type-is": { @@ -2260,7 +2436,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", - "dev": true, "license": "MIT", "dependencies": { "bluebird": "^3.7.2" @@ -2279,7 +2454,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -2334,7 +2508,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -2359,7 +2532,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2378,7 +2550,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2396,40 +2567,21 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2444,7 +2596,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2453,11 +2604,22 @@ "node": ">=8" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -2467,7 +2629,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -2486,7 +2647,6 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -2496,7 +2656,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2506,14 +2665,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2528,7 +2685,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index 7d6de29..af53a8f 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,22 @@ "start": "node dist/app.js" }, "dependencies": { + "axios": "^1.7.9", "dotenv": "^16.4.7", + "ejs": "^3.1.10", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "method-override": "^3.0.0", "mysql2": "^3.3.3", - "node-cron": "^3.0.0", - "sequelize": "^6.32.1" + "node-cron": "^3.0.3", + "sequelize": "^6.32.1", + "sequelize-cli": "^6.6.2" }, "devDependencies": { "@types/express": "^4.17.17", + "@types/method-override": "^3.0.0", "@types/node": "^20.4.2", - "sequelize-cli": "^6.6.2", + "@types/node-cron": "^3.0.11", "typescript": "^5.2.2" } } diff --git a/public/thumbnails/EdJHvSsaoVc.jpg b/public/thumbnails/EdJHvSsaoVc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24996c7a1cd84eebbb7bb7daa317b9b6f27e242a GIT binary patch literal 34142 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

kHz-M1TbdWfe*JQ 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 { + try { + // マイグレーション実行 + await runMigrations(); -// データベース接続テスト -sequelize.authenticate() - .then(() => { - console.log("Database connected successfully!"); - }) - .catch((err) => { - console.error("Unable to connect to the database:", err); - process.exit(1); // 接続エラーの場合はプロセスを終了 - }); + // // 定期実行タスクを設定(1時間ごと) + // cron.schedule("0 * * * *", async () => { + // console.log("Updating live schedules..."); + // await updateAllLiveSchedules(); + // }); -app.get("/", (req, res) => { - res.send("Server is running and database connection is active."); -}); -app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); -}); + // ベーシック認証 + app.use( + "/admin", + basicAuth({ + users: { [BASIC_AUTH_USER]: BASIC_AUTH_PASS }, + challenge: true, + }) + ); + + // 管理画面ビューの設定 + app.set("view engine", "ejs"); + app.set("views", "./views"); + + // メソッドオーバーライドを設定 + app.use(methodOverride("_method")); + + // JSON と URL エンコードされたデータのパース + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // 管理画面ルート + app.use("/admin", adminRoutes); + + // 管理画面 API + app.use("/admin/api", apiRoutes); + + // 静的ファイルの提供 + app.use("/thumbnails", express.static("public/thumbnails")); + + // サーバー起動 + app.listen(WEB_PORT, () => { + console.log(`Server is running on http://localhost:${WEB_PORT}`); + }); + } catch (error) { + console.error("Failed to start the server:", error); + process.exit(1); // エラーが発生した場合は終了 + } +}; + +startServer(); diff --git a/src/config/env.ts b/src/config/env.ts index 1403e02..331ebe9 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -3,7 +3,16 @@ import dotenv from "dotenv"; // .env ファイルを読み込む dotenv.config(); -const requiredEnvVars = ["DB_NAME", "DB_USER", "DB_PASS", "DB_HOST"]; +const requiredEnvVars = [ + "DB_NAME", + "DB_USER", + "DB_PASS", + "DB_HOST", + "BASIC_AUTH_USER", + "BASIC_AUTH_PASS", + "WEB_PORT", + "YOUTUBE_API_KEY", +]; requiredEnvVars.forEach((varName) => { if (!process.env[varName]) { @@ -15,3 +24,7 @@ export const DB_NAME = process.env.DB_NAME as string; export const DB_USER = process.env.DB_USER as string; export const DB_PASS = process.env.DB_PASS as string; export const DB_HOST = process.env.DB_HOST as string; +export const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER as string; +export const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS as string; +export const WEB_PORT = parseInt(process.env.WEB_PORT as string, 10); +export const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY as string; diff --git a/migrations/20241225032432-create-channel.js b/src/migrations/20241225032432-create-channel.js similarity index 100% rename from migrations/20241225032432-create-channel.js rename to src/migrations/20241225032432-create-channel.js diff --git a/migrations/20241225032605-create-live-schedule.js b/src/migrations/20241225032605-create-live-schedule.js similarity index 97% rename from migrations/20241225032605-create-live-schedule.js rename to src/migrations/20241225032605-create-live-schedule.js index 96b6404..364f203 100644 --- a/migrations/20241225032605-create-live-schedule.js +++ b/src/migrations/20241225032605-create-live-schedule.js @@ -9,7 +9,7 @@ module.exports = { primaryKey: true, type: Sequelize.INTEGER }, - channelId: { + channel_id: { type: Sequelize.INTEGER }, title: { diff --git a/src/migrations/20241225081006-add-videoId-to-liveSchedule.js b/src/migrations/20241225081006-add-videoId-to-liveSchedule.js new file mode 100644 index 0000000..7474b76 --- /dev/null +++ b/src/migrations/20241225081006-add-videoId-to-liveSchedule.js @@ -0,0 +1,12 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("LiveSchedules", "video_id", { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("LiveSchedules", "video_id"); + }, +}; diff --git a/src/migrations/20241225084215-add-channel-handle-to-channels.js b/src/migrations/20241225084215-add-channel-handle-to-channels.js new file mode 100644 index 0000000..831bb0f --- /dev/null +++ b/src/migrations/20241225084215-add-channel-handle-to-channels.js @@ -0,0 +1,12 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("Channels", "channel_handle", { + type: Sequelize.STRING, + unique: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("Channels", "channel_handle"); + }, +}; diff --git a/src/models/channel.ts b/src/models/channel.ts new file mode 100644 index 0000000..4cf24ad --- /dev/null +++ b/src/models/channel.ts @@ -0,0 +1,65 @@ +import { Sequelize, Model, DataTypes, Optional } from "sequelize"; + +// チャンネルの属性インターフェース +interface ChannelAttributes { + id?: number; // 自動生成される場合はオプショナル + name: string; // チャンネル名(任意の説明用) + channel_handle: string; // チャンネルハンドル(@example) + youtube_id: string; // チャンネルID (UC_xxx...) + createdAt?: Date; + updatedAt?: Date; +} + +// オプショナル属性(作成時に不要な属性を定義) +interface ChannelCreationAttributes extends Optional {} + +// チャンネルモデルクラス +class Channel + extends Model + implements ChannelAttributes { + public id?: number; + public name!: string; + public channel_handle!: string; + public youtube_id!: string; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + // 関連付けメソッド + static associate(models: any) { + // 1つのチャンネルが複数のライブスケジュールを持つ + Channel.hasMany(models.LiveSchedule, { foreignKey: "channel_id" }); + } +} + +// モデルを初期化 +export default (sequelize: Sequelize) => { + Channel.init( + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + channel_handle: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + youtube_id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + }, + { + sequelize, + modelName: "Channel", + } + ); + + return Channel; +}; diff --git a/src/models/index.ts b/src/models/index.ts index e786e57..256ddde 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,5 +1,7 @@ +import { Sequelize, DataTypes } from "sequelize"; import { DB_NAME, DB_USER, DB_PASS, DB_HOST } from "../config/env"; -import { Sequelize } from "sequelize"; +import ChannelModel from "./channel"; +import LiveScheduleModel from "./liveschedule"; const sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, { host: DB_HOST, @@ -7,4 +9,14 @@ const sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, { logging: console.log, }); -export default sequelize; +// モデルを初期化 +const Channel = ChannelModel(sequelize); +const LiveSchedule = LiveScheduleModel(sequelize); + +// 関連付け +Channel.associate?.({ LiveSchedule }); +LiveSchedule.associate?.({ Channel }); + +// エクスポート +export { sequelize, Channel, LiveSchedule }; +export default { sequelize, Channel, LiveSchedule }; diff --git a/src/models/liveschedule.ts b/src/models/liveschedule.ts new file mode 100644 index 0000000..11d011b --- /dev/null +++ b/src/models/liveschedule.ts @@ -0,0 +1,70 @@ +import { Model, Sequelize, DataTypes, Optional } from "sequelize"; + +// ライブスケジュールの属性インターフェース +interface LiveScheduleAttributes { + id?: number; + video_id: string; + channel_id: number; + title: string; + start_time: Date; + thumbnail_url: string | null; + createdAt?: Date; + updatedAt?: Date; +} + +interface LiveScheduleCreationAttributes extends Optional {} + +class LiveSchedule extends Model + implements LiveScheduleAttributes { + public id?: number; + public video_id!: string; + public channel_id!: number; + public title!: string; + public start_time!: Date; + public thumbnail_url!: string; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + static associate(models: any) { + LiveSchedule.belongsTo(models.Channel, { foreignKey: "channel_id" }); + } +} + +export default (sequelize: Sequelize) => { + LiveSchedule.init( + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + video_id: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + channel_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + start_time: { + type: DataTypes.DATE, + allowNull: false, + }, + thumbnail_url: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + modelName: "LiveSchedule", + } + ); + + return LiveSchedule; +}; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index e69de29..a9397b1 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -0,0 +1,143 @@ +import { Router } from "express"; +import { Channel } from "../models"; +import { getChannelIdByHandle } from "../utils/youtube"; +import { updateLiveSchedulesForChannel } from "../services/liveScheduleService"; + + +const router = Router(); + +// チャンネル一覧表示 +router.get("/channels", async (req, res) => { + try { + const channels = await Channel.findAll(); + res.render("admin/channels", { title: "Channels", channels }); + } catch (error) { + console.error(error); + res.status(500).send("Failed to load channels"); + } +}); + +// チャンネル登録フォームの表示 +router.get("/channels/new", (req, res) => { + res.render("admin/new-channel", { title: "Add New Channel" }); +}); + +// チャンネル登録処理 +router.post("/channels/new", async (req, res) => { + try { + const { name, channel_handle, youtube_id } = req.body; + + // 必須項目のバリデーション + if (!channel_handle && !youtube_id) { + return res + .status(400) + .send("Either 'channel_handle' or 'youtube_id' must be provided."); + } + + let resolvedYoutubeId = youtube_id; + let resolvedChannelHandle = channel_handle; + + // チャンネルハンドルから YouTube ID を取得 + if (!youtube_id && channel_handle) { + resolvedYoutubeId = await getChannelIdByHandle(channel_handle); + if (!resolvedYoutubeId) { + return res.status(400).send("Failed to fetch YouTube ID from handle."); + } + } + + // チャンネルIDからハンドルを取得する場合も将来的に実装可能 + // if (!channel_handle && youtube_id) { + // resolvedChannelHandle = await getChannelHandleById(youtube_id); + // } + + // チャンネルを保存 + await Channel.create({ + name, + channel_handle: resolvedChannelHandle, + youtube_id: resolvedYoutubeId, + }); + + res.redirect("/admin/channels"); + } catch (error) { + console.error("Failed to create channel:", error); + res.status(500).send("An error occurred while creating the channel."); + } +}); + + +// チャンネル編集フォームの表示 +router.get("/channels/:id/edit", async (req, res) => { + try { + const { id } = req.params; + const channel = await Channel.findByPk(id); + + if (!channel) { + return res.status(404).send("Channel not found"); + } + + res.render("admin/edit-channel", { title: "Edit Channel", channel }); + } catch (error) { + console.error(error); + res.status(500).send("Failed to load channel for editing"); + } +}); + +// チャンネル更新処理 +router.put("/channels/:id", async (req, res) => { + try { + const { id } = req.params; + const { name, youtube_id } = req.body; + + const channel = await Channel.findByPk(id); + if (!channel) { + return res.status(404).send("Channel not found"); + } + + channel.name = name; + channel.youtube_id = youtube_id; + await channel.save(); + + res.redirect("/admin/channels"); + } catch (error) { + console.error(error); + res.status(500).send("Failed to update channel"); + } +}); + +// チャンネル削除処理 +router.delete("/channels/:id", async (req, res) => { + try { + const { id } = req.params; + const channel = await Channel.findByPk(id); + + if (!channel) { + return res.status(404).send("Channel not found"); + } + + await channel.destroy(); + res.redirect("/admin/channels"); + } catch (error) { + console.error(error); + res.status(500).send("Failed to delete channel"); + } +}); + +// チャンネルのテスト +router.get("/channels/:id/test", async (req, res) => { + try { + const channel = await Channel.findByPk(req.params.id); + + if (!channel) { + return res.status(404).send("Channel not found"); + } + + await updateLiveSchedulesForChannel(channel); + + res.send(`Live schedules successfully updated for channel: ${channel.name}`); + } catch (error) { + console.error("Error updating live schedules for channel:", error); + res.status(500).send("Failed to update live schedules for this channel."); + } +}); + +export default router; diff --git a/src/routes/api.ts b/src/routes/api.ts new file mode 100644 index 0000000..9ea17ef --- /dev/null +++ b/src/routes/api.ts @@ -0,0 +1,75 @@ +import { Router } from "express"; +import { Channel, LiveSchedule } from "../models"; // Sequelize モデルをインポート + +const router = Router(); + +// チャンネル一覧を取得 +router.get("/channels", async (req, res) => { + try { + const channels = await Channel.findAll(); + res.json(channels); + } catch (error) { + res.status(500).json({ error: "Failed to fetch channels" }); + } +}); + +// // チャンネルを登録 +// router.post("/channels", async (req, res) => { +// try { +// const { name, youtube_id } = req.body; +// const newChannel = await Channel.create({ name, youtube_id }); +// res.status(201).json(newChannel); +// } catch (error) { +// res.status(400).json({ error: "Failed to create channel" }); +// } +// }); + +// // チャンネルを更新 +// router.put("/channels/:id", async (req, res) => { +// try { +// const { id } = req.params; +// const { name, youtube_id } = req.body; +// const channel = await Channel.findByPk(id); + +// if (!channel) { +// return res.status(404).json({ error: "Channel not found" }); +// } + +// channel.name = name; +// channel.youtube_id = youtube_id; +// await channel.save(); + +// res.json(channel); +// } catch (error) { +// res.status(400).json({ error: "Failed to update channel" }); +// } +// }); + +// // チャンネルを削除 +// router.delete("/channels/:id", async (req, res) => { +// try { +// const { id } = req.params; +// const channel = await Channel.findByPk(id); + +// if (!channel) { +// return res.status(404).json({ error: "Channel not found" }); +// } + +// await channel.destroy(); +// res.status(204).send(); +// } catch (error) { +// res.status(500).json({ error: "Failed to delete channel" }); +// } +// }); + +// ライブスケジュール一覧を取得 +router.get("/live-schedules", async (req, res) => { + try { + const schedules = await LiveSchedule.findAll({ include: [Channel] }); + res.json(schedules); + } catch (error) { + res.status(500).json({ error: "Failed to fetch live schedules" }); + } +}); + +export default router; diff --git a/src/services/liveScheduleService.ts b/src/services/liveScheduleService.ts new file mode 100644 index 0000000..1391002 --- /dev/null +++ b/src/services/liveScheduleService.ts @@ -0,0 +1,57 @@ +import { Channel, LiveSchedule } from "../models"; +import { fetchUpcomingLiveStreams } from "../utils/youtube"; +import { downloadThumbnail } from "../utils/downloadThumbnail"; + +// ライブスケジュールを保存する共通処理 +export const saveLiveSchedules = async (channel_id: number, liveStreams: any[]) => { + try { + const promises = liveStreams.map(async (stream) => { + if (!stream.video_id || !stream.title || !stream.start_time) { + console.error("Invalid stream data, skipping:", stream); + return; + } + + const existingSchedule = await LiveSchedule.findOne({ + where: { video_id: stream.video_id }, + }); + + if (!existingSchedule) { + const cachedThumbnail = await downloadThumbnail(stream.thumbnail_url, stream.video_id).catch((error) => { + console.warn(`Failed to download thumbnail for video_id ${stream.video_id}:`, error); + return null; + }); + + await LiveSchedule.create({ + channel_id, + video_id: stream.video_id, + title: stream.title, + start_time: new Date(stream.start_time), + thumbnail_url: cachedThumbnail, + }); + } + }); + + await Promise.all(promises); + console.log("Live schedules saved successfully for channel_id:", channel_id); + } catch (error) { + console.error("Failed to save live schedules:", error); + throw error; + } +}; + +// 指定したチャンネルのスケジュールを更新 +export const updateLiveSchedulesForChannel = async (channel: InstanceType) => { + try { + if (!channel.youtube_id) { + throw new Error(`YouTube ID is missing for channel: ${channel.name}`); + } + + const liveStreams = await fetchUpcomingLiveStreams(channel.youtube_id); + await saveLiveSchedules(channel.id!, liveStreams); + + console.log(`Live schedules updated for channel: ${channel.name}`); + } catch (error) { + console.error(`Failed to update live schedules for channel ${channel.name}:`, error); + throw error; + } +}; diff --git a/src/services/updateLiveSchedules.ts b/src/services/updateLiveSchedules.ts new file mode 100644 index 0000000..8fbebf1 --- /dev/null +++ b/src/services/updateLiveSchedules.ts @@ -0,0 +1,17 @@ +import { Channel } from "../models"; +import { updateLiveSchedulesForChannel } from "./liveScheduleService"; + +export const updateAllLiveSchedules = async () => { + try { + const channels = await Channel.findAll(); + + for (const channel of channels) { + await updateLiveSchedulesForChannel(channel); + } + + console.log("All live schedules updated successfully!"); + } catch (error) { + console.error("Failed to update live schedules:", error); + throw error; + } +}; diff --git a/src/utils/downloadThumbnail.ts b/src/utils/downloadThumbnail.ts new file mode 100644 index 0000000..7c9e7b3 --- /dev/null +++ b/src/utils/downloadThumbnail.ts @@ -0,0 +1,32 @@ +import fs from "fs"; +import path from "path"; +import axios from "axios"; + +const THUMBNAIL_DIR = path.resolve(__dirname, "../../public/thumbnails"); + +if (!fs.existsSync(THUMBNAIL_DIR)) { + fs.mkdirSync(THUMBNAIL_DIR, { recursive: true }); +} + +export const downloadThumbnail = async (url: string, video_id: string) => { + const filePath = path.join(THUMBNAIL_DIR, `${video_id}.jpg`); + + if (fs.existsSync(filePath)) { + return `/thumbnails/${video_id}.jpg`; // キャッシュが存在する場合 + } + + try { + const response = await axios.get(url, { responseType: "stream" }); + const writer = fs.createWriteStream(filePath); + + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on("finish", () => resolve(`/thumbnails/${video_id}.jpg`)); + writer.on("error", reject); + }); + } catch (error) { + console.error(`Failed to download thumbnail for ${video_id}:`, error); + throw error; + } +}; diff --git a/src/utils/migrate.ts b/src/utils/migrate.ts new file mode 100644 index 0000000..03fc04d --- /dev/null +++ b/src/utils/migrate.ts @@ -0,0 +1,25 @@ +import dotenv from "dotenv"; +dotenv.config(); + +import { exec } from "child_process"; +import util from "util"; + +const execPromise = util.promisify(exec); + +export const runMigrations = async () => { + try { + console.log("Starting migrations..."); + const { stdout, stderr } = await execPromise( + "npx sequelize-cli db:migrate", + { env: process.env } // 環境変数を渡す + ); + + if (stdout) console.log("Migration output:", stdout); + if (stderr) console.error("Migration error:", stderr); + + console.log("Migrations completed successfully!"); + } catch (error) { + console.error("Failed to run migrations:", error); + throw error; + } +}; diff --git a/src/utils/youtube.ts b/src/utils/youtube.ts new file mode 100644 index 0000000..c707716 --- /dev/null +++ b/src/utils/youtube.ts @@ -0,0 +1,54 @@ +import axios from "axios"; +import { YOUTUBE_API_KEY } from "../config/env"; + +const BASE_URL = "https://www.googleapis.com/youtube/v3"; + +// ライブスケジュールを取得する関数 +export const fetchUpcomingLiveStreams = async (youtube_id: string) => { + try { + const response = await axios.get(`${BASE_URL}/search`, { + params: { + part: "snippet", + channelId: youtube_id, + eventType: "upcoming", + type: "video", + maxResults: 10, + key: YOUTUBE_API_KEY, + }, + }); + + const items = response.data.items || []; + return items.map((item: any) => ({ + video_id: item.id?.videoId, + title: item.snippet?.title, + start_time: item.snippet?.publishedAt, + thumbnail_url: item.snippet?.thumbnails?.high?.url, + })); + } catch (error) { + console.error(`Failed to fetch live streams for channel ${youtube_id}:`, error); + throw error; + } +}; + +// チャンネルハンドルからチャンネルIDを取得 +export const getChannelIdByHandle = async (channelHandle: string): Promise => { + try { + const response = await axios.get(`${BASE_URL}/channels`, { + params: { + part: "id", + forHandle: channelHandle, // forHandle パラメータを使用 + key: YOUTUBE_API_KEY, + }, + }); + + if (response.data.items && response.data.items.length > 0) { + return response.data.items[0].id; + } else { + console.error("No items found in API response:", response.data); + return null; + } + } catch (error: any) { + console.error("Failed to fetch channel ID:", error.response?.data || error.message); + return null; + } +}; diff --git a/views/admin/channels.ejs b/views/admin/channels.ejs new file mode 100644 index 0000000..dd084f2 --- /dev/null +++ b/views/admin/channels.ejs @@ -0,0 +1,44 @@ + + + + + + Channel List + + +

Channel List

+ Add New Channel + + + + + + + + + + + + <% channels.forEach(channel => { %> + + + + + + + + <% }) %> + +
IDNameChannel HandleYouTube IDActions
<%= channel.id %><%= channel.name %><%= channel.channel_handle %><%= channel.youtube_id %> +
+ +
+
+ +
+
+ +
+
+ + diff --git a/views/admin/edit-channel.ejs b/views/admin/edit-channel.ejs new file mode 100644 index 0000000..fca4341 --- /dev/null +++ b/views/admin/edit-channel.ejs @@ -0,0 +1,23 @@ + + + + + + <%= title %> + + +

<%= title %>

+
+ + +
+ + +
+ + +
+ +
+ + diff --git a/views/admin/index.ejs b/views/admin/index.ejs new file mode 100644 index 0000000..2a7c4cd --- /dev/null +++ b/views/admin/index.ejs @@ -0,0 +1,12 @@ + + + + + + <%= title %> + + +

<%= title %>

+

Welcome to the Admin Dashboard!

+ + diff --git a/views/admin/new-channel.ejs b/views/admin/new-channel.ejs new file mode 100644 index 0000000..1216f7a --- /dev/null +++ b/views/admin/new-channel.ejs @@ -0,0 +1,25 @@ + + + + + + Add New Channel + + +

Add New Channel

+
+ + +
+ + +
+ + +
+

Note: Either 'Channel Handle' or 'YouTube ID' must be provided.

+ +
+ Back to Channel List + + diff --git a/views/admin/test-result.ejs b/views/admin/test-result.ejs new file mode 100644 index 0000000..3a15556 --- /dev/null +++ b/views/admin/test-result.ejs @@ -0,0 +1,27 @@ + + + + + + <%= title %> + + +

<%= title %>

+

Channel: <%= channel.name %> (<%= channel.youtube_id %>)

+

Live Streams

+ <% if (liveStreams.length === 0) { %> +

No upcoming live streams found.

+ <% } else { %> +
    + <% liveStreams.forEach(stream => { %> +
  • + Title: <%= stream.title %>
    + Start Time: <%= new Date(stream.start_time).toLocaleString() %>
    + Thumbnail: Thumbnail +
  • + <% }) %> +
+ <% } %> + Back to Channel List + + diff --git a/wait-for-it.sh b/wait-for-it.sh index 9135392..1da1cbd 100644 --- a/wait-for-it.sh +++ b/wait-for-it.sh @@ -9,7 +9,6 @@ port="$1" shift until nc -z "$host" "$port"; do - echo "Waiting for $host:$port to be ready..." sleep 1 done