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 0000000..24996c7 Binary files /dev/null and b/public/thumbnails/EdJHvSsaoVc.jpg differ diff --git a/src/app.ts b/src/app.ts index 4966fc7..9c78104 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,25 +1,64 @@ +import { WEB_PORT, BASIC_AUTH_USER, BASIC_AUTH_PASS } from "./config/env"; +import cron from "node-cron"; import express from "express"; -import sequelize from "./models"; +import apiRoutes from "./routes/api"; +import adminRoutes from "./routes/admin"; +import basicAuth from "express-basic-auth"; +import { runMigrations } from "./utils/migrate"; +import methodOverride from "method-override"; +import { updateAllLiveSchedules } from "./services/updateLiveSchedules"; const app = express(); -const port = 3000; -app.use(express.json()); +const startServer = async () => { + 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