first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:25-alpine
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache chromium && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm config set registry https://repo.huaweicloud.com/repository/npm/ && \
|
||||||
|
npm config set fetch-retries 3 && \
|
||||||
|
npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV CHROME_PATH=/usr/bin/chromium
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
https://tsx.hirok.io/watch-mode
|
||||||
|
|
||||||
|
docker compose down && docker compose up -d --build
|
||||||
6
db.json
Normal file
6
db.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"setting": {
|
||||||
|
"count": "46",
|
||||||
|
"time": "2026/5/27 23:37:18"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
dy-monitor:
|
||||||
|
build: .
|
||||||
|
container_name: dy-monitor
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
# 每30秒重新运行一次检查,默认每天早上8点
|
||||||
|
# - CRON=*/30 * * * * *
|
||||||
|
# 每30秒重新运行一次检查,默认每天早上8点
|
||||||
|
# - CRON=*/30 * * * * *
|
||||||
|
# - DOUYIN_PAGE=抖音主播主页地址
|
||||||
|
- PAGE_TIMEOUT=30000
|
||||||
|
# Chrome 路径
|
||||||
|
- CHROME_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
|
# 可选:覆盖邮箱、URL
|
||||||
|
# - MAIL_HOST=xxx@qq.com
|
||||||
|
# - MAIL_PORT=xxx@qq.com
|
||||||
|
# - MAIL_SECURE=xxx@qq.com
|
||||||
|
# - MAIL_USER=xxx
|
||||||
|
# - MAIL_PASS=xxx
|
||||||
|
|
||||||
|
# # 持久化数据库文件
|
||||||
|
# volumes:
|
||||||
|
# - ./db.json:/app/db.json
|
||||||
1003
package-lock.json
generated
Normal file
1003
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "dy-not",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.9.1",
|
||||||
|
"@types/node-schedule": "^2.1.8",
|
||||||
|
"@types/nodemailer": "^8.0.0",
|
||||||
|
"lowdb": "^7.0.1",
|
||||||
|
"puppeteer-core": "^25.1.0",
|
||||||
|
"tsx": "^4.22.3",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"nodemailer": "^8.0.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/config.ts
Normal file
33
src/config.ts
Normal file
File diff suppressed because one or more lines are too long
42
src/db/index.ts
Normal file
42
src/db/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { JSONFilePreset, } from 'lowdb/node'
|
||||||
|
import { Low } from 'lowdb'
|
||||||
|
|
||||||
|
// 定义数据结构:setting 是对象,不是数组
|
||||||
|
type Data = {
|
||||||
|
setting: {
|
||||||
|
count: string | number;
|
||||||
|
time: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DbJson {
|
||||||
|
private defaultData: Data = {
|
||||||
|
setting: {
|
||||||
|
count: 0,
|
||||||
|
time: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db!: Low<Data>;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init () {
|
||||||
|
this.db = await JSONFilePreset<Data>('db.json', this.defaultData)
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(params: Data['setting']) {
|
||||||
|
this.db.data.setting = params
|
||||||
|
await this.db.write()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取当前保存的 setting
|
||||||
|
async get() {
|
||||||
|
return this.db.data.setting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DbJson
|
||||||
68
src/index.ts
Normal file
68
src/index.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import puppeteer from 'puppeteer-core';
|
||||||
|
import config from "./config";
|
||||||
|
import db from "./db/index";
|
||||||
|
import mailer from "./utils/mailer";
|
||||||
|
import schedule from 'node-schedule';
|
||||||
|
|
||||||
|
// 把你的逻辑封装成函数
|
||||||
|
async function startTask() {
|
||||||
|
console.log('');
|
||||||
|
console.log('====================================');
|
||||||
|
console.log('⏰ 定时任务开始执行:', new Date().toLocaleString());
|
||||||
|
console.log('====================================');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 启动无头浏览器
|
||||||
|
const browser = await puppeteer.launch(config.launchConfig);
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setExtraHTTPHeaders(config.httpHeaders);
|
||||||
|
|
||||||
|
// 访问页面
|
||||||
|
await page.goto(config.page, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: <number>config.pageTimeOut
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setViewport({ width: 1080, height: 1024 });
|
||||||
|
|
||||||
|
// 抓取数量
|
||||||
|
const number = await page.$eval(
|
||||||
|
'#semiTabpost h2 span:last-child',
|
||||||
|
el => el.textContent.trim()
|
||||||
|
);
|
||||||
|
console.log('1 抓取到主播最新作品数:', number);
|
||||||
|
|
||||||
|
// 获取旧值
|
||||||
|
const { count } = await db.get();
|
||||||
|
console.log('2 数据库里的旧值:', count);
|
||||||
|
|
||||||
|
// 判断是否更新
|
||||||
|
if (String(count) !== String(number)) {
|
||||||
|
console.log('✅ 主播更新作品了');
|
||||||
|
await mailer.send();
|
||||||
|
await db.set({
|
||||||
|
count: number,
|
||||||
|
time: new Date().toLocaleString()
|
||||||
|
});
|
||||||
|
console.log('✅ 已更新到数据库');
|
||||||
|
} else {
|
||||||
|
console.log('❌ 主播没有更新作品');
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('✅ 本次任务执行完成\n');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 任务执行出错:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log('✅ 程序已启动,执行任务,定时规则:', config.cron);
|
||||||
|
|
||||||
|
schedule.scheduleJob(config.cron, async () => {
|
||||||
|
await startTask();
|
||||||
|
});
|
||||||
|
|
||||||
|
// startTask()
|
||||||
41
src/utils/mailer.ts
Normal file
41
src/utils/mailer.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
|
||||||
|
class Mailer {
|
||||||
|
transporter: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.transporter = nodemailer.createTransport(config.mailer);
|
||||||
|
this.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async verify() {
|
||||||
|
try {
|
||||||
|
await this.transporter.verify();
|
||||||
|
console.log("✅ 邮件服务已经初始化成功...");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ 邮件服务初始化失败: ", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async send() {
|
||||||
|
console.log('准备发送邮件通知!');
|
||||||
|
try {
|
||||||
|
const result = await this.transporter.sendMail({
|
||||||
|
from: '2271608011@qq.com', // sender address
|
||||||
|
to: "2271608011@qq.com", // list of recipients
|
||||||
|
subject: "小🐰碎碎念 - 更新了!", // subject line
|
||||||
|
html: `您关注的博主,更新啦!<a href="${config.page}">抖音主页</a>`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 邮件发送成功:', result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 邮件发送失败:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Mailer
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
|
||||||
|
// Treat files as modules even if it doesn't use import/export
|
||||||
|
"moduleDetection": "force",
|
||||||
|
|
||||||
|
// Ignore module structure
|
||||||
|
"module": "Preserve",
|
||||||
|
|
||||||
|
// Allow JSON modules to be imported
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
// Allow JS files to be imported from TS and vice versa
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Use correct ESM import behavior
|
||||||
|
"esModuleInterop": true,
|
||||||
|
|
||||||
|
// Disallow features that require cross-file awareness
|
||||||
|
"isolatedModules": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user