This commit is contained in:
2025-04-07 15:42:28 +08:00
commit c023e530e1
12 changed files with 563 additions and 0 deletions

26
.dockerignore Normal file
View File

@@ -0,0 +1,26 @@
node_modules
npm-debug.log
logs
*.log
cache
*.jpg
*.json
!package.json
!package-lock.json
!config/*.json
.git
.github
.gitignore
.vscode
.env
.env.*
README.md
LICENSE
*.md
Dockerfile
docker-compose.yml
.dockerignore

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
logs/
cache/
package-lock.json

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN mkdir -p /app/cache /app/logs && \
chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "app.js"]

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2025 HCha. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
# BingWallpaperAPI
This project is a simple API service that caches the daily Bing wallpaper and provides it for use in other applications. It fetches the wallpaper metadata from the Bing API, downloads the image, and saves it locally.
## Features
- Fetches the daily Bing wallpaper.
- Caches the wallpaper image and metadata.
- Provides an API endpoint to access the wallpaper.
- Automatically updates the wallpaper on a daily basis.
## Installation
1. Clone the repository:
```bash
git clone https://github.com/HChaZZY/BingWallpaperAPI.git
cd BingWallpaperAPI
```
2. Install the dependencies:
```bash
npm install
```
3. **Docker** (Optional)
- Using Docker Compose:
```bash
docker-compose up -d
```
- Using Docker:
```bash
docker build -t bing-wallpaper-api .
docker run -d -p 3000:3000 bing-wallpaper-api
```
## Usage
1. Configure the settings in `config/default.json` if needed:
- Server port and host (`server.port` and `server.host`)
- Bing API URL (`bing.apiUrl` and `bing.baseUrl`)
- Cache paths (`cache.path` and `cache.metadataPath`)
- Update schedule (`schedule.checkInterval`, using cron format)
- Retry settings (`retry.maxAttempts` and `retry.delayMs`)
2. Start the server:
```bash
npm start
```
After successful startup, the console will display the following information:
```
Listening 0.0.0.0:3000
http://localhost:3000 for Wallpaper
```
3. Access wallpaper and health check:
- Wallpaper image: `http://localhost:3000/images`
- Health check: `http://localhost:3000/health`
## System Requirements
- Node.js >= 14.0.0
## Main Dependencies
- Express: Web server framework
- Axios: HTTP client
- node-cron: Scheduled task scheduling
- Winston: Log recording
## License
MIT License
© 2025 HCha. All rights reserved.

96
app.js Normal file
View File

@@ -0,0 +1,96 @@
const express = require('express');
const cron = require('node-cron');
const fs = require('fs');
const path = require('path');
const bingService = require('./src/services/bing.service');
const config = require('./config/default.json');
const { createLogger } = require('./src/utils/logger');
const logger = createLogger('app');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
const PORT = process.env.PORT || config.server.port;
const HOST = config.server.host;
const wallpaperCachePath = path.resolve(config.cache.path);
const metadataCachePath = path.resolve(config.cache.metadataPath);
const cacheDir = path.dirname(wallpaperCachePath);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
logger.info(`Created cache directory: ${cacheDir}`);
}
async function updateWallpaper() {
try {
logger.info('Start checking for wallpaper update...');
const updated = await bingService.checkAndUpdateWallpaper();
if (updated) {
logger.info('Wallpaper updated. Saving metadata...');
} else {
logger.info('Wallpaper does not need updating.');
}
} catch (error) {
logger.error(`Wallpaper update failed: ${error.message}`);
}
}
(async () => {
try {
if (!fs.existsSync(wallpaperCachePath)) {
logger.info('No cached wallpaper found. Downloading...');
await updateWallpaper();
} else {
logger.info(`Found cached wallpaper: ${wallpaperCachePath}`);
}
} catch (error) {
logger.error(`Initial wallpaper load failed: ${error.message}`);
}
})();
cron.schedule(config.schedule.checkInterval, async () => {
logger.info('Executing scheduled wallpaper update.');
await updateWallpaper();
});
app.get('/images', (req, res) => {
try {
if (fs.existsSync(wallpaperCachePath)) {
res.setHeader('Content-Type', 'image/jpeg');
res.setHeader('Cache-Control', 'public, max-age=3600');
return res.sendFile(wallpaperCachePath);
} else {
logger.warn('No wallpaper found in cache. Returning 404.');
return res.status(404).send('No wallpaper found.');
}
} catch (error) {
logger.error(`Wallpaper request failed: ${error.message}`);
return res.status(500).send('Internal Server Error');
}
});
app.get('/health', (req, res) => {
return res.json({ status: 'OK', version: '1.0.0' });
});
app.listen(PORT, HOST, () => {
logger.info(`Listening ${HOST}:${PORT}`);
logger.info(`http://localhost:${PORT} for Wallpaper`);
});
process.on('SIGTERM', () => {
logger.info('SIGTERM, shutting down gracefully');
process.exit(0);
});
process.on('uncaughtException', (error) => {
logger.error(`Uncaught Exception: ${error.message}`, { stack: error.stack });
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('UnhandledPromiseRejection: ', { reason });
});

21
config/default.json Normal file
View File

@@ -0,0 +1,21 @@
{
"server": {
"port": 3000,
"host": "0.0.0.0"
},
"bing": {
"apiUrl": "https://cn.bing.com/HPImageArchive.aspx?format=js&n=1&idx=0&mkt=zh-CN",
"baseUrl": "https://global.bing.com"
},
"cache": {
"path": "./cache/bing-wallpaper.jpg",
"metadataPath": "./cache/metadata.json"
},
"schedule": {
"checkInterval": "0 * * * *"
},
"retry": {
"maxAttempts": 3,
"delayMs": 5000
}
}

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: bing-wallpaper-api:1.0.0
container_name: bing-wallpaper-api
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- bing_cache:/app/cache
- bing_logs:/app/logs
environment:
- NODE_ENV=production
- PORT=3000
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 1m
timeout: 10s
retries: 3
start_period: 30s
volumes:
bing_cache:
name: bing_wallpaper_cache
bing_logs:
name: bing_wallpaper_logs

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "bing-wallpaper-api",
"version": "1.0.0",
"description": "Cache Bing daily wallpaper and provide API service",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"bing",
"wallpaper",
"api"
],
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.3.4",
"bing-wallpaper-api": "file:",
"express": "^4.18.2",
"node-cron": "^3.0.2",
"winston": "^3.8.2"
},
"devDependencies": {
"nodemon": "^2.0.22"
},
"engines": {
"node": ">=14.0.0"
}
}

27
public/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bing Wallpaper</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: black;
height: 100vh;
width: 100vw;
}
img {
object-fit: contain;
width: 100%;
height: 100%;
display: block;
}
</style>
</head>
<body>
<img src="/images" alt="Bing Wallpaper">
</body>
</html>

View File

@@ -0,0 +1,158 @@
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const config = require('../../config/default.json');
const writeFile = promisify(fs.writeFile);
const { createLogger } = require('../utils/logger');
const logger = createLogger('bing-service');
class BingService {
constructor() {
this.apiUrl = config.bing.apiUrl;
this.baseUrl = config.bing.baseUrl;
this.cachePath = config.cache.path;
this.metadataPath = config.cache.metadataPath;
this.maxRetryAttempts = config.retry.maxAttempts;
this.retryDelay = config.retry.delayMs;
}
async getMetadata() {
let retryCount = 0;
while (retryCount < this.maxRetryAttempts) {
try {
logger.info('Acquiring Bing wallpaper metadata...');
const response = await axios.get(this.apiUrl);
if (response.status !== 200 || !response.data || !response.data.images) {
throw new Error('Invalid response from Bing API');
}
logger.info('Bing wallpaper metadata acquired successfully.');
return response.data;
} catch (error) {
retryCount++;
logger.error(`Failed to acquire Bing wallpaper metadata (Attempt ${retryCount}/${this.maxRetryAttempts}): ${error.message}`);
if (retryCount < this.maxRetryAttempts) {
logger.info(`Retry after ${this.retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
} else {
throw new Error(`Failed to acquire Bing wallpaper metadata, max retries reached: ${error.message}`);
}
}
}
}
generateWallpaperUrl(metadata) {
try {
if (!metadata || !metadata.images || !metadata.images[0]) {
throw new Error('Invalid metadata provided.');
}
const image = metadata.images[0];
const urlBase = image.urlbase;
const uhdUrl = `${urlBase}_UHD.jpg`;
const fullUhdUrl = `${this.baseUrl}${uhdUrl}`;
logger.info(`Generated wallpaper URL: ${fullUhdUrl}`);
return {
url: fullUhdUrl,
startDate: image.startdate,
title: image.title,
copyright: image.copyright
};
} catch (error) {
logger.error(`GenerateWallpaperUrl error: ${error.message}`);
throw error;
}
}
async downloadWallpaper(url) {
try {
logger.info(`Downloading wallpaper from: ${url}`);
const cacheDir = path.dirname(this.cachePath);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
const response = await axios({
method: 'get',
url: url,
responseType: 'arraybuffer'
});
if (response.status !== 200) {
throw new Error(`Download failed with status code: ${response.status}`);
}
await writeFile(this.cachePath, response.data);
logger.info(`Wallpaper saved to: ${this.cachePath}`);
return true;
} catch (error) {
logger.error(`Download wallpaper error: ${error.message}`);
throw error;
}
}
async saveMetadata(metadata) {
try {
const cacheDir = path.dirname(this.metadataPath);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
await writeFile(this.metadataPath, JSON.stringify(metadata, null, 2));
logger.info(`Metadata saved to: ${this.metadataPath}`);
return true;
} catch (error) {
logger.error(`Metadata save error: ${error.message}`);
throw error;
}
}
async checkAndUpdateWallpaper() {
try {
const metadata = await this.getMetadata();
const wallpaperInfo = this.generateWallpaperUrl(metadata);
let needUpdate = true;
if (fs.existsSync(this.metadataPath)) {
try {
const oldMetadata = JSON.parse(fs.readFileSync(this.metadataPath, 'utf8'));
if (oldMetadata.images &&
oldMetadata.images[0] &&
oldMetadata.images[0].startdate === metadata.images[0].startdate) {
needUpdate = false;
logger.info('Wallpaper is up to date. No update needed.');
}
} catch (error) {
logger.warn(`Failed to read old metadata, forced update: ${error.message}`);
}
}
if (needUpdate) {
await this.downloadWallpaper(wallpaperInfo.url);
await this.saveMetadata(metadata);
logger.info('Wallpaper updated successfully.');
return true;
}
return false;
} catch (error) {
logger.error(`Failed to check and update wallpaper: ${error.message}`);
throw error;
}
}
}
module.exports = new BingService();

52
src/utils/logger.js Normal file
View File

@@ -0,0 +1,52 @@
const winston = require('winston');
const path = require('path');
const fs = require('fs');
const logDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
function createLogger(module) {
return winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.printf(info => {
const { timestamp, level, message, ...rest } = info;
const moduleStr = module ? `[${module}]` : '';
const restString = Object.keys(rest).length > 0 ?
`\n${JSON.stringify(rest, null, 2)}` : '';
return `${timestamp} ${level.toUpperCase()} ${moduleStr}: ${message}${restString}`;
})
),
defaultMeta: { service: 'bing-wallpaper-api' },
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 2
})
]
});
}
module.exports = {
createLogger
};