文章

动漫管理系统

动漫管理系统

动漫管理系统 API

下面是一个完整的 Node.js 后端程序,可以管理动漫文件夹结构、播放记录,并提供相关操作接口。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
anime-manager/
├── data/
│   └── animeData.json (自动生成)
├── controllers/
│   ├── animeController.js
│   └── historyController.js
├── routes/
│   ├── animeRoutes.js
│   └── historyRoutes.js
├── utils/
│   ├── fileScanner.js
│   └── dataManager.js
├── app.js
└── package.json

安装依赖

1
npm install express fs-extra path body-parser cors

代码实现

1. app.js (主入口文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const animeRoutes = require('./routes/animeRoutes');
const historyRoutes = require('./routes/historyRoutes');
const fileScanner = require('./utils/fileScanner');
const dataManager = require('./utils/dataManager');

const app = express();
const ANIME_DIR = './anime'; // 动漫文件夹路径
const DATA_FILE = './data/animeData.json'; // 数据存储文件

// 中间件
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 初始化扫描动漫文件夹
async function initialize() {
  try {
    const animeData = await fileScanner.scanAnimeFolder(ANIME_DIR);
    await dataManager.saveData(DATA_FILE, animeData);
    console.log('动漫数据初始化完成');
  } catch (err) {
    console.error('初始化失败:', err);
  }
}

// 路由
app.use('/api/anime', animeRoutes);
app.use('/api/history', historyRoutes);

// 错误处理
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: '服务器错误' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => {
  await initialize();
  console.log(`服务器运行在端口 ${PORT}`);
});

2. utils/fileScanner.js (文件扫描工具)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const fs = require('fs-extra');
const path = require('path');

module.exports = {
  // 扫描动漫文件夹
  async scanAnimeFolder(baseDir) {
    try {
      const animeData = {};
      const animeFolders = await fs.readdir(baseDir);
      
      for (const animeName of animeFolders) {
        const animePath = path.join(baseDir, animeName);
        const stats = await fs.stat(animePath);
        
        if (stats.isDirectory()) {
          animeData[animeName] = {
            episodes: []
          };
          
          const episodeFiles = await fs.readdir(animePath);
          for (const episodeName of episodeFiles) {
            const episodePath = path.join(animePath, episodeName);
            const epStats = await fs.stat(episodePath);
            
            if (!epStats.isDirectory()) {
              animeData[animeName].episodes.push({
                name: episodeName,
                path: episodePath,
                playTime: 0 // 默认播放时长为0
              });
            }
          }
        }
      }
      
      return animeData;
    } catch (err) {
      throw err;
    }
  }
};

3. utils/dataManager.js (数据管理工具)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('fs-extra');
const path = require('path');

module.exports = {
  // 保存数据到文件
  async saveData(filePath, data) {
    try {
      await fs.ensureDir(path.dirname(filePath));
      await fs.writeJson(filePath, data, { spaces: 2 });
    } catch (err) {
      throw err;
    }
  },
  
  // 从文件读取数据
  async loadData(filePath) {
    try {
      return await fs.readJson(filePath);
    } catch (err) {
      if (err.code === 'ENOENT') {
        return {};
      }
      throw err;
    }
  }
};

4. controllers/animeController.js (动漫控制器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const fs = require('fs-extra');
const path = require('path');
const dataManager = require('../utils/dataManager');
const fileScanner = require('../utils/fileScanner');

const DATA_FILE = './data/animeData.json';
const ANIME_DIR = './anime';

module.exports = {
  // 获取所有动漫
  async getAllAnime(req, res) {
    try {
      const data = await dataManager.loadData(DATA_FILE);
      const animeList = Object.keys(data).map(animeName => ({
        name: animeName,
        episodeCount: data[animeName].episodes.length
      }));
      res.json(animeList);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  },
  
  // 获取动漫详情
  async getAnimeDetails(req, res) {
    try {
      const { animeName } = req.params;
      const data = await dataManager.loadData(DATA_FILE);
      
      if (!data[animeName]) {
        return res.status(404).json({ error: '动漫未找到' });
      }
      
      res.json(data[animeName]);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  },
  
  // 重命名动漫
  async renameAnime(req, res) {
    try {
      const { oldName, newName } = req.body;
      const data = await dataManager.loadData(DATA_FILE);
      
      if (!data[oldName]) {
        return res.status(404).json({ error: '动漫未找到' });
      }
      
      // 重命名文件夹
      await fs.rename(
        path.join(ANIME_DIR, oldName),
        path.join(ANIME_DIR, newName)
      );
      
      // 更新数据
      data[newName] = data[oldName];
      delete data[oldName];
      await dataManager.saveData(DATA_FILE, data);
      
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  },
  
  // 重命名剧集
  async renameEpisode(req, res) {
    try {
      const { animeName, oldName, newName } = req.body;
      const data = await dataManager.loadData(DATA_FILE);
      
      if (!data[animeName]) {
        return res.status(404).json({ error: '动漫未找到' });
      }
      
      const episodeIndex = data[animeName].episodes.findIndex(ep => ep.name === oldName);
      if (episodeIndex === -1) {
        return res.status(404).json({ error: '剧集未找到' });
      }
      
      // 重命名文件
      await fs.rename(
        path.join(ANIME_DIR, animeName, oldName),
        path.join(ANIME_DIR, animeName, newName)
      );
      
      // 更新数据
      data[animeName].episodes[episodeIndex].name = newName;
      data[animeName].episodes[episodeIndex].path = path.join(ANIME_DIR, animeName, newName);
      await dataManager.saveData(DATA_FILE, data);
      
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  }
};

5. controllers/historyController.js (播放记录控制器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const dataManager = require('../utils/dataManager');

const DATA_FILE = './data/animeData.json';

module.exports = {
  // 更新播放记录
  async updatePlayTime(req, res) {
    try {
      const { animeName, episodeName, playTime } = req.body;
      const data = await dataManager.loadData(DATA_FILE);
      
      if (!data[animeName]) {
        return res.status(404).json({ error: '动漫未找到' });
      }
      
      const episode = data[animeName].episodes.find(ep => ep.name === episodeName);
      if (!episode) {
        return res.status(404).json({ error: '剧集未找到' });
      }
      
      episode.playTime = playTime;
      await dataManager.saveData(DATA_FILE, data);
      
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  },
  
  // 获取播放记录
  async getPlayTime(req, res) {
    try {
      const { animeName, episodeName } = req.params;
      const data = await dataManager.loadData(DATA_FILE);
      
      if (!data[animeName]) {
        return res.status(404).json({ error: '动漫未找到' });
      }
      
      const episode = data[animeName].episodes.find(ep => ep.name === episodeName);
      if (!episode) {
        return res.status(404).json({ error: '剧集未找到' });
      }
      
      res.json({ playTime: episode.playTime });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  }
};

6. routes/animeRoutes.js (动漫路由)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express');
const animeController = require('../controllers/animeController');

const router = express.Router();

// 获取所有动漫
router.post('/', animeController.getAllAnime);

// 获取动漫详情
router.post('/:animeName', animeController.getAnimeDetails);

// 重命名动漫
router.post('/rename', animeController.renameAnime);

// 重命名剧集
router.post('/episode/rename', animeController.renameEpisode);

module.exports = router;

7. routes/historyRoutes.js (播放记录路由)

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const historyController = require('../controllers/historyController');

const router = express.Router();

// 更新播放记录
router.post('/update', historyController.updatePlayTime);

// 获取播放记录
router.get('/:animeName/:episodeName', historyController.getPlayTime);

module.exports = router;

API 文档

动漫相关接口

  1. 获取所有动漫
    • POST /api/anime
    • 返回所有动漫列表及每部动漫的剧集数量
  2. 获取动漫详情
    • POST /api/anime/:animeName
    • 返回指定动漫的所有剧集信息及播放记录
  3. 重命名动漫
    • POST /api/anime/rename
    • 请求体: { oldName: "旧动漫名", newName: "新动漫名" }
  4. 重命名剧集
    • POST /api/anime/episode/rename
    • 请求体: { animeName: "动漫名", oldName: "旧剧集名", newName: "新剧集名" }

播放记录相关接口

  1. 更新播放记录
    • POST /api/history/update
    • 请求体: { animeName: "动漫名", episodeName: "剧集名", playTime: 123 } (播放时间,单位秒)
  2. 获取播放记录
    • GET /api/history/:animeName/:episodeName
    • 返回指定剧集的播放时间

使用说明

  1. 将动漫文件夹放在项目根目录下的 anime 文件夹中
  2. 启动服务器: node app.js
  3. 服务器会自动扫描动漫文件夹并生成数据文件
  4. 通过 API 接口管理动漫和播放记录

这个系统会在启动时自动扫描动漫文件夹结构,并将信息保存在 data/animeData.json 文件中。所有播放记录也会保存在这个文件中。

本文由作者按照 CC BY 4.0 进行授权