├── .gitignore ├── Dockerfile ├── README.md ├── data └── 115-cookies example.txt ├── frontend ├── .env ├── .npmrc ├── .vscode │ └── settings.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public │ ├── icon.png │ ├── img │ │ ├── archive.svg │ │ ├── artplayer.png │ │ ├── audio.svg │ │ ├── back.svg │ │ ├── code.svg │ │ ├── config.svg │ │ ├── copy.svg │ │ ├── db.svg │ │ ├── default.svg │ │ ├── ebook.svg │ │ ├── excel.svg │ │ ├── fig.png │ │ ├── fileball.svg │ │ ├── folder.svg │ │ ├── home.svg │ │ ├── iina.svg │ │ ├── image.svg │ │ ├── infuse.svg │ │ ├── logo.svg │ │ ├── more.svg │ │ ├── mpv.png │ │ ├── mxplayer.svg │ │ ├── nfo.svg │ │ ├── nplayer.svg │ │ ├── omni.png │ │ ├── pdf.svg │ │ ├── potplayer.svg │ │ ├── ppt.svg │ │ ├── text.svg │ │ ├── video.svg │ │ ├── vlc.svg │ │ └── word.svg │ ├── manifest.json │ └── poppins.otf ├── src │ ├── api │ │ └── index.ts │ ├── assets │ │ ├── fonts │ │ │ └── iconfont │ │ │ │ ├── demo.css │ │ │ │ ├── demo_index.html │ │ │ │ ├── iconfont.css │ │ │ │ ├── iconfont.js │ │ │ │ ├── iconfont.json │ │ │ │ ├── iconfont.svg │ │ │ │ ├── iconfont.ttf │ │ │ │ ├── iconfont.woff │ │ │ │ └── iconfont.woff2 │ │ └── icon │ │ │ ├── attr.svg │ │ │ ├── desc.svg │ │ │ ├── download.svg │ │ │ ├── m3u8.svg │ │ │ ├── more.svg │ │ │ ├── play.svg │ │ │ └── spin.svg │ ├── enums │ │ └── httpEnum.ts │ ├── hooks │ │ ├── useIsMobile.ts │ │ ├── useMessage.ts │ │ └── useRandomId.ts │ ├── http │ │ ├── Axios.ts │ │ ├── axiosCancel.ts │ │ ├── axiosRetry.ts │ │ ├── axiosTransform.ts │ │ ├── checkStatus.ts │ │ ├── helper.ts │ │ └── index.ts │ ├── index.css │ ├── layout │ │ ├── components │ │ │ └── header.tsx │ │ └── index.tsx │ ├── main.tsx │ ├── routes │ │ ├── index.tsx │ │ └── router.tsx │ ├── utils │ │ ├── index.ts │ │ └── is.ts │ ├── views │ │ ├── index │ │ │ ├── components │ │ │ │ ├── FileItem.tsx │ │ │ │ └── PlayerList.tsx │ │ │ └── index.tsx │ │ └── player │ │ │ └── index.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types │ ├── axios.d.ts │ └── index.d.ts └── vite.config.ts ├── makefile ├── requirements.txt ├── server ├── __init__.py ├── file_lister.py └── main.py ├── start.py ├── start.sh └── supervisord.conf /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | static/* 4 | 5 | # 忽略 data/ 文件夹下的所有文件 6 | data/* 7 | # 但不忽略 115-cookies example.txt 8 | !data/115-cookies example.txt 9 | 10 | 11 | venv/ 12 | # Logs 13 | *.log 14 | 115-cookies.txt 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | pnpm-debug.log* 19 | lerna-debug.log* 20 | node_modules 21 | dist 22 | dist-ssr 23 | frontend/node_modules 24 | frontend/dist 25 | frontend/dist-ssr 26 | *.local 27 | .vite/ 28 | 29 | # Editor directories and files 30 | .vscode/* 31 | !.vscode/extensions.json 32 | .idea 33 | .DS_Store 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 阶段 1: 构建前端静态文件 2 | # 使用 Node.js 的官方基础镜像 3 | FROM node:18.12 AS build-stage 4 | 5 | # 设置前端工作目录 6 | WORKDIR /app/frontend 7 | 8 | # 安装pnpm 9 | RUN npm install --global pnpm 10 | 11 | # 复制前端相关的文件 12 | COPY ./frontend/package*.json ./ 13 | COPY ./frontend/pnpm-lock.yaml ./ 14 | 15 | # 安装前端依赖 16 | RUN pnpm install 17 | 18 | # 复制前端代码 19 | COPY ./frontend . 20 | 21 | # 构建前端静态文件 22 | RUN npm run build 23 | 24 | # 清理 pnpm 缓存 25 | RUN pnpm store prune 26 | 27 | # 阶段 2: 设置 Python 环境并复制前端构建产物 28 | # 使用官方 Python 3.11 基础镜像 29 | FROM python:3.11-slim-buster 30 | 31 | # 设置时区环境变量 32 | ENV TZ=Asia/Shanghai 33 | 34 | # 设置 USTC 镜像源 35 | RUN sed -i 's|http://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list 36 | 37 | # 设置工作目录 38 | WORKDIR /app 39 | 40 | # 复制依赖文件 41 | COPY ./requirements.txt /app/requirements.txt 42 | 43 | # 安装依赖和时区设置 44 | RUN apt-get update -y \ 45 | && apt-get install -y --no-install-recommends tzdata \ 46 | && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ 47 | && echo $TZ > /etc/timezone \ 48 | && apt-get install -y --fix-missing build-essential \ 49 | && python -m pip install --upgrade pip \ 50 | && pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r /app/requirements.txt \ 51 | && apt-get remove -y build-essential \ 52 | && apt-get clean \ 53 | && rm -rf /var/lib/apt/lists/* 54 | 55 | # 复制后端代码 56 | COPY ./server /app/server 57 | COPY ./start.sh ./start.py ./supervisord.conf /app/ 58 | 59 | # 从构建阶段复制前端构建产物 60 | COPY --from=build-stage /app/frontend/dist /app/static 61 | 62 | # 暴露容器内部的端口 63 | EXPOSE 9115 64 | 65 | # 启动命令 66 | # CMD ["python", "/app/start.py"] 67 | CMD ["bash", "/app/start.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 115 File Lister 2 | 3 | Figma 设计稿-高保真可交互原型:[移动端](https://www.figma.com/proto/XsOdLNW1WeIlO9buKodqlo/115Filelister?page-id=0%3A1&node-id=411-1764&viewport=-605%2C861%2C0.5&t=RkiOH1qpBqISYwc2-1&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=411%3A1764&show-proto-sidebar=1) + [PC端](https://www.figma.com/proto/XsOdLNW1WeIlO9buKodqlo/115Filelister?page-id=0%3A1&node-id=407-1351&viewport=-605%2C861%2C0.5&t=RkiOH1qpBqISYwc2-1&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=407%3A1351&show-proto-sidebar=1),平时精力有限,欢迎PR. 4 | 5 | ## 部署 6 | 7 | ```yaml 8 | version: '3' 9 | services: 10 | 115_file_lister: 11 | image: alanoo/115_file_lister:latest 12 | container_name: 115_file_lister 13 | restart: always 14 | network_mode: bridge 15 | ports: 16 | - 9115:9115 17 | volumes: 18 | - /appdata/115_file_lister:/app/data 19 | environment: 20 | - path_persistence_commitment="true" 21 | ``` 22 | ```bash 23 | docker run -d \ 24 | --name='115_file_lister' \ 25 | --restart always \ 26 | -p 9115:9115 \ 27 | -e path_persistence_commitment="true" \ 28 | -v '/appdata/115_file_lister':'/app/data' \ 29 | alanoo/115_file_lister:latest 30 | ``` 31 | 32 | 在 `app/data/115-cookies.txt` 设置 `cookie` ,重启,然后浏览器访问 `IP:9115` 33 | 34 | ## 调试 35 | ```bash 36 | # 安装依赖 37 | make install 38 | 39 | # 启动 40 | make dev 41 | ``` 42 | 43 | ### API文档 44 | 后端启动后,浏览器访问 `http://localhost:9115/docs` 查看 API 文档。 45 | 46 | ## 致谢 47 | 后端:本项目的后端部分的核心实现,都来自 [@ChenyangGao](https://github.com/ChenyangGao) 大佬的 [项目](https://github.com/ChenyangGao/web-mount-packs)。本项目只是做了微不足道的搬运而已,感谢! 48 | 49 | 前端:UI页面由 [zkl2333](https://github.com/zkl2333) 大佬帮忙实现,感谢。 -------------------------------------------------------------------------------- /data/115-cookies example.txt: -------------------------------------------------------------------------------- 1 | 复制一份 115-cookies example.txt 并重命名为 115-cookies.txt,并将cookie填在里面 2 | 3 | cookie格式为 cid uid seid -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | # 可以添加一个 .env.local 文件,所有情况下都会加载,但会被 git 忽略 2 | # https://cn.vitejs.dev/guide/env-and-mode#env-files 3 | VITE_API_URL = http://127.0.0.1:9115 -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { "css.lint.unknownAtRules": "ignore" } 2 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | 运行调试 4 | ```bash 5 | make dev 6 | 7 | ``` 8 | 9 | 10 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 11 | 12 | Currently, two official plugins are available: 13 | 14 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 15 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 16 | 17 | ## Expanding the ESLint configuration 18 | 19 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 20 | 21 | - Configure the top-level `parserOptions` property like this: 22 | 23 | ```js 24 | export default { 25 | // other rules... 26 | parserOptions: { 27 | ecmaVersion: 'latest', 28 | sourceType: 'module', 29 | project: ['./tsconfig.json', './tsconfig.node.json'], 30 | tsconfigRootDir: __dirname, 31 | }, 32 | } 33 | ``` 34 | 35 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 36 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 37 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 38 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 24 | 25 | 26 | 115 FileLister 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^5.4.0", 14 | "antd": "^5.14.0", 15 | "artplayer": "^5.1.6", 16 | "artplayer-plugin-hls-quality": "^2.0.0", 17 | "axios": "^1.6.7", 18 | "dayjs": "^1.11.12", 19 | "highlight.js": "^11.9.0", 20 | "hls.js": "^1.5.13", 21 | "lodash-es": "^4.17.21", 22 | "mobx": "^6.12.1", 23 | "mobx-react": "^9.1.0", 24 | "prismjs": "^1.29.0", 25 | "qs": "^6.12.0", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-router-dom": "^6.22.0", 29 | "react-waypoint": "^10.3.0", 30 | "styled-components": "^6.1.8", 31 | "swr": "^2.2.5", 32 | "xterm": "^5.3.0", 33 | "xterm-addon-canvas": "^0.5.0", 34 | "xterm-addon-fit": "^0.8.0", 35 | "xterm-addon-search": "^0.13.0", 36 | "xterm-addon-web-links": "^0.9.0", 37 | "xterm-addon-webgl": "^0.16.0" 38 | }, 39 | "devDependencies": { 40 | "@types/lodash-es": "^4.17.12", 41 | "@types/node": "^20.11.16", 42 | "@types/prismjs": "^1.26.3", 43 | "@types/qs": "^6.9.14", 44 | "@types/react": "^18.2.54", 45 | "@types/react-dom": "^18.2.18", 46 | "@types/react-router-dom": "^5.3.3", 47 | "@typescript-eslint/eslint-plugin": "^6.21.0", 48 | "@typescript-eslint/parser": "^6.21.0", 49 | "@vitejs/plugin-react": "^4.2.1", 50 | "autoprefixer": "^10.4.17", 51 | "eslint": "^8.56.0", 52 | "eslint-plugin-react-hooks": "^4.6.0", 53 | "eslint-plugin-react-refresh": "^0.4.5", 54 | "postcss-import": "^16.1.0", 55 | "tailwindcss": "^3.4.1", 56 | "typescript": "^5.3.3", 57 | "vite": "^5.0.12", 58 | "vite-plugin-svgr": "^4.2.0" 59 | }, 60 | "volta": { 61 | "node": "20.15.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/icon.png -------------------------------------------------------------------------------- /frontend/public/img/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/artplayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/img/artplayer.png -------------------------------------------------------------------------------- /frontend/public/img/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/public/img/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/config.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/img/db.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/public/img/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/ebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/public/img/excel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/fig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/img/fig.png -------------------------------------------------------------------------------- /frontend/public/img/fileball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/public/img/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/public/img/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/img/iina.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /frontend/public/img/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/infuse.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /frontend/public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/img/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/mpv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/img/mpv.png -------------------------------------------------------------------------------- /frontend/public/img/mxplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/img/nfo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/img/nplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/public/img/omni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/img/omni.png -------------------------------------------------------------------------------- /frontend/public/img/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/potplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/public/img/ppt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/img/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /frontend/public/img/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/public/img/vlc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/public/img/word.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "115FileLister", 3 | "name": "115FileLister", 4 | "icons": [ 5 | { 6 | "src": "/icon.png", 7 | "sizes": "any", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#171717", 14 | "background_color": "#171717" 15 | } -------------------------------------------------------------------------------- /frontend/public/poppins.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/public/poppins.otf -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { defHttp } from "/@/http"; 2 | 3 | export interface Ancestor { 4 | id: number; 5 | parent_id: number; 6 | name: string; 7 | is_directory: boolean; 8 | } 9 | 10 | export interface FileInfo { 11 | id: number; 12 | parent_id: number; 13 | name: string; 14 | path: string; 15 | pickcode: string; 16 | is_directory: boolean; 17 | sha1: string | null; 18 | size: number | null; 19 | format_size: string; 20 | ico: string; 21 | ctime: number; 22 | mtime: number; 23 | atime: number; 24 | thumb: string; 25 | star: boolean; 26 | labels: any[]; 27 | score: number; 28 | hidden: boolean; 29 | described: boolean; 30 | ancestors: Ancestor[]; 31 | url?: string; 32 | short_url?: string; 33 | } 34 | 35 | export type FileListResponse = FileInfo[]; 36 | 37 | /** 38 | * 获取文件或目录的属性 39 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 40 | * @param params.id - 文件或目录的 id,优先级高于 path 41 | * @param params.path - 文件或目录的路径,优先级高于 path2 42 | * @returns 返回文件或目录的属性信息 43 | */ 44 | export const getFileAttr = (params: { 45 | pickcode?: string; 46 | id?: number; 47 | path?: string; 48 | }) => 49 | defHttp.get({ 50 | url: `/attr`, 51 | params, 52 | }); 53 | 54 | /** 55 | * 罗列对应目录的所有文件和目录属性 56 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 57 | * @param params.id - 文件或目录的 id,优先级高于 path 58 | * @param params.path - 文件或目录的路径,优先级高于 path2 59 | * @returns 返回目录下的所有文件和目录属性列表 60 | */ 61 | export const getList = (params: { 62 | pickcode?: string; 63 | id?: number; 64 | path?: string; 65 | }) => 66 | defHttp.get({ 67 | url: `/list`, 68 | params, 69 | }); 70 | 71 | /** 72 | * 获取文件或目录的祖先节点列表(包含自己) 73 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 74 | * @param params.id - 文件或目录的 id,优先级高于 path 75 | * @param params.path - 文件或目录的路径,优先级高于 path2 76 | * @returns 返回祖先节点列表 77 | */ 78 | export const getAncestors = (params: { 79 | pickcode?: string; 80 | id?: number; 81 | path?: string; 82 | }) => 83 | defHttp.get({ 84 | url: `/ancestors`, 85 | params, 86 | }); 87 | 88 | /** 89 | * 获取文件或目录的备注 90 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 91 | * @param params.id - 文件或目录的 id,优先级高于 path 92 | * @param params.path - 文件或目录的路径,优先级高于 path2 93 | * @returns 返回文件或目录的备注信息 94 | */ 95 | export const getDesc = (params: { 96 | pickcode?: string; 97 | id?: number; 98 | path?: string; 99 | }) => 100 | defHttp.get({ 101 | url: `/desc`, 102 | params, 103 | }); 104 | 105 | /** 106 | * 获取文件的下载链接 107 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 108 | * @param params.id - 文件或目录的 id,优先级高于 path 109 | * @param params.path - 文件或目录的路径,优先级高于 path2 110 | * @param params.web - 是否使用 web 接口获取下载链接,如果文件被封禁但小于 115 MB,启用此选项可成功下载文件 111 | * @returns 返回文件的下载链接 112 | */ 113 | export const getUrl = (params: { 114 | pickcode?: string; 115 | id?: number; 116 | path?: string; 117 | web?: boolean; 118 | }) => 119 | defHttp.get({ 120 | url: `/url`, 121 | params, 122 | }); 123 | 124 | /** 125 | * 下载文件 126 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 127 | * @param params.id - 文件或目录的 id,优先级高于 path 128 | * @param params.path - 文件或目录的路径,优先级高于 path2 129 | * @param params.web - 是否使用 web 接口获取下载链接,如果文件被封禁但小于 115 MB,启用此选项可成功下载文件 130 | * @returns 返回下载的文件 131 | */ 132 | export const fileDownload = (params: { 133 | pickcode?: string; 134 | id?: number; 135 | path?: string; 136 | web?: boolean; 137 | }) => 138 | defHttp.get({ 139 | url: `/download`, 140 | params, 141 | }); 142 | 143 | /** 144 | * 获取音视频的 m3u8 文件 145 | * @param params.pickcode - 文件或目录的 pickcode,优先级高于 id 146 | * @param params.id - 文件或目录的 id,优先级高于 path 147 | * @param params.path - 文件或目录的路径,优先级高于 path2 148 | * @param params.definition - 分辨率(3 - HD, 4 - UD) 149 | * @returns 返回音视频的 m3u8 文件 150 | */ 151 | export const fileM3u8 = (params: { 152 | pickcode?: string; 153 | id?: number; 154 | path?: string; 155 | definition?: number; 156 | }) => 157 | defHttp.get({ 158 | url: `/m3u8`, 159 | params, 160 | }); 161 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/demo_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iconfont Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 36 | 37 | 38 |
39 |

40 | 41 | 42 |

43 | 53 |
54 |
55 |
    56 | 57 |
  • 58 | 59 |
    podcast
    60 |
    
    61 |
  • 62 | 63 |
  • 64 | 65 |
    logs
    66 |
    
    67 |
  • 68 | 69 |
  • 70 | 71 |
    arrange
    72 |
    
    73 |
  • 74 | 75 |
  • 76 | 77 |
    subscribe
    78 |
    
    79 |
  • 80 | 81 |
  • 82 | 83 |
    settings
    84 |
    
    85 |
  • 86 | 87 |
88 |
89 |

Unicode 引用

90 |
91 | 92 |

Unicode 是字体在网页端最原始的应用方式,特点是:

93 |
    94 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 95 |
  • 默认情况下不支持多色,直接添加多色图标会自动去色。
  • 96 |
97 |
98 |

注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

99 |
100 |

Unicode 使用步骤如下:

101 |

第一步:拷贝项目下面生成的 @font-face

102 |
@font-face {
104 |   font-family: 'iconfont';
105 |   src: url('iconfont.woff2?t=1711384457279') format('woff2'),
106 |        url('iconfont.woff?t=1711384457279') format('woff'),
107 |        url('iconfont.ttf?t=1711384457279') format('truetype'),
108 |        url('iconfont.svg?t=1711384457279#iconfont') format('svg');
109 | }
110 | 
111 |

第二步:定义使用 iconfont 的样式

112 |
.iconfont {
114 |   font-family: "iconfont" !important;
115 |   font-size: 16px;
116 |   font-style: normal;
117 |   -webkit-font-smoothing: antialiased;
118 |   -moz-osx-font-smoothing: grayscale;
119 | }
120 | 
121 |

第三步:挑选相应图标并获取字体编码,应用于页面

122 |
123 | <span class="iconfont">&#x33;</span>
125 | 
126 |
127 |

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

128 |
129 |
130 |
131 |
132 |
    133 | 134 |
  • 135 | 136 |
    137 | podcast 138 |
    139 |
    .icon-podcast 140 |
    141 |
  • 142 | 143 |
  • 144 | 145 |
    146 | logs 147 |
    148 |
    .icon-logs 149 |
    150 |
  • 151 | 152 |
  • 153 | 154 |
    155 | arrange 156 |
    157 |
    .icon-arrange 158 |
    159 |
  • 160 | 161 |
  • 162 | 163 |
    164 | subscribe 165 |
    166 |
    .icon-subscribe 167 |
    168 |
  • 169 | 170 |
  • 171 | 172 |
    173 | settings 174 |
    175 |
    .icon-settings 176 |
    177 |
  • 178 | 179 |
180 |
181 |

font-class 引用

182 |
183 | 184 |

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

185 |

与 Unicode 使用方式相比,具有如下特点:

186 |
    187 |
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 188 |
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • 189 |
190 |

使用步骤如下:

191 |

第一步:引入项目下面生成的 fontclass 代码:

192 |
<link rel="stylesheet" href="./iconfont.css">
193 | 
194 |

第二步:挑选相应图标并获取类名,应用于页面:

195 |
<span class="iconfont icon-xxx"></span>
196 | 
197 |
198 |

" 199 | iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

200 |
201 |
202 |
203 |
204 |
    205 | 206 |
  • 207 | 210 |
    podcast
    211 |
    #icon-podcast
    212 |
  • 213 | 214 |
  • 215 | 218 |
    logs
    219 |
    #icon-logs
    220 |
  • 221 | 222 |
  • 223 | 226 |
    arrange
    227 |
    #icon-arrange
    228 |
  • 229 | 230 |
  • 231 | 234 |
    subscribe
    235 |
    #icon-subscribe
    236 |
  • 237 | 238 |
  • 239 | 242 |
    settings
    243 |
    #icon-settings
    244 |
  • 245 | 246 |
247 |
248 |

Symbol 引用

249 |
250 | 251 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 252 | 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

253 |
    254 |
  • 支持多色图标了,不再受单色限制。
  • 255 |
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 256 |
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • 257 |
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • 258 |
259 |

使用步骤如下:

260 |

第一步:引入项目下面生成的 symbol 代码:

261 |
<script src="./iconfont.js"></script>
262 | 
263 |

第二步:加入通用 CSS 代码(引入一次就行):

264 |
<style>
265 | .icon {
266 |   width: 1em;
267 |   height: 1em;
268 |   vertical-align: -0.15em;
269 |   fill: currentColor;
270 |   overflow: hidden;
271 | }
272 | </style>
273 | 
274 |

第三步:挑选相应图标并获取类名,应用于页面:

275 |
<svg class="icon" aria-hidden="true">
276 |   <use xlink:href="#icon-xxx"></use>
277 | </svg>
278 | 
279 |
280 |
281 | 282 |
283 |
284 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 4481671 */ 3 | src: url('iconfont.woff2?t=1711384457279') format('woff2'), 4 | url('iconfont.woff?t=1711384457279') format('woff'), 5 | url('iconfont.ttf?t=1711384457279') format('truetype'), 6 | url('iconfont.svg?t=1711384457279#iconfont') format('svg'); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-podcast:before { 18 | content: "\e61d"; 19 | } 20 | 21 | .icon-logs:before { 22 | content: "\e619"; 23 | } 24 | 25 | .icon-arrange:before { 26 | content: "\e61a"; 27 | } 28 | 29 | .icon-subscribe:before { 30 | content: "\e61b"; 31 | } 32 | 33 | .icon-settings:before { 34 | content: "\e61c"; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.js: -------------------------------------------------------------------------------- 1 | window._iconfont_svg_string_4481671='',function(e){var a=(a=document.getElementsByTagName("script"))[a.length-1],t=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var c,l,n,o,i,s=function(a,t){t.parentNode.insertBefore(a,t)};if(t&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}c=function(){var a,t=document.createElement("div");t.innerHTML=e._iconfont_svg_string_4481671,(t=t.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",t=t,(a=document.body).firstChild?s(t,a.firstChild):a.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(l=function(){document.removeEventListener("DOMContentLoaded",l,!1),c()},document.addEventListener("DOMContentLoaded",l,!1)):document.attachEvent&&(n=c,o=e.document,i=!1,h(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,d())})}function d(){i||(i=!0,n())}function h(){try{o.documentElement.doScroll("left")}catch(a){return void setTimeout(h,50)}d()}}(window); -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4481671", 3 | "name": "115FileLister", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "39690943", 10 | "name": "podcast", 11 | "font_class": "podcast", 12 | "unicode": "e61d", 13 | "unicode_decimal": 58909 14 | }, 15 | { 16 | "icon_id": "39690905", 17 | "name": "logs", 18 | "font_class": "logs", 19 | "unicode": "e619", 20 | "unicode_decimal": 58905 21 | }, 22 | { 23 | "icon_id": "39690906", 24 | "name": "arrange", 25 | "font_class": "arrange", 26 | "unicode": "e61a", 27 | "unicode_decimal": 58906 28 | }, 29 | { 30 | "icon_id": "39690907", 31 | "name": "subscribe", 32 | "font_class": "subscribe", 33 | "unicode": "e61b", 34 | "unicode_decimal": 58907 35 | }, 36 | { 37 | "icon_id": "39690909", 38 | "name": "settings", 39 | "font_class": "settings", 40 | "unicode": "e61c", 41 | "unicode_decimal": 58908 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created by iconfont 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/src/assets/fonts/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/src/assets/fonts/iconfont/iconfont.woff -------------------------------------------------------------------------------- /frontend/src/assets/fonts/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alano-i/115FileLister/7c9b65071486d419fd5c7a01f630a1b4dc5b714d/frontend/src/assets/fonts/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/icon/attr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/desc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/m3u8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 9 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/spin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/enums/httpEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: Request result set 3 | */ 4 | export enum ResultEnum { 5 | SUCCESS = 0, 6 | ERROR = -1, 7 | TIMEOUT = 401, 8 | TYPE = "success", 9 | } 10 | 11 | /** 12 | * @description: request method 13 | */ 14 | export enum RequestEnum { 15 | GET = "GET", 16 | POST = "POST", 17 | PUT = "PUT", 18 | DELETE = "DELETE", 19 | } 20 | 21 | /** 22 | * @description: contentType 23 | */ 24 | export enum ContentTypeEnum { 25 | // json 26 | JSON = "application/json;charset=UTF-8", 27 | // form-data qs 28 | FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8", 29 | // form-data upload 30 | FORM_DATA = "multipart/form-data;charset=UTF-8", 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | // 定义一个hook来检测是否为移动设备 2 | import { useEffect, useState } from "react"; 3 | 4 | // 定义移动设备的正则表达式,用来检测用户代理字符串 5 | const mobileDeviceRegex = 6 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; 7 | 8 | // 使用React Hook来创建这个功能 9 | export const useIsMobile = (): boolean => { 10 | const [isMobile, setIsMobile] = useState(false); 11 | 12 | useEffect(() => { 13 | // 检查用户代理字符串 14 | const userAgent = 15 | typeof window.navigator === "undefined" ? "" : navigator.userAgent; 16 | // 设置状态为true如果是移动设备 17 | setIsMobile(mobileDeviceRegex.test(userAgent)); 18 | }, []); 19 | 20 | return isMobile; 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useMessage.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { MessageContext } from "/@/routes"; 3 | 4 | export const useMessage = () => { 5 | const messageApi = useContext(MessageContext); 6 | const config = { 7 | style: { 8 | marginTop: "90px", 9 | } 10 | }; 11 | const success = (content: string) => { 12 | return messageApi?.success({ 13 | ...config, 14 | content, 15 | }); 16 | }; 17 | 18 | const warning = (content: string) => { 19 | return messageApi?.warning({ 20 | ...config, 21 | content, 22 | }); 23 | }; 24 | 25 | const error = (content: string) => { 26 | return messageApi?.error({ 27 | ...config, 28 | content, 29 | }); 30 | }; 31 | 32 | return { 33 | success, 34 | warning, 35 | error, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/src/hooks/useRandomId.ts: -------------------------------------------------------------------------------- 1 | const useRandomId = () => { 2 | let d = new Date().getTime(); //获取当前时间 3 | if ( 4 | typeof performance !== "undefined" && 5 | typeof performance.now === "function" 6 | ) { 7 | d += performance.now(); //使用高精度时间 8 | } 9 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 10 | const r = (d + Math.random() * 16) % 16 | 0; 11 | d = Math.floor(d / 16); 12 | return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); 13 | }); 14 | }; 15 | export default useRandomId; 16 | -------------------------------------------------------------------------------- /frontend/src/http/Axios.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AxiosRequestConfig, 3 | AxiosInstance, 4 | AxiosResponse, 5 | AxiosError, 6 | InternalAxiosRequestConfig, 7 | } from "axios"; 8 | import type { RequestOptions, Result, UploadFileParams } from "#/axios"; 9 | import type { CreateAxiosOptions } from "./axiosTransform"; 10 | import axios from "axios"; 11 | import qs from "qs"; 12 | import { AxiosCanceler } from "./axiosCancel"; 13 | import { isFunction } from "/@/utils/is"; 14 | import { cloneDeep } from "lodash-es"; 15 | import { ContentTypeEnum, RequestEnum } from "/@/enums/httpEnum"; 16 | 17 | export * from "./axiosTransform"; 18 | 19 | /** 20 | * @description: axios module 21 | */ 22 | export class VAxios { 23 | private axiosInstance: AxiosInstance; 24 | private readonly options: CreateAxiosOptions; 25 | 26 | constructor(options: CreateAxiosOptions) { 27 | this.options = options; 28 | this.axiosInstance = axios.create(options); 29 | this.setupInterceptors(); 30 | } 31 | 32 | /** 33 | * @description: Create axios instance 34 | */ 35 | private createAxios(config: CreateAxiosOptions): void { 36 | this.axiosInstance = axios.create(config); 37 | } 38 | 39 | private getTransform() { 40 | const { transform } = this.options; 41 | return transform; 42 | } 43 | 44 | getAxios(): AxiosInstance { 45 | return this.axiosInstance; 46 | } 47 | 48 | /** 49 | * @description: Reconfigure axios 50 | */ 51 | configAxios(config: CreateAxiosOptions) { 52 | if (!this.axiosInstance) { 53 | return; 54 | } 55 | this.createAxios(config); 56 | } 57 | 58 | /** 59 | * @description: Set general header 60 | */ 61 | setHeader(headers: any): void { 62 | if (!this.axiosInstance) { 63 | return; 64 | } 65 | Object.assign(this.axiosInstance.defaults.headers, headers); 66 | } 67 | 68 | /** 69 | * @description: Interceptor configuration 拦截器配置 70 | */ 71 | private setupInterceptors() { 72 | // const transform = this.getTransform(); 73 | const { 74 | axiosInstance, 75 | options: { transform }, 76 | } = this; 77 | if (!transform) { 78 | return; 79 | } 80 | const { 81 | requestInterceptors, 82 | requestInterceptorsCatch, 83 | responseInterceptors, 84 | responseInterceptorsCatch, 85 | } = transform; 86 | 87 | const axiosCanceler = new AxiosCanceler(); 88 | 89 | // Request interceptor configuration processing 90 | this.axiosInstance.interceptors.request.use( 91 | (config: InternalAxiosRequestConfig) => { 92 | // If cancel repeat request is turned on, then cancel repeat request is prohibited 93 | const requestOptions = 94 | (config as unknown as any).requestOptions ?? 95 | this.options.requestOptions; 96 | const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true; 97 | 98 | !ignoreCancelToken && axiosCanceler.addPending(config); 99 | 100 | if (requestInterceptors && isFunction(requestInterceptors)) { 101 | config = requestInterceptors(config, this.options); 102 | } 103 | return config; 104 | }, 105 | undefined 106 | ); 107 | 108 | // Request interceptor error capture 109 | requestInterceptorsCatch && 110 | isFunction(requestInterceptorsCatch) && 111 | this.axiosInstance.interceptors.request.use( 112 | undefined, 113 | requestInterceptorsCatch 114 | ); 115 | 116 | // Response result interceptor processing 117 | this.axiosInstance.interceptors.response.use((res: AxiosResponse) => { 118 | res && axiosCanceler.removePending(res.config); 119 | if (responseInterceptors && isFunction(responseInterceptors)) { 120 | res = responseInterceptors(res); 121 | } 122 | return res; 123 | }, undefined); 124 | 125 | // Response result interceptor error capture 126 | responseInterceptorsCatch && 127 | isFunction(responseInterceptorsCatch) && 128 | this.axiosInstance.interceptors.response.use(undefined, (error) => { 129 | return responseInterceptorsCatch(axiosInstance, error); 130 | }); 131 | } 132 | 133 | /** 134 | * @description: File Upload 135 | */ 136 | uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { 137 | const formData = new window.FormData(); 138 | const customFilename = params.name || "file"; 139 | 140 | if (params.filename) { 141 | formData.append(customFilename, params.file, params.filename); 142 | } else { 143 | formData.append(customFilename, params.file); 144 | } 145 | 146 | if (params.data) { 147 | Object.keys(params.data).forEach((key) => { 148 | const value = params.data![key]; 149 | if (Array.isArray(value)) { 150 | value.forEach((item) => { 151 | formData.append(`${key}[]`, item); 152 | }); 153 | return; 154 | } 155 | 156 | formData.append(key, params.data![key]); 157 | }); 158 | } 159 | 160 | return this.axiosInstance.request({ 161 | ...config, 162 | method: "POST", 163 | data: formData, 164 | headers: { 165 | "Content-type": ContentTypeEnum.FORM_DATA, 166 | // @ts-ignore 167 | ignoreCancelToken: true, 168 | }, 169 | }); 170 | } 171 | 172 | // support form-data 173 | supportFormData(config: AxiosRequestConfig) { 174 | const headers = config.headers || this.options.headers; 175 | const contentType = headers?.["Content-Type"] || headers?.["content-type"]; 176 | 177 | if ( 178 | contentType !== ContentTypeEnum.FORM_URLENCODED || 179 | !Reflect.has(config, "data") || 180 | config.method?.toUpperCase() === RequestEnum.GET 181 | ) { 182 | return config; 183 | } 184 | 185 | return { 186 | ...config, 187 | data: qs.stringify(config.data, { arrayFormat: "brackets" }), 188 | }; 189 | } 190 | 191 | get( 192 | config: AxiosRequestConfig, 193 | options?: RequestOptions 194 | ): Promise { 195 | return this.request({ ...config, method: "GET" }, options); 196 | } 197 | 198 | post( 199 | config: AxiosRequestConfig, 200 | options?: RequestOptions 201 | ): Promise { 202 | return this.request({ ...config, method: "POST" }, options); 203 | } 204 | 205 | put( 206 | config: AxiosRequestConfig, 207 | options?: RequestOptions 208 | ): Promise { 209 | return this.request({ ...config, method: "PUT" }, options); 210 | } 211 | 212 | delete( 213 | config: AxiosRequestConfig, 214 | options?: RequestOptions 215 | ): Promise { 216 | return this.request({ ...config, method: "DELETE" }, options); 217 | } 218 | 219 | request( 220 | config: AxiosRequestConfig, 221 | options?: RequestOptions 222 | ): Promise { 223 | let conf: CreateAxiosOptions = cloneDeep(config); 224 | // cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求 225 | if (config.cancelToken) { 226 | conf.cancelToken = config.cancelToken; 227 | } 228 | 229 | if (config.signal) { 230 | conf.signal = config.signal; 231 | } 232 | 233 | const transform = this.getTransform(); 234 | 235 | const { requestOptions } = this.options; 236 | 237 | const opt: RequestOptions = Object.assign({}, requestOptions, options); 238 | 239 | const { beforeRequestHook, requestCatchHook, transformResponseHook } = 240 | transform || {}; 241 | if (beforeRequestHook && isFunction(beforeRequestHook)) { 242 | conf = beforeRequestHook(conf, opt); 243 | } 244 | conf.requestOptions = opt; 245 | 246 | conf = this.supportFormData(conf); 247 | 248 | return new Promise((resolve, reject) => { 249 | this.axiosInstance 250 | .request>(conf) 251 | .then((res: AxiosResponse) => { 252 | if (transformResponseHook && isFunction(transformResponseHook)) { 253 | try { 254 | const ret = transformResponseHook(res, opt); 255 | resolve(ret); 256 | } catch (err) { 257 | reject(err || new Error("request error!")); 258 | } 259 | return; 260 | } 261 | resolve(res as unknown as Promise); 262 | }) 263 | .catch((e: Error | AxiosError) => { 264 | if (requestCatchHook && isFunction(requestCatchHook)) { 265 | reject(requestCatchHook(e, opt)); 266 | return; 267 | } 268 | if (axios.isAxiosError(e)) { 269 | // rewrite error message from axios in here 270 | } 271 | reject(e); 272 | }); 273 | }); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /frontend/src/http/axiosCancel.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from "axios"; 2 | 3 | // 用于存储每个请求的标识和取消函数 4 | const pendingMap = new Map(); 5 | 6 | const getPendingUrl = (config: AxiosRequestConfig): string => { 7 | return [config.method, config.url].join("&"); 8 | }; 9 | 10 | export class AxiosCanceler { 11 | /** 12 | * 添加请求 13 | * @param config 请求配置 14 | */ 15 | public addPending(config: AxiosRequestConfig): void { 16 | this.removePending(config); 17 | const url = getPendingUrl(config); 18 | const controller = new AbortController(); 19 | config.signal = config.signal || controller.signal; 20 | if (!pendingMap.has(url)) { 21 | // 如果当前请求不在等待中,将其添加到等待中 22 | pendingMap.set(url, controller); 23 | } 24 | } 25 | 26 | /** 27 | * 清除所有等待中的请求 28 | */ 29 | public removeAllPending(): void { 30 | pendingMap.forEach((abortController) => { 31 | if (abortController) { 32 | abortController.abort(); 33 | } 34 | }); 35 | this.reset(); 36 | } 37 | 38 | /** 39 | * 移除请求 40 | * @param config 请求配置 41 | */ 42 | public removePending(config: AxiosRequestConfig): void { 43 | const url = getPendingUrl(config); 44 | if (pendingMap.has(url)) { 45 | // 如果当前请求在等待中,取消它并将其从等待中移除 46 | const abortController = pendingMap.get(url); 47 | if (abortController) { 48 | abortController.abort(url); 49 | } 50 | pendingMap.delete(url); 51 | } 52 | } 53 | 54 | /** 55 | * 重置 56 | */ 57 | public reset(): void { 58 | pendingMap.clear(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/http/axiosRetry.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError, AxiosInstance } from "axios"; 2 | /** 3 | * 请求重试机制 4 | */ 5 | 6 | export class AxiosRetry { 7 | /** 8 | * 重试 9 | */ 10 | retry(axiosInstance: AxiosInstance, error: AxiosError) { 11 | // @ts-ignore 12 | const { config } = error.response; 13 | const { waitTime, count } = config?.requestOptions?.retryRequest ?? {}; 14 | config.__retryCount = config.__retryCount || 0; 15 | if (config.__retryCount >= count) { 16 | return Promise.reject(error); 17 | } 18 | config.__retryCount += 1; 19 | //请求返回后config的header不正确造成重试请求失败,删除返回headers采用默认headers 20 | delete config.headers; 21 | return this.delay(waitTime).then(() => axiosInstance(config)); 22 | } 23 | 24 | /** 25 | * 延迟 26 | */ 27 | private delay(waitTime: number) { 28 | return new Promise((resolve) => setTimeout(resolve, waitTime)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/http/axiosTransform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Data processing class, can be configured according to the project 3 | */ 4 | import type { 5 | AxiosInstance, 6 | AxiosRequestConfig, 7 | AxiosResponse, 8 | InternalAxiosRequestConfig, 9 | } from "axios"; 10 | import type { RequestOptions, Result } from "#/axios"; 11 | 12 | export interface CreateAxiosOptions extends AxiosRequestConfig { 13 | authenticationScheme?: string; 14 | transform?: AxiosTransform; 15 | requestOptions?: RequestOptions; 16 | } 17 | 18 | export abstract class AxiosTransform { 19 | /** 20 | * A function that is called before a request is sent. It can modify the request configuration as needed. 21 | * 在发送请求之前调用的函数。它可以根据需要修改请求配置。 22 | */ 23 | beforeRequestHook?: ( 24 | config: AxiosRequestConfig, 25 | options: RequestOptions 26 | ) => AxiosRequestConfig; 27 | 28 | /** 29 | * @description: 处理响应数据 30 | */ 31 | transformResponseHook?: ( 32 | res: AxiosResponse, 33 | options: RequestOptions 34 | ) => any; 35 | 36 | /** 37 | * @description: 请求失败处理 38 | */ 39 | requestCatchHook?: (e: Error, options: RequestOptions) => Promise; 40 | 41 | /** 42 | * @description: 请求之前的拦截器 43 | */ 44 | requestInterceptors?: ( 45 | config: InternalAxiosRequestConfig, 46 | options: CreateAxiosOptions 47 | ) => InternalAxiosRequestConfig; 48 | 49 | /** 50 | * @description: 请求之后的拦截器 51 | */ 52 | responseInterceptors?: (res: AxiosResponse) => AxiosResponse; 53 | 54 | /** 55 | * @description: 请求之前的拦截器错误处理 56 | */ 57 | requestInterceptorsCatch?: (error: Error) => void; 58 | 59 | /** 60 | * @description: 请求之后的拦截器错误处理 61 | */ 62 | responseInterceptorsCatch?: ( 63 | axiosInstance: AxiosInstance, 64 | error: Error 65 | ) => void; 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/http/checkStatus.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessageMode } from "#/axios"; 2 | import { message as AntMessage } from "antd"; 3 | 4 | export function checkStatus( 5 | status: number, 6 | msg: string, 7 | errorMessageMode: ErrorMessageMode = "message" 8 | ): void { 9 | let errMessage = ""; 10 | 11 | switch (status) { 12 | case 400: 13 | errMessage = `${msg}`; 14 | break; 15 | // 401: Not logged in 16 | // Jump to the login page if not logged in, and carry the path of the current page 17 | // Return to the current page after successful login. This step needs to be operated on the login page. 18 | case 401: 19 | errMessage = msg || "sys.api.errMsg401"; 20 | break; 21 | case 403: 22 | errMessage = "sys.api.errMsg403"; 23 | break; 24 | // 404请求不存在 25 | case 404: 26 | errMessage = "sys.api.errMsg404"; 27 | break; 28 | case 405: 29 | errMessage = "sys.api.errMsg405"; 30 | break; 31 | case 408: 32 | errMessage = "sys.api.errMsg408"; 33 | break; 34 | case 500: 35 | errMessage = "sys.api.errMsg500"; 36 | break; 37 | case 501: 38 | errMessage = "sys.api.errMsg501"; 39 | break; 40 | case 502: 41 | errMessage = "sys.api.errMsg502"; 42 | break; 43 | case 503: 44 | errMessage = "sys.api.errMsg503"; 45 | break; 46 | case 504: 47 | errMessage = "sys.api.errMsg504"; 48 | break; 49 | case 505: 50 | errMessage = "sys.api.errMsg505"; 51 | break; 52 | default: 53 | } 54 | 55 | if (errMessage && errorMessageMode === "message") { 56 | AntMessage.error(errMessage); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/http/helper.ts: -------------------------------------------------------------------------------- 1 | import { isObject, isString } from "/@/utils/is"; 2 | 3 | const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm:ss"; 4 | 5 | export function joinTimestamp( 6 | join: boolean, 7 | restful: T 8 | ): T extends true ? string : object; 9 | 10 | export function joinTimestamp(join: boolean, restful = false): string | object { 11 | if (!join) { 12 | return restful ? "" : {}; 13 | } 14 | const now = new Date().getTime(); 15 | if (restful) { 16 | return `?_t=${now}`; 17 | } 18 | return { _t: now }; 19 | } 20 | 21 | /** 22 | * @description: Format request parameter time 23 | */ 24 | export function formatRequestDate(params: Recordable) { 25 | if (Object.prototype.toString.call(params) !== "[object Object]") { 26 | return; 27 | } 28 | 29 | for (const key in params) { 30 | const format = params[key]?.format ?? null; 31 | if (format && typeof format === "function") { 32 | params[key] = params[key].format(DATE_TIME_FORMAT); 33 | } 34 | if (isString(key)) { 35 | const value = params[key]; 36 | if (value) { 37 | try { 38 | params[key] = isString(value) ? value.trim() : value; 39 | } catch (error: any) { 40 | throw new Error(error); 41 | } 42 | } 43 | } 44 | if (isObject(params[key])) { 45 | formatRequestDate(params[key]); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/http/index.ts: -------------------------------------------------------------------------------- 1 | // axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动 2 | // The axios configuration can be changed according to the project, just change the file, other files can be left unchanged 3 | 4 | import type { AxiosInstance, AxiosResponse } from "axios"; 5 | import { clone } from "lodash-es"; 6 | import type { RequestOptions, Result } from "#/axios"; 7 | import type { AxiosTransform, CreateAxiosOptions } from "./axiosTransform"; 8 | import { VAxios } from "./Axios"; 9 | import { checkStatus } from "./checkStatus"; 10 | import { RequestEnum, ContentTypeEnum } from "/@/enums/httpEnum"; 11 | import { isString } from "/@/utils/is"; 12 | import { setObjToUrlParams, deepMerge } from "/@/utils"; 13 | import { joinTimestamp, formatRequestDate } from "./helper"; 14 | import { AxiosRetry } from "/@/http/axiosRetry"; 15 | import axios from "axios"; 16 | 17 | /** 18 | * @description: 数据处理,方便区分多种处理方式 19 | */ 20 | const transform: AxiosTransform = { 21 | /** 22 | * @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误 23 | */ 24 | transformResponseHook: ( 25 | res: AxiosResponse, 26 | options: RequestOptions 27 | ) => { 28 | const { isTransformResponse, isReturnNativeResponse } = options; 29 | // 是否返回原生响应头 比如:需要获取响应头时使用该属性 30 | if (isReturnNativeResponse) { 31 | return res; 32 | } 33 | // 不进行任何处理,直接返回 34 | // 用于页面代码可能需要直接获取code,data,message这些信息时开启 35 | if (!isTransformResponse) { 36 | return res.data; 37 | } 38 | // 错误的时候返回 39 | 40 | const { data } = res; 41 | 42 | if (!data) { 43 | // return '[HTTP] Request has no return value'; 44 | throw new Error("sys.api.apiRequestFailed"); 45 | } 46 | 47 | return data; 48 | }, 49 | 50 | // 请求之前处理config 51 | beforeRequestHook: (config, options) => { 52 | const { 53 | apiUrl, 54 | joinPrefix, 55 | joinParamsToUrl, 56 | formatDate, 57 | joinTime = true, 58 | urlPrefix, 59 | } = options; 60 | 61 | if (joinPrefix) { 62 | config.url = `${urlPrefix}${config.url}`; 63 | } 64 | 65 | if (apiUrl && isString(apiUrl)) { 66 | config.url = `${apiUrl}${config.url}`; 67 | } 68 | 69 | const params = config.params || {}; 70 | const data = config.data || false; 71 | formatDate && data && !isString(data) && formatRequestDate(data); 72 | if (config.method?.toUpperCase() === RequestEnum.GET) { 73 | if (!isString(params)) { 74 | // 给 get 请求加上时间戳参数,避免从缓存中拿数据。 75 | config.params = Object.assign( 76 | params || {}, 77 | joinTimestamp(joinTime, false) 78 | ); 79 | } else { 80 | // 兼容restful风格 81 | config.url = config.url + params + `${joinTimestamp(joinTime, true)}`; 82 | config.params = undefined; 83 | } 84 | } else { 85 | if (!isString(params)) { 86 | formatDate && formatRequestDate(params); 87 | if ( 88 | Reflect.has(config, "data") && 89 | config.data && 90 | (Object.keys(config.data).length > 0 || 91 | config.data instanceof FormData) 92 | ) { 93 | config.data = data; 94 | config.params = params; 95 | } else { 96 | // 非GET请求如果没有提供data,则将params视为data 97 | config.data = params; 98 | config.params = undefined; 99 | } 100 | if (joinParamsToUrl) { 101 | config.url = setObjToUrlParams( 102 | config.url as string, 103 | Object.assign({}, config.params, config.data) 104 | ); 105 | } 106 | } else { 107 | // 兼容restful风格 108 | config.url = config.url + params; 109 | config.params = undefined; 110 | } 111 | } 112 | return config; 113 | }, 114 | 115 | /** 116 | * @description: 请求拦截器处理 117 | */ 118 | requestInterceptors: (config) => { 119 | return config; 120 | }, 121 | 122 | /** 123 | * @description: 响应拦截器处理 124 | */ 125 | responseInterceptors: (res: AxiosResponse) => { 126 | return res; 127 | }, 128 | 129 | /** 130 | * @description: 响应错误处理 131 | */ 132 | responseInterceptorsCatch: (axiosInstance: AxiosInstance, error: any) => { 133 | const { response, config } = error || {}; 134 | const msg: string = response?.data?.message ?? ""; 135 | const errorMessageMode = config?.requestOptions?.errorMessageMode || "none"; 136 | 137 | if (axios.isCancel(error)) { 138 | return Promise.reject(error); 139 | } 140 | 141 | checkStatus(error?.response?.status, msg, errorMessageMode); 142 | 143 | // 添加自动重试机制 保险起见 只针对GET请求 144 | const retryRequest = new AxiosRetry(); 145 | const { isOpenRetry } = config.requestOptions.retryRequest; 146 | config.method?.toUpperCase() === RequestEnum.GET && 147 | isOpenRetry && 148 | // @ts-ignore 149 | retryRequest.retry(axiosInstance, error); 150 | return Promise.reject(error); 151 | }, 152 | }; 153 | 154 | function createAxios(opt?: Partial) { 155 | return new VAxios( 156 | // 深度合并 157 | deepMerge( 158 | { 159 | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes 160 | // authentication schemes,e.g: Bearer 161 | // authenticationScheme: 'Bearer', 162 | authenticationScheme: "", 163 | timeout: 10 * 1000, 164 | // 基础接口地址 165 | baseURL: "/api", 166 | headers: { "Content-Type": ContentTypeEnum.JSON }, 167 | // 如果是form-data格式 168 | // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED }, 169 | // 数据处理方式 170 | transform: clone(transform), 171 | // 配置项,下面的选项都可以在独立的接口请求中覆盖 172 | requestOptions: { 173 | // 默认将prefix 添加到url 174 | joinPrefix: false, 175 | // 是否返回原生响应头 比如:需要获取响应头时使用该属性 176 | isReturnNativeResponse: false, 177 | // 需要对返回数据进行处理 178 | isTransformResponse: true, 179 | // post请求的时候添加参数到url 180 | joinParamsToUrl: false, 181 | // 消息提示类型 182 | errorMessageMode: "message", 183 | // 格式化提交参数时间 184 | formatDate: true, 185 | // 是否加入时间戳 186 | joinTime: true, 187 | // 忽略重复请求 188 | ignoreCancelToken: true, 189 | // 是否携带token 190 | withToken: true, 191 | retryRequest: { 192 | isOpenRetry: false, 193 | count: 5, 194 | waitTime: 100, 195 | }, 196 | }, 197 | }, 198 | opt || {} 199 | ) 200 | ); 201 | } 202 | export const defHttp = createAxios(); 203 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | html { 11 | width: 100%; 12 | height: calc(100% + env(safe-area-inset-top)); 13 | color: #fff; 14 | } 15 | 16 | body, 17 | #root { 18 | height: 100%; 19 | } 20 | 21 | /* 设置滚动条的样式 */ 22 | ::-webkit-scrollbar { 23 | width: 8px; 24 | background: #131313; 25 | } 26 | 27 | /* 滚动槽 */ 28 | ::-webkit-scrollbar-track { 29 | /*-webkit-box-shadow: rgba(255, 0, 0, 1);*/ 30 | border-radius: 10px; 31 | background: rgba(255, 255, 255, 0); 32 | } 33 | 34 | /* 滚动条滑块-窗口激活 */ 35 | ::-webkit-scrollbar-thumb { 36 | border-radius: 6px; 37 | background: rgba(255, 255, 255, 0.15); 38 | /*-webkit-box-shadow: rgba(255, 0, 0, 0);*/ 39 | } 40 | 41 | /* 滚动条滑块-窗口失去焦点 */ 42 | ::-webkit-scrollbar-thumb:window-inactive { 43 | background: rgba(255, 255, 255, 0.1); 44 | } 45 | 46 | .icon { 47 | width: 1em; 48 | height: 1em; 49 | vertical-align: -0.15em; 50 | fill: currentColor; 51 | overflow: hidden; 52 | } 53 | 54 | :root { 55 | box-sizing: border-box; 56 | /* padding: env(safe-area-inset-top) env(safe-area-inset-right) 57 | env(safe-area-inset-bottom) env(safe-area-inset-left); */ 58 | --safe-area-inset-top: env(safe-area-inset-top); 59 | padding-left: env(safe-area-inset-left); 60 | padding-right: env(safe-area-inset-right); 61 | padding-bottom: env(safe-area-inset-bottom); 62 | } 63 | 64 | :root::after { 65 | content: ""; 66 | position: fixed; 67 | top: 0; 68 | left: 0; 69 | width: 100%; 70 | height: env(safe-area-inset-top); 71 | background: #131313; 72 | 73 | @media (max-width: 768px) { 74 | background: #212121; 75 | } 76 | } 77 | 78 | :root::before { 79 | content: ""; 80 | position: fixed; 81 | top: 0; 82 | left: 0; 83 | width: 100%; 84 | height: 600px; 85 | background: #131313; 86 | z-index: -1; 87 | 88 | @media (max-width: 768px) { 89 | background: #212121; 90 | } 91 | } 92 | 93 | .ant-dropdown-menu { 94 | /* background: rgba(255, 255, 255, 7%) !important; */ 95 | background: #242424 !important; 96 | /* backdrop-filter: blur(18px) !important; */ 97 | /* -webkit-backdrop-filter: blur(18px) !important; */ 98 | border-radius: 8px !important; 99 | padding: 6px !important; 100 | box-shadow: 0 5px 16px rgba(0, 0, 0, 0.5) !important; 101 | } 102 | 103 | .ant-dropdown-menu-item { 104 | min-width: 120px; 105 | border-radius: 5px !important; 106 | margin: 2px !important; 107 | padding: 8px !important; 108 | color: #fff !important; 109 | } 110 | 111 | .ant-dropdown-menu-item:hover { 112 | background: rgba(255, 255, 255, 1) !important; 113 | color: #000000 !important; 114 | } 115 | 116 | .ant-modal-content { 117 | border-radius: 10px !important; 118 | border: 0.5px solid rgba(255, 255, 255, 0.07) !important; 119 | background: rgba(242, 243, 255, 0.07) !important; 120 | box-shadow: 0px 8px 50px 0px rgba(0, 0, 0, 0.6) !important; 121 | 122 | backdrop-filter: blur(18px) !important; 123 | -webkit-backdrop-filter: blur(18px) !important; 124 | .ant-modal-title { 125 | color: #ffffff; 126 | background: #24242400; 127 | } 128 | .ant-modal-close-x { 129 | color: #ffffff5f; 130 | } 131 | .ant-modal-close-x :hover { 132 | color: #ffffff8f; 133 | transition: all 0.3s ease-in-out !important; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /frontend/src/layout/components/header.tsx: -------------------------------------------------------------------------------- 1 | const Header: React.FC = () => { 2 | return ( 3 |
4 | logo 5 |
9 | 115 FileLister 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default Header; 16 | -------------------------------------------------------------------------------- /frontend/src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from "antd"; 2 | import React from "react"; 3 | import { Outlet } from "react-router-dom"; 4 | import Header from "/@/layout/components/header"; 5 | import zhCN from "antd/lib/locale/zh_CN"; 6 | 7 | const BasicLayout: React.FC = () => { 8 | return ( 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default BasicLayout; 21 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import Routes from "/@/routes"; 5 | import "./assets/fonts/iconfont/iconfont"; 6 | import "./index.css"; 7 | 8 | const element = document.getElementById("root"); 9 | 10 | const safeAreaInsetTop = getComputedStyle( 11 | document.documentElement 12 | ).getPropertyValue("--safe-area-inset-top"); 13 | 14 | // 注入安全区域 15 | document.documentElement.style.paddingTop = 16 | safeAreaInsetTop === "0px" ? "16px" : safeAreaInsetTop; 17 | 18 | if (element) { 19 | const App = () => ( 20 | //严格模式会在开发环境令组件渲染两次(非开发环境不受影响) 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | const root = createRoot(element); 28 | root.render(); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Routes, Route, Outlet } from "react-router-dom"; 3 | import Layout from "/@/layout"; 4 | import { ConfigProvider, Result, Spin, message } from "antd"; 5 | import { createContext } from "react"; 6 | import { Router } from "./router"; 7 | import zhCN from "antd/lib/locale/zh_CN"; 8 | import { MessageInstance } from "antd/es/message/interface"; 9 | 10 | export const MessageContext = createContext(null); 11 | 12 | const Config = () => { 13 | const [messageApi, contextHolder] = message.useMessage(); 14 | 15 | return ( 16 | 17 | 18 | {contextHolder} 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | const BasicRoutes = () => { 26 | return ( 27 | }> 28 | 29 | }> 30 | }> 31 | {Router?.map(({ element, path }) => ( 32 | 33 | ))} 34 | 35 | 43 | } 44 | /> 45 | 49 | } 50 | /> 51 | 59 | } 60 | /> 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default BasicRoutes; 68 | -------------------------------------------------------------------------------- /frontend/src/routes/router.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Index from "/@/views/index"; 3 | import Player from "/@/views/player"; 4 | 5 | interface T { 6 | path: string; 7 | element: React.ReactNode; 8 | children?: Array; 9 | } 10 | 11 | export const Router: Array = [ 12 | { 13 | path: "/", 14 | element: , 15 | }, 16 | { 17 | path: "/player", 18 | element: , 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { intersectionWith, isEqual, mergeWith, unionWith } from "lodash-es"; 2 | import { isArray, isObject } from "/@/utils/is"; 3 | 4 | export const noop = () => {}; 5 | 6 | /** 7 | * @description: Set ui mount node 8 | */ 9 | export function getPopupContainer(node?: HTMLElement): HTMLElement { 10 | return (node?.parentNode as HTMLElement) ?? document.body; 11 | } 12 | 13 | /** 14 | * Add the object as a parameter to the URL 15 | * @param baseUrl url 16 | * @param obj 17 | * @returns {string} 18 | * eg: 19 | * let obj = {a: '3', b: '4'} 20 | * setObjToUrlParams('www.baidu.com', obj) 21 | * ==>www.baidu.com?a=3&b=4 22 | */ 23 | export function setObjToUrlParams(baseUrl: string, obj: any): string { 24 | let parameters = ""; 25 | for (const key in obj) { 26 | parameters += key + "=" + encodeURIComponent(obj[key]) + "&"; 27 | } 28 | parameters = parameters.replace(/&$/, ""); 29 | return /\?$/.test(baseUrl) 30 | ? baseUrl + parameters 31 | : baseUrl.replace(/\/?$/, "?") + parameters; 32 | } 33 | 34 | /** 35 | * Recursively merge two objects. 36 | * 递归合并两个对象。 37 | * 38 | * @param source The source object to merge from. 要合并的源对象。 39 | * @param target The target object to merge into. 目标对象,合并后结果存放于此。 40 | * @param mergeArrays How to merge arrays. Default is "replace". 41 | * 如何合并数组。默认为replace。 42 | * - "union": Union the arrays. 对数组执行并集操作。 43 | * - "intersection": Intersect the arrays. 对数组执行交集操作。 44 | * - "concat": Concatenate the arrays. 连接数组。 45 | * - "replace": Replace the source array with the target array. 用目标数组替换源数组。 46 | * @returns The merged object. 合并后的对象。 47 | */ 48 | export function deepMerge< 49 | T extends object | null | undefined, 50 | U extends object | null | undefined 51 | >( 52 | source: T, 53 | target: U, 54 | mergeArrays: "union" | "intersection" | "concat" | "replace" = "replace" 55 | ): T & U { 56 | if (!target) { 57 | return source as T & U; 58 | } 59 | if (!source) { 60 | return target as T & U; 61 | } 62 | return mergeWith({}, source, target, (sourceValue, targetValue) => { 63 | if (isArray(targetValue) && isArray(sourceValue)) { 64 | switch (mergeArrays) { 65 | case "union": 66 | return unionWith(sourceValue, targetValue, isEqual); 67 | case "intersection": 68 | return intersectionWith(sourceValue, targetValue, isEqual); 69 | case "concat": 70 | return sourceValue.concat(targetValue); 71 | case "replace": 72 | return targetValue; 73 | default: 74 | throw new Error( 75 | `Unknown merge array strategy: ${mergeArrays as string}` 76 | ); 77 | } 78 | } 79 | if (isObject(targetValue) && isObject(sourceValue)) { 80 | return deepMerge(sourceValue, targetValue, mergeArrays); 81 | } 82 | return undefined; 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /frontend/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isArguments, 3 | isArrayBuffer, 4 | isArrayLike, 5 | isArrayLikeObject, 6 | isBuffer, 7 | isBoolean, 8 | isDate, 9 | isElement, 10 | isEmpty, 11 | isEqual, 12 | isEqualWith, 13 | isError, 14 | isFunction, 15 | isFinite, 16 | isLength, 17 | isMap, 18 | isMatch, 19 | isMatchWith, 20 | isNative, 21 | isNil, 22 | isNumber, 23 | isNull, 24 | isObjectLike, 25 | isPlainObject, 26 | isRegExp, 27 | isSafeInteger, 28 | isSet, 29 | isString, 30 | isSymbol, 31 | isTypedArray, 32 | isUndefined, 33 | isWeakMap, 34 | isWeakSet, 35 | } from "lodash-es"; 36 | const toString = Object.prototype.toString; 37 | 38 | export function is(val: unknown, type: string) { 39 | return toString.call(val) === `[object ${type}]`; 40 | } 41 | 42 | export function isDef(val?: T): val is T { 43 | return typeof val !== "undefined"; 44 | } 45 | 46 | // TODO 此处 isObject 存在歧义 47 | export function isObject(val: any): val is Record { 48 | return val !== null && is(val, "Object"); 49 | } 50 | 51 | // TODO 此处 isArray 存在歧义 52 | export function isArray(val: any): val is Array { 53 | return val && Array.isArray(val); 54 | } 55 | 56 | export function isWindow(val: any): val is Window { 57 | return typeof window !== "undefined" && is(val, "Window"); 58 | } 59 | 60 | export const isServer = typeof window === "undefined"; 61 | 62 | export const isClient = !isServer; 63 | 64 | export function isHttpUrl(path: string): boolean { 65 | const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/; 66 | return reg.test(path); 67 | } 68 | 69 | export function isPascalCase(str: string): boolean { 70 | const regex = /^[A-Z][A-Za-z]*$/; 71 | return regex.test(str); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/views/index/components/FileItem.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import { FileInfo } from "/@/api"; 3 | import PlayIcon from "/@/assets/icon/play.svg?react"; 4 | import MoreIcon from "/@/assets/icon/more.svg?react"; 5 | import { Dropdown, MenuProps } from "antd"; 6 | import { 7 | InfoCircleOutlined, 8 | FileTextOutlined, 9 | DownloadOutlined, 10 | LinkOutlined, 11 | } from "@ant-design/icons"; 12 | import { useState } from "react"; 13 | import PlayerList from "./PlayerList"; 14 | 15 | // prettier-ignore 16 | // 定义文件类型与图标的对应关系 17 | const fileIconMap: { [key: string]: string[] } = { 18 | image: ["jpg", "jpeg", "png", "gif", "webp", "bmp", "tiff", "svg", "heic", "raw", "psd"], 19 | pdf: ["pdf"], 20 | word: ["doc", "docx"], 21 | excel: ["xls", "xlsx"], 22 | powerpoint: ["ppt", "pptx"], 23 | text: ["txt", "ass", "srt"], 24 | code: ["js", "json", "html", "css", "scss", "py", "less", "md", "xml"], 25 | audio: ["mp3", "flac", "wav", "aac", "ogg", "wma", "alac", "aiff", "dsd", "pcm", "m4a", "opus", "amr", "ape", "au", "ra", "mp2", "ac3", "dts"], 26 | video: [ 27 | "mkv", "mp4", "avi", "m2ts", "iso", "flv", "mov", "wmv", "webm", "mpeg", "mpg", "m4v", 28 | "3gp", "3g2", "ts", "vob", "ogv", "mxf", "rm", "rmvb", "asf", "f4v", "f4p", "f4a", "f4b", 29 | "divx", "xvid", "swf", "ogm", "dvr-ms", "mts", "m2v", "gxf", "pvr", "mpeg1", "h264", 30 | "h265", "yuv", "mjpeg", "bik", "bdmv", "mk3d", "m2p", "avs", "f4m" 31 | ], 32 | configuration: ["ini", "yaml", "yml"], 33 | db: ["db"], 34 | nfo: ["nfo"], 35 | ebook: ["epub", "mobi", "azw3", "kfx", "azw"], 36 | archive: ["zip", "rar", "tar", "7z"], 37 | }; 38 | 39 | // 定义图标路径 40 | const iconPathMap: { [key: string]: string } = { 41 | image: "/img/image.svg", 42 | pdf: "/img/pdf.svg", 43 | word: "/img/word.svg", 44 | excel: "/img/excel.svg", 45 | powerpoint: "/img/ppt.svg", 46 | text: "/img/text.svg", 47 | audio: "/img/audio.svg", 48 | video: "/img/video.svg", 49 | archive: "/img/archive.svg", 50 | code: "/img/code.svg", 51 | db: "/img/db.svg", 52 | nfo: "/img/nfo.svg", 53 | ebook: "/img/ebook.svg", 54 | configuration: "/img/config.svg", 55 | default: "/img/default.svg", // 默认图标路径 56 | }; 57 | 58 | /** 59 | * 根据文件后缀名返回文件类型 60 | * @param fileName - 文件名 61 | * @returns 文件类型 62 | */ 63 | function getFileType(fileName: string): string { 64 | const fileExtension = fileName.split(".").pop()?.toLowerCase() || ""; 65 | 66 | for (const [iconType, extensions] of Object.entries(fileIconMap)) { 67 | if (extensions.includes(fileExtension)) { 68 | return iconType; 69 | } 70 | } 71 | 72 | return "default"; 73 | } 74 | 75 | /** 76 | * 根据文件后缀名返回图标路径 77 | * @param fileType - 文件名 78 | * @returns 文件类型 79 | */ 80 | function getFileIcon(fileType: string): string { 81 | return iconPathMap[fileType]; 82 | } 83 | 84 | const FileItem = ({ 85 | file, 86 | onClick, 87 | }: { 88 | file: FileInfo; 89 | onClick: () => void; 90 | }) => { 91 | const fileType = getFileType(file.name); 92 | const fileIcon = getFileIcon(fileType); 93 | 94 | const items: MenuProps["items"] = [ 95 | file.is_directory 96 | ? null 97 | : { 98 | key: "download", 99 | label: "下载", 100 | icon: , 101 | onClick: () => { 102 | window.location.href = `/api/download?pickcode=${file.pickcode}`; 103 | }, 104 | }, 105 | { 106 | key: "desc", 107 | label: "备注", 108 | icon: , 109 | onClick: () => { 110 | window.open(`/api/desc?pickcode=${file.pickcode}`); 111 | }, 112 | }, 113 | { 114 | key: "attr", 115 | label: "属性", 116 | icon: , 117 | onClick: () => { 118 | window.open(`/api/attr?pickcode=${file.pickcode}`); 119 | }, 120 | }, 121 | fileType === "video" || fileType === "audio" 122 | ? { 123 | key: "video", 124 | label: "M3U8", 125 | icon: , 126 | onClick: () => { 127 | window.open(`/api/m3u8?pickcode=${file.pickcode}`); 128 | }, 129 | } 130 | : null, 131 | ].filter(Boolean); 132 | 133 | const [isModalOpen, setIsModalOpen] = useState(false); 134 | 135 | return ( 136 | 140 | 141 |
142 | {file.is_directory ? ( 143 | folder 148 | ) : ( 149 | file 150 | )} 151 | {/*
{file.name}
*/} 152 |
153 |
{file.name}
154 |
155 | {dayjs(file.mtime * 1000).format("YYYY-MM-DD HH:mm")} 156 |
157 |
158 |
159 | 160 | 161 |
{file.format_size || "-"}
162 | 163 | 164 |
165 | {dayjs(file.mtime * 1000).format("YYYY-MM-DD HH:mm")} 166 |
167 | 168 | 169 |
170 | {fileType === "video" && ( 171 |
172 |
{ 175 | e.stopPropagation(); 176 | setIsModalOpen(true); 177 | }} 178 | > 179 | 180 | 播放 181 |
182 | 187 |
188 | )} 189 |
e.stopPropagation()} 192 | > 193 | 194 |
195 | 196 |
197 |
198 |
199 |
200 | 201 | 202 | ); 203 | }; 204 | 205 | export default FileItem; -------------------------------------------------------------------------------- /frontend/src/views/index/components/PlayerList.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "antd"; 2 | import { FileInfo } from "/@/api"; 3 | 4 | const getPlayer = (file: FileInfo) => { 5 | const url = `${window.location.protocol}//${window.location.host}/api/download?pickcode=${file.pickcode}`; 6 | const suffix = file.name.split(".").pop(); 7 | return [ 8 | { 9 | name: "IINA", 10 | url: `iina://weblink?url=${url}`, 11 | icon: "/img/iina.svg", 12 | }, 13 | { 14 | name: "Pot", 15 | url: `potplayer://${url}`, 16 | icon: "/img/potplayer.svg", 17 | }, 18 | { 19 | name: "VLC", 20 | url: `vlc://${url}`, 21 | icon: "/img/vlc.svg", 22 | }, 23 | { 24 | name: "Fileball", 25 | url: `filebox://play?url=${url}`, 26 | icon: "/img/fileball.svg", 27 | }, 28 | { 29 | name: "MX", 30 | url: `intent:${url}#Intent;package=com.mxtech.videoplayer.pro;S.title=;end`, 31 | icon: "/img/mxplayer.svg", 32 | }, 33 | { 34 | name: "Infuse", 35 | url: `infuse://x-callback-url/play?url=${url}`, 36 | icon: "/img/infuse.svg", 37 | }, 38 | { 39 | name: "nPlayer", 40 | url: `nplayer-${url}`, 41 | icon: "/img/nplayer.svg", 42 | }, 43 | // { 44 | // name: "Omni", 45 | // url: `omniplayer://weblink?url=${url}`, 46 | // icon: "/img/omni.png", 47 | // }, 48 | { 49 | name: "Fig", 50 | url: `figplayer://weblink?url=${url}`, 51 | icon: "/img/fig.png", 52 | }, 53 | { 54 | name: "MPV", 55 | url: `mpv://${url}`, 56 | icon: "/img/mpv.png", 57 | }, 58 | { 59 | name: "在线播放", 60 | url: `/player?pickcode=${file.pickcode}&real_type=${suffix}`, 61 | icon: "/img/artplayer.png", 62 | }, 63 | ]; 64 | }; 65 | 66 | // 播放器跳转列表 67 | const PlayerList = ({ 68 | file, 69 | isModalOpen, 70 | setIsModalOpen, 71 | }: { 72 | file: FileInfo; 73 | isModalOpen: boolean; 74 | setIsModalOpen: (isModalOpen: boolean) => void; 75 | }) => { 76 | const handleOk = () => { 77 | setIsModalOpen(false); 78 | }; 79 | 80 | const handleCancel = () => { 81 | setIsModalOpen(false); 82 | }; 83 | 84 | return ( 85 | 93 | { 94 |
95 |
96 | 选择播放器 97 |
98 |
99 | {getPlayer(file).map((player) => ( 100 |
{ 104 | console.log(player.url); // 输出URL到控制台 105 | window.open(player.url, "_blank"); 106 | }} 107 | > 108 | player 113 | 114 | {player.name} 115 | 116 |
117 | ))} 118 |
119 |
120 | } 121 |
122 | ); 123 | }; 124 | 125 | export default PlayerList; 126 | -------------------------------------------------------------------------------- /frontend/src/views/index/index.tsx: -------------------------------------------------------------------------------- 1 | import { Ancestor, FileInfo, getAncestors, getList } from "/@/api"; 2 | import { useSearchParams } from "react-router-dom"; 3 | import FileItem from "./components/FileItem"; 4 | import useSWR from "swr"; 5 | import { Fragment } from "react"; 6 | import { message } from "antd"; 7 | import Spin from "/@/assets/icon/spin.svg?react"; 8 | 9 | const getData = async (path: string) => { 10 | const fileList = await getList({ path }); 11 | if (fileList.length > 0) { 12 | const ancestors = fileList[0].ancestors.slice(0, -1) || []; 13 | return { fileList, ancestors }; 14 | } 15 | const ancestors = await getAncestors({ path }); 16 | return { fileList, ancestors }; 17 | }; 18 | 19 | const Index = () => { 20 | let [searchParams, setSearchParams] = useSearchParams(); 21 | 22 | const path = searchParams.get("path") || "/"; 23 | 24 | const { 25 | data, 26 | error: fileListError, 27 | isLoading: isFileListLoading, 28 | } = useSWR<{ fileList: FileInfo[]; ancestors: Ancestor[] }>( 29 | ["/list", path], 30 | ([_url, path]: any) => getData(path) 31 | ); 32 | 33 | const { fileList, ancestors } = data || { 34 | fileList: [], 35 | ancestors: [], 36 | }; 37 | 38 | const navTo = (file: Pick) => { 39 | setSearchParams({ path: file.path }); 40 | }; 41 | 42 | const renderFileList = () => { 43 | if (isFileListLoading) { 44 | return ( 45 |
46 | 47 | 加载中... 48 |
49 | ); 50 | } 51 | 52 | if (fileListError) { 53 | return
错误: {fileListError.message}
; 54 | } 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {fileList?.map((file) => { 68 | return ( 69 | { 73 | if (file.is_directory) { 74 | navTo(file); 75 | } 76 | }} 77 | /> 78 | ); 79 | })} 80 | 81 |
名称大小修改时间操作
82 | ); 83 | }; 84 | 85 | return ( 86 |
87 | {/* 面包屑 */} 88 |
89 | back 94 |
{ 97 | navTo({ path: "/", ancestors: [] }); 98 | }} 99 | > 100 | 首页 101 |
102 | {ancestors?.map((item, index) => { 103 | return ( 104 | 105 | { 108 | const newAncestors = ancestors.slice(0, index + 1); 109 | navTo({ 110 | path: newAncestors.map((item) => item.name).join("/"), 111 | ancestors: newAncestors, 112 | }); 113 | }} 114 | > 115 | {item.name} 116 | 117 | {index !== ancestors.length - 1 && " / "} 118 | 119 | ); 120 | })} 121 |
122 | 复制路径 { 127 | navigator.clipboard.writeText(path).then( 128 | () => { 129 | message.success("已复制路径:" + path); 130 | }, 131 | () => { 132 | message.error("复制路径失败,请手动复制"); 133 | } 134 | ); 135 | }} 136 | /> 137 |
138 |
139 |
140 | {/* 返回上级目录 */} 141 | {path !== "/" && ( 142 |
{ 145 | const parentPath = ancestors 146 | .slice(0, -1) 147 | .map((item) => item.name) 148 | .join("/"); 149 | setSearchParams({ path: parentPath }); 150 | }} 151 | > 152 |
153 | back 158 | 返回上级目录 159 |
160 |
161 | )} 162 |
163 | 164 | {/* 文件列表 */} 165 | {renderFileList()} 166 |
167 | ); 168 | }; 169 | 170 | export default Index; 171 | -------------------------------------------------------------------------------- /frontend/src/views/player/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import Artplayer from "artplayer"; 3 | import { useSearchParams } from "react-router-dom"; 4 | import Hls from "hls.js"; 5 | 6 | const Player = () => { 7 | let [searchParams, _] = useSearchParams(); 8 | 9 | const pickcode = searchParams.get("pickcode"); 10 | const realType = searchParams.get("real_type"); 11 | 12 | const artRef = useRef(null); 13 | const artInstanceRef = useRef(null); 14 | 15 | const createArtOption = () => { 16 | const art = artInstanceRef.current!; 17 | const container = artRef.current!; 18 | 19 | const option: Artplayer["Option"] = { 20 | container: container, 21 | setting: true, 22 | fullscreen: true, 23 | fullscreenWeb: true, 24 | url: `/api/m3u8?pickcode=${pickcode}&definition=3`, 25 | type: "m3u8", 26 | settings: [ 27 | { 28 | html: "画质", 29 | width: 150, 30 | selector: [ 31 | { 32 | html: "转码 HD", 33 | url: `/api/m3u8?pickcode=${pickcode}&definition=3`, 34 | type: "m3u8", 35 | }, 36 | { 37 | html: "转码 UD", 38 | url: `/api/m3u8?pickcode=${pickcode}&definition=4`, 39 | type: "m3u8", 40 | }, 41 | { 42 | html: "直连 原始画质", 43 | url: `/api/download?pickcode=${pickcode}`, 44 | type: realType, 45 | }, 46 | ], 47 | onSelect: function (item, $dom, event) { 48 | console.info(item, $dom, event); 49 | art.switchQuality(item.url); 50 | art.type = item.type; 51 | art.notice.show = "切换画质: " + item.html; 52 | return item.html; 53 | }, 54 | }, 55 | ], 56 | customType: { 57 | m3u8: function playM3u8( 58 | video: HTMLMediaElement, 59 | url: string, 60 | art: Artplayer 61 | ) { 62 | if (Hls.isSupported()) { 63 | if (art.hls) art.hls.destroy(); 64 | const hls = new Hls(); 65 | hls.loadSource(url); 66 | hls.attachMedia(video); 67 | art.hls = hls; 68 | art.on("destroy", () => hls.destroy()); 69 | } else if (video.canPlayType("application/vnd.apple.mpegurl")) { 70 | video.src = url; 71 | } else { 72 | art.notice.show = "不支持播放格式: m3u8"; 73 | } 74 | }, 75 | }, 76 | }; 77 | 78 | return option; 79 | }; 80 | 81 | const getInstance = (art: Artplayer) => console.info(art); 82 | 83 | useEffect(() => { 84 | if (!artRef.current) { 85 | return; 86 | } 87 | 88 | const art = new Artplayer(createArtOption()); 89 | artInstanceRef.current = art; 90 | 91 | art.on("error", (error, reconnectTime) => { 92 | console.info(error, reconnectTime); 93 | }); 94 | 95 | if (getInstance && typeof getInstance === "function") { 96 | getInstance(art); 97 | } 98 | 99 | return () => { 100 | if (art && art.destroy) { 101 | art.destroy(false); 102 | } 103 | }; 104 | }, []); 105 | 106 | if (!pickcode) { 107 | return
无效的 pickcode
; 108 | } 109 | 110 | return ( 111 |
112 |
122 |
123 | ); 124 | }; 125 | 126 | export default Player; 127 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | corePlugins: { 9 | preflight: false, // 禁用掉 Tailwind 的 Preflight(基础样式重置) 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "experimentalDecorators": true, 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": "./", 23 | "paths": { 24 | "/@/*": [ 25 | "src/*" 26 | ], 27 | "#/*": ["types/*"] 28 | } 29 | }, 30 | "include": ["src", "types/**/*.d.ts",], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/types/axios.d.ts: -------------------------------------------------------------------------------- 1 | export type ErrorMessageMode = "none" | "modal" | "message" | undefined; 2 | export type SuccessMessageMode = ErrorMessageMode; 3 | 4 | export interface RequestOptions { 5 | // Splicing request parameters to url 6 | joinParamsToUrl?: boolean; 7 | // Format request parameter time 8 | formatDate?: boolean; 9 | // Whether to process the request result 10 | isTransformResponse?: boolean; 11 | // Whether to return native response headers 12 | // For example: use this attribute when you need to get the response headers 13 | isReturnNativeResponse?: boolean; 14 | // Whether to join url 15 | joinPrefix?: boolean; 16 | // Interface address, use the default apiUrl if you leave it blank 17 | apiUrl?: string; 18 | // 请求拼接路径 19 | urlPrefix?: string; 20 | // Error message prompt type 21 | errorMessageMode?: ErrorMessageMode; 22 | // Success message prompt type 23 | successMessageMode?: SuccessMessageMode; 24 | // Whether to add a timestamp 25 | joinTime?: boolean; 26 | ignoreCancelToken?: boolean; 27 | // Whether to send token in header 28 | withToken?: boolean; 29 | // 请求重试机制 30 | retryRequest?: RetryRequest; 31 | } 32 | 33 | export interface RetryRequest { 34 | isOpenRetry: boolean; 35 | count: number; 36 | waitTime: number; 37 | } 38 | export interface Result { 39 | code: number; 40 | type: "success" | "error" | "warning"; 41 | message: string; 42 | data: T; 43 | } 44 | 45 | // multipart/form-data: upload file 46 | export interface UploadFileParams { 47 | // Other parameters 48 | data?: Recordable; 49 | // File parameter interface field name 50 | name?: string; 51 | // file name 52 | file: File | Blob; 53 | // file name 54 | filename?: string; 55 | [key: string]: any; 56 | } 57 | -------------------------------------------------------------------------------- /frontend/types/index.d.ts: -------------------------------------------------------------------------------- 1 | type Recordable = Record; 2 | 3 | interface LoginFormValues { 4 | username: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | import tailwindcss from "tailwindcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import svgr from "vite-plugin-svgr"; 7 | import tailwindcssNesting from "tailwindcss/nesting"; 8 | import postcssImport from "postcss-import"; 9 | 10 | function pathResolve(dir: string) { 11 | return resolve(process.cwd(), ".", dir); 12 | } 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig(({ mode }) => { 16 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; 17 | 18 | return { 19 | root: "./", 20 | base: "/", 21 | publicDir: "public", 22 | plugins: [svgr(), react()], 23 | resolve: { 24 | alias: [ 25 | { 26 | find: /\/@\//, 27 | replacement: pathResolve("src") + "/", 28 | }, 29 | ], 30 | }, 31 | server: { 32 | host: true, 33 | port: 3000, 34 | // open: true, 35 | proxy: { 36 | "/api": { 37 | target: process.env.VITE_API_URL, 38 | changeOrigin: true, 39 | secure: false, 40 | }, 41 | "/ws": { 42 | target: "ws://127.0.0.1", 43 | ws: true, 44 | }, 45 | }, 46 | }, 47 | build: { 48 | target: "es2015", 49 | cssTarget: "chrome80", 50 | outDir: "dist", 51 | }, 52 | css: { 53 | postcss: { 54 | plugins: [postcssImport, tailwindcssNesting, tailwindcss, autoprefixer], 55 | }, 56 | }, 57 | }; 58 | }); 59 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | #安装后端依赖 2 | install-py: 3 | pip3 install -U -r requirements.txt 4 | 5 | #安装前端依赖 6 | install-f: 7 | cd frontend && pnpm i 8 | 9 | #启动前端 10 | dev-f: 11 | cd frontend && pnpm dev & 12 | 13 | #启动后端 14 | dev-b: 15 | python3 start.py 16 | 17 | #前端打包 18 | build: 19 | cd frontend && npm run build 20 | 21 | #启动前后端 22 | dev:dev-f dev-b 23 | 24 | #安装前后端依赖 25 | install:install-py install-f 26 | 27 | #启动前后端 28 | dev2: 29 | concurrently --kill-others-on-fail "make dev-f" "make dev-b" 30 | dev-b2: 31 | concurrently --kill-others-on-fail "make dev-b" 32 | 33 | #安装后端依赖2 34 | install-py2: 35 | concurrently --kill-others-on-fail "make install-py" 36 | 37 | #构建docker镜像 38 | docker: 39 | docker build -t alanoo/115_file_lister:latest . 40 | 41 | # 构建 docker 镜像并推送到 Docker Hub 42 | dp: 43 | docker buildx build --platform linux/amd64,linux/arm64 -t alanoo/115_file_lister:latest --push . -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cachetools 2 | blacksheep 3 | python-115 4 | httpx 5 | uvicorn 6 | supervisor -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | if not os.environ.get("WORKDIR"): 3 | workdir = os.path.join(os.path.dirname( 4 | os.path.dirname(os.path.abspath(__file__))), 'data') 5 | else: 6 | workdir = os.environ.get("WORKDIR") 7 | if not os.path.exists(workdir): 8 | os.makedirs(workdir) 9 | log_dir = os.path.join(workdir, 'logs') 10 | if not os.path.exists(log_dir): 11 | os.makedirs(log_dir) 12 | conf_dir = os.path.join(workdir, 'conf') 13 | if not os.path.exists(conf_dir): 14 | os.makedirs(conf_dir) 15 | 16 | if not os.path.exists(f'{workdir}/115-cookies.txt'): 17 | with open(f'{workdir}/115-cookies.txt', 'w', encoding='utf-8') as file: 18 | file.write('') 19 | os.environ["WORKDIR"] = workdir -------------------------------------------------------------------------------- /server/file_lister.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | 4 | __author__ = "ChenyangGao " 5 | __version__ = (0, 0, 5) 6 | __version_str__ = ".".join(map(str, __version__)) 7 | __doc__ = """\ 8 | 🕸️ 获取你的 115 网盘账号上文件信息和下载链接 🕷️ 9 | 10 | 🚫 注意事项:请求头需要携带 User-Agent。 11 | 如果使用 web 的下载接口,则有如下限制: 12 | - 大于等于 115 MB 时不能下载 13 | - 不能直接请求直链,需要携带特定的 Cookie 和 User-Agent 14 | """ 15 | 16 | from argparse import ArgumentParser, RawTextHelpFormatter 17 | 18 | parser = ArgumentParser( 19 | formatter_class=RawTextHelpFormatter, 20 | description=__doc__, 21 | ) 22 | parser.add_argument("-H", "--host", default="0.0.0.0", help="ip 或 hostname,默认值 '0.0.0.0'") 23 | parser.add_argument("-p", "--port", default=9115, type=int, help="端口号,默认值 9115") 24 | parser.add_argument("-r", "--reload", action="store_true", help="此项目所在目录下的文件发生变动时重启,此选项仅用于调试") 25 | parser.add_argument("-v", "--version", action="store_true", help="输出版本号") 26 | 27 | 28 | if __name__ == "__main__": 29 | from warnings import warn 30 | 31 | parser.add_argument("-c", "--cookies", default="", help="115 登录 cookies,优先级高于 -cp/--cookies-path") 32 | parser.add_argument("-cp", "--cookies-path", default="", help="""\ 33 | 存储 115 登录 cookies 的文本文件的路径,如果缺失,则从 115-cookies.txt 文件中获取,此文件可在如下目录之一: 34 | 1. 当前工作目录 35 | 2. 用户根目录 36 | 3. 此脚本所在目录""") 37 | parser.add_argument("-wc", "--web-cookies", default="", help="提供一个 web 的 cookies,因为目前使用的获取 .m3u8 的接口,需要 web 的 cookies 才能正确获取数据,如不提供,则将自动扫码获取") 38 | parser.add_argument("-l", "--lock-dir-methods", action="store_true", help="对 115 的文件系统进行增删改查的操作(但不包括上传和下载)进行加锁,限制为不可并发,这样就可减少 405 响应,以降低扫码的频率") 39 | parser.add_argument("-pc", "--path-persistence-commitment", action="store_true", help="路径持久性承诺,只要你能保证文件不会被移动(可新增删除,但对应的路径不可被其他文件复用),打开此选项,用路径请求直链时,可节约一半时间") 40 | 41 | args = parser.parse_args() 42 | if args.version: 43 | print(__version_str__) 44 | raise SystemExit(0) 45 | 46 | cookies = args.cookies 47 | cookies_path = args.cookies_path 48 | web_cookies = args.web_cookies 49 | lock_dir_methods = args.lock_dir_methods 50 | path_persistence_commitment = args.path_persistence_commitment 51 | else: 52 | from os import environ 53 | 54 | args = parser.parse_args() 55 | if args.version: 56 | print(__version_str__) 57 | raise SystemExit(0) 58 | 59 | print(""" 60 | \t\t🌍 支持如下环境变量 🛸 61 | 62 | - \x1b[1m\x1b[32mcookies\x1b[0m: 115 登录 cookies,优先级高于 \x1b[1m\x1b[32mcookies_path\x1b[0m 63 | - \x1b[1m\x1b[32mcookies_path\x1b[0m: 存储 115 登录 cookies 的文本文件的路径,如果缺失,则从 \x1b[4m\x1b[34m115-cookies.txt\x1b[0m 文件中获取,此文件可以在如下路径之一 64 | 1. 当前工作目录 65 | 2. 用户根目录 66 | 3. 此脚本所在目录 下 67 | - \x1b[1m\x1b[32mweb_cookies\x1b[0m: 提供一个 web 的 cookies,因为目前使用的获取 .m3u8 的接口,需要 web 的 cookies 才能正确获取数据,如不提供,则将自动扫码获取 68 | - \x1b[1m\x1b[32mlock_dir_methods\x1b[0m: (\x1b[1m\x1b传入任何值都视为设置,包括空字符串\x1b[0m)对 115 的文件系统进行增删改查的操作(\x1b[1m\x1b但不包括上传和下载\x1b[0m)进行加锁,限制为不可并发,这样就可减少 405 响应,以降低扫码的频率 69 | - \x1b[1m\x1b[32mpath_persistence_commitment\x1b[0m: (\x1b[1m\x1b传入任何值都视为设置,包括空字符串\x1b[0m)路径持久性承诺,只要你能保证文件不会被移动(\x1b[1m\x1b可新增删除,但对应的路径不可被其他文件复用\x1b[0m),打开此选项,用路径请求直链时,可节约一半时间 70 | """) 71 | cookies = environ.get("cookies", "") 72 | cookies_path = environ.get("cookies_path", "") 73 | cookies_path = environ.get("cookies_path", f"{environ.get('WORKDIR', '/app/data')}/115-cookies.txt") 74 | environ["VERSION_115_FILE_LISTER"] = f"{__version_str__}" 75 | web_cookies = environ.get("web_cookies", "") 76 | lock_dir_methods = environ.get("lock_dir_methods") is not None 77 | path_persistence_commitment = environ.get("path_persistence_commitment") is not None 78 | 79 | 80 | from asyncio import Lock 81 | from collections.abc import Mapping, MutableMapping 82 | from functools import partial, update_wrapper 83 | from os import stat 84 | from os.path import dirname, expanduser, join as joinpath, realpath 85 | from pathlib import Path 86 | from sys import exc_info 87 | from urllib.parse import quote 88 | 89 | from cachetools import LRUCache, TTLCache 90 | from blacksheep import ( 91 | route, text, html, file, redirect, 92 | Application, Content, Request, Response, StreamedContent 93 | ) 94 | from blacksheep.server.openapi.common import ParameterInfo 95 | from blacksheep.server.openapi.ui import ReDocUIProvider 96 | from blacksheep.server.openapi.v3 import OpenAPIHandler 97 | from blacksheep.server.remotes.forwarding import ForwardedHeadersMiddleware 98 | from openapidocs.v3 import Info # type: ignore 99 | from httpx import HTTPStatusError 100 | from p115 import P115Client, P115Url, AVAILABLE_APPS, AuthenticationError 101 | 102 | 103 | cookies_path_mtime = 0 104 | login_lock = Lock() 105 | web_login_lock = Lock() 106 | fs_lock = Lock() if lock_dir_methods else None 107 | 108 | if not cookies: 109 | if cookies_path: 110 | try: 111 | cookies = open(cookies_path).read() 112 | except FileNotFoundError: 113 | pass 114 | else: 115 | seen = set() 116 | for dir_ in (".", expanduser("~"), dirname(__file__)): 117 | dir_ = realpath(dir_) 118 | if dir_ in seen: 119 | continue 120 | seen.add(dir_) 121 | try: 122 | path = joinpath(dir_, "115-cookies.txt") 123 | cookies = open(path).read() 124 | cookies_path_mtime = stat(path).st_mtime_ns 125 | if cookies: 126 | cookies_path = path 127 | break 128 | except FileNotFoundError: 129 | pass 130 | 131 | def save_cookies(): 132 | if cookies_path: 133 | try: 134 | open(cookies_path, "w").write(client.cookies) 135 | except Exception: 136 | logger.exception("can't save cookies to file: %r", cookies_path) 137 | 138 | client = P115Client(cookies, app="qandroid") 139 | if cookies_path and cookies != client.cookies: 140 | save_cookies() 141 | 142 | device = "qandroid" 143 | if client.cookies: 144 | device = client.login_device()["icon"] 145 | if device not in AVAILABLE_APPS: 146 | # 115 浏览器版 147 | if device == "desktop": 148 | device = "web" 149 | else: 150 | warn(f"encountered an unsupported app {device!r}, fall back to 'qandroid'") 151 | device = "qandroid" 152 | fs = client.get_fs(client, path_to_id=LRUCache(65536)) 153 | # NOTE: id 到 pickcode 的映射 154 | id_to_pickcode: MutableMapping[int, str] = LRUCache(65536) 155 | # NOTE: 有些播放器,例如 IINA,拖动进度条后,可能会有连续 2 次请求下载链接,而后台请求一次链接大约需要 170-200 ms,因此弄个 0.3 秒的缓存 156 | url_cache: MutableMapping[tuple[str, str], P115Url] = TTLCache(64, ttl=0.3) 157 | 158 | 159 | app = Application() 160 | logger = getattr(app, "logger") 161 | docs = OpenAPIHandler(info=Info( 162 | title="115 filelist web api docs", 163 | version=__version_str__, 164 | )) 165 | docs.ui_providers.append(ReDocUIProvider()) 166 | docs.bind_app(app) 167 | common_status_docs = docs(responses={ 168 | 200: "请求成功", 169 | 401: "未登录或登录失效", 170 | 403: "禁止访问或权限不足", 171 | 404: "文件或目录不存在", 172 | 406: "不能完成请求", 173 | 500: "服务器错误", 174 | 503: "服务暂不可用", 175 | }) 176 | 177 | static_dir = Path(__file__).parents[1] / "static" 178 | if static_dir.exists(): 179 | app.serve_files(static_dir,fallback_document="index.html") 180 | else: 181 | logger.warning("no frontend provided") 182 | 183 | 184 | @app.on_middlewares_configuration 185 | def configure_forwarded_headers(app): 186 | app.middlewares.insert(0, ForwardedHeadersMiddleware(accept_only_proxied_requests=False)) 187 | 188 | 189 | def format_bytes( 190 | n: int, 191 | /, 192 | unit: str = "", 193 | precision: int = 2, 194 | ) -> str: 195 | "scale bytes to its proper byte format" 196 | if unit == "B" or not unit and n < 1024: 197 | return f"{n} B" 198 | b = 1 199 | b2 = 1024 200 | for u in ["K", "M", "G", "T", "P", "E", "Z", "Y"]: 201 | b, b2 = b2, b2 << 10 202 | if u == unit if unit else n < b2: 203 | break 204 | return f"%.{precision}f {u}B" % (n / b) 205 | 206 | 207 | async def relogin(exc=None): 208 | global cookies_path_mtime 209 | if exc is None: 210 | exc = exc_info()[0] 211 | mtime = cookies_path_mtime 212 | async with login_lock: 213 | need_update = mtime == cookies_path_mtime 214 | if cookies_path and need_update: 215 | try: 216 | mtime = stat(cookies_path).st_mtime_ns 217 | if mtime != cookies_path_mtime: 218 | client.cookies = open(cookies_path).read() 219 | cookies_path_mtime = mtime 220 | need_update = False 221 | except FileNotFoundError: 222 | logger.error("\x1b[1m\x1b[33m[SCAN] 🦾 文件空缺\x1b[0m") 223 | if need_update: 224 | if exc is None: 225 | logger.error("\x1b[1m\x1b[33m[SCAN] 🦾 重新扫码\x1b[0m") 226 | else: 227 | logger.error("""{prompt}一个 Web API 受限 (响应 "405: Not Allowed"), 将自动扫码登录同一设备\n{exc}""".format( 228 | prompt = "\x1b[1m\x1b[33m[SCAN] 🤖 重新扫码:\x1b[0m", 229 | exc = f" ├ \x1b[31m{type(exc).__module__}.{type(exc).__qualname__}\x1b[0m: {exc}") 230 | ) 231 | await client.login_another_app( 232 | device, 233 | replace=True, 234 | timeout=5, 235 | async_=True, 236 | ) 237 | if cookies_path: 238 | save_cookies() 239 | cookies_path_mtime = stat(cookies_path).st_mtime_ns 240 | 241 | 242 | async def call_wrap(func, /, *args, **kwds): 243 | kwds["async_"] = True 244 | try: 245 | if fs_lock is None: 246 | return await func(*args, **kwds) 247 | else: 248 | async with fs_lock: 249 | return await func(*args, **kwds) 250 | except HTTPStatusError as e: 251 | if e.response.status_code != 405: 252 | raise 253 | await relogin(e) 254 | return await call_wrap(func, *args, **kwds) 255 | 256 | 257 | def normalize_attr( 258 | attr: Mapping, 259 | origin: str = "", 260 | ) -> dict: 261 | KEYS = ( 262 | "id", "parent_id", "name", "path", "pickcode", "is_directory", "sha1", 263 | "size", "ico", "ctime", "mtime", "atime", "thumb", "star", "labels", 264 | "score", "hidden", "described", "violated", "ancestors", 265 | ) 266 | data = {k: attr[k] for k in KEYS if k in attr} 267 | data["id"] = str(data["id"]) 268 | data["parent_id"] = str(data["parent_id"]) 269 | for info in data["ancestors"]: 270 | info["id"] = str(info["id"]) 271 | info["parent_id"] = str(info["parent_id"]) 272 | if not attr["is_directory"]: 273 | pickcode = attr["pickcode"] 274 | url = f"{origin}/api/download{quote(attr['path'], safe=':/')}?pickcode={pickcode}" 275 | short_url = f"{origin}/api/download?pickcode={pickcode}" 276 | if attr["violated"] and attr["size"] < 1024 * 1024 * 115: 277 | url += "&web=true" 278 | short_url += "&web=true" 279 | data["format_size"] = format_bytes(attr["size"]) 280 | data["url"] = url 281 | data["short_url"] = short_url 282 | return data 283 | 284 | 285 | def redirect_exception_response(func, /): 286 | async def wrapper(*args, **kwds): 287 | try: 288 | return await func(*args, **kwds) 289 | except HTTPStatusError as e: 290 | return text( 291 | f"{type(e).__module__}.{type(e).__qualname__}: {e}", 292 | e.response.status_code, 293 | ) 294 | except AuthenticationError as e: 295 | return text(str(e), 401) 296 | except PermissionError as e: 297 | return text(str(e), 403) 298 | except FileNotFoundError as e: 299 | return text(str(e), 404) 300 | except OSError as e: 301 | return text(str(e), 500) 302 | except Exception as e: 303 | return text(str(e), 503) 304 | return update_wrapper(wrapper, func) 305 | 306 | 307 | @common_status_docs 308 | @route("/api/login/status", methods=["GET"]) 309 | @redirect_exception_response 310 | async def login_status(request: Request): 311 | """查询是否登录状态 312 | 313 |
314 |
如果是登录状态,返回 true,否则为 false 315 | """ 316 | return await client.login_status(async_=True) 317 | 318 | 319 | @common_status_docs 320 | @route("/api/login/qrcode/token", methods=["GET"]) 321 | @redirect_exception_response 322 | async def login_qrcode_token(request: Request): 323 | """获取扫码令牌 324 | """ 325 | resp = await client.login_qrcode_token(async_=True) 326 | if resp["state"]: 327 | data = resp["data"] 328 | data["qrcode_image"] = "https://qrcodeapi.115.com/api/1.0/web/1.0/qrcode?uid=" + data["uid"] 329 | return data 330 | raise OSError(resp) 331 | 332 | 333 | @common_status_docs 334 | @route("/api/login/qrcode/status", methods=["GET"]) 335 | @redirect_exception_response 336 | async def login_qrcode_status(request: Request, uid: str, time: int, sign: str): 337 | """查询扫码状态 338 | 339 |
340 |
返回的状态码: 341 |
  0:waiting 342 |
  1:scanned 343 |
  2:signed in 344 |
  -1:expired 345 |
  -2:canceled 346 |
  其它:abort 347 | 348 | :param uid: 扫码的 uid (由 /api/login/qrcode/token 获取) 349 | :param time: 扫码令牌的请求时间 (由 /api/login/qrcode/token 获取) 350 | :param sign: 扫码的 uid (由 /api/login/qrcode/token 获取) 351 | """ 352 | payload = {"uid": uid, "time": time, "sign": sign} 353 | while True: 354 | try: 355 | resp = await client.login_qrcode_status(payload, async_=True) 356 | except Exception: 357 | continue 358 | else: 359 | if resp["state"]: 360 | data = resp["data"] 361 | match data.get("status"): 362 | case 0: 363 | data["message"] = "waiting" 364 | case 1: 365 | data["message"] = "scanned" 366 | case 2: 367 | data["message"] = "signed in" 368 | case -1: 369 | data["message"] = "expired" 370 | case -2: 371 | data["message"] = "canceled" 372 | case _: 373 | data["message"] = "abort" 374 | return data 375 | raise OSError(resp) 376 | 377 | 378 | @common_status_docs 379 | @route("/api/login/qrcode/result", methods=["GET"]) 380 | @redirect_exception_response 381 | async def login_qrcode_result(request: Request, uid: str, app: str = "qandroid"): 382 | """绑定扫码结果 383 | 384 | :param uid: 扫码的 uid (由 /api/login/qrcode/token 获取) 385 | :param app: 绑定到设备,默认值 "qandroid" 386 | """ 387 | global device 388 | resp = await client.login_qrcode_result({"account": uid, "app": app}) 389 | if resp["state"]: 390 | data = resp["data"] 391 | client.cookies = data["cookie"] 392 | if cookies_path: 393 | save_cookies() 394 | device = app 395 | return data 396 | raise OSError(resp) 397 | 398 | 399 | @common_status_docs 400 | @route("/api/attr", methods=["GET", "HEAD"]) 401 | @route("/api/attr/{path:path2}", methods=["GET", "HEAD"]) 402 | @redirect_exception_response 403 | async def get_attr( 404 | request: Request, 405 | pickcode: str = "", 406 | id: int = -1, 407 | path: str = "", 408 | path2: str = "", 409 | ): 410 | """获取文件或目录的属性 411 | 412 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 413 | :param id: 文件或目录的 id,优先级高于 path 414 | :param path: 文件或目录的路径,优先级高于 path2 415 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 416 | """ 417 | if pickcode: 418 | id = await call_wrap(fs.get_id_from_pickcode, pickcode) 419 | attr = await call_wrap(fs.attr, (path or path2) if id < 0 else id) 420 | origin = f"{request.scheme}://{request.host}" 421 | return normalize_attr(attr, origin) 422 | 423 | 424 | @common_status_docs 425 | @route("/api/list", methods=["GET", "HEAD"]) 426 | @route("/api/list/{path:path2}", methods=["GET", "HEAD"]) 427 | @redirect_exception_response 428 | async def get_list( 429 | request: Request, 430 | pickcode: str = "", 431 | id: int = -1, 432 | path: str = "", 433 | path2: str = "", 434 | ): 435 | """罗列归属于此目录的所有文件和目录属性 436 | 437 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 438 | :param id: 文件或目录的 id,优先级高于 path 439 | :param path: 文件或目录的路径,优先级高于 path2 440 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 441 | """ 442 | if pickcode: 443 | id = await call_wrap(fs.get_id_from_pickcode, pickcode) 444 | children = await call_wrap(fs.listdir_attr, (path or path2) if id < 0 else id) 445 | origin = f"{request.scheme}://{request.host}" 446 | return [ 447 | normalize_attr(attr, origin) 448 | for attr in children 449 | ] 450 | 451 | 452 | @common_status_docs 453 | @route("/api/ancestors", methods=["GET", "HEAD"]) 454 | @route("/api/ancestors/{path:path2}", methods=["GET", "HEAD"]) 455 | @redirect_exception_response 456 | async def get_ancestors( 457 | pickcode: str = "", 458 | id: int = -1, 459 | path: str = "", 460 | path2: str = "", 461 | ): 462 | """获取祖先节点 463 | 464 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 465 | :param id: 文件或目录的 id,优先级高于 path 466 | :param path: 文件或目录的路径,优先级高于 path2 467 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 468 | """ 469 | if pickcode: 470 | id = await call_wrap(fs.get_id_from_pickcode, pickcode) 471 | return await call_wrap(fs.get_ancestors, (path or path2) if id < 0 else id) 472 | 473 | 474 | @common_status_docs 475 | @route("/api/desc", methods=["GET", "HEAD"]) 476 | @route("/api/desc/{path:path2}", methods=["GET", "HEAD"]) 477 | @redirect_exception_response 478 | async def get_desc( 479 | pickcode: str = "", 480 | id: int = -1, 481 | path: str = "", 482 | path2: str = "", 483 | ): 484 | """获取备注 485 | 486 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 487 | :param id: 文件或目录的 id,优先级高于 path 488 | :param path: 文件或目录的路径,优先级高于 path2 489 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 490 | """ 491 | if pickcode: 492 | id = await call_wrap(fs.get_id_from_pickcode, pickcode) 493 | return html(await call_wrap(fs.desc, (path or path2) if id < 0 else id)) 494 | 495 | 496 | @common_status_docs 497 | @route("/api/url", methods=["GET", "HEAD"]) 498 | @route("/api/url/{path:path2}", methods=["GET", "HEAD"]) 499 | @redirect_exception_response 500 | async def get_url( 501 | request: Request, 502 | pickcode: str = "", 503 | id: int = -1, 504 | path: str = "", 505 | path2: str = "", 506 | web: bool = False, 507 | ): 508 | """获取下载链接 509 | 510 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 511 | :param id: 文件或目录的 id,优先级高于 path 512 | :param path: 文件或目录的路径,优先级高于 path2 513 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 514 | :param web: 是否使用 web 接口获取下载链接。如果文件被封禁,但小于 115 MB,启用此选项可成功下载文件 515 | """ 516 | user_agent = (request.get_first_header(b"User-agent") or b"").decode("utf-8") 517 | if not pickcode: 518 | pickcode = await call_wrap(fs.get_pickcode, (path or path2) if id < 0 else id) 519 | try: 520 | url = url_cache[(pickcode, user_agent)] 521 | except KeyError: 522 | url = url_cache[(pickcode, user_agent)] = await call_wrap( 523 | fs.get_url_from_pickcode, 524 | pickcode, 525 | headers={"User-Agent": user_agent}, 526 | use_web_api=web, 527 | ) 528 | return {"url": url, "headers": url["headers"]} 529 | 530 | 531 | @common_status_docs 532 | @route("/api/download", methods=["GET", "HEAD"]) 533 | @route("/api/download/{path:path2}", methods=["GET", "HEAD"]) 534 | @redirect_exception_response 535 | async def file_download( 536 | request: Request, 537 | pickcode: str = "", 538 | id: int = -1, 539 | path: str = "", 540 | path2: str = "", 541 | web: bool = False, 542 | ): 543 | """下载文件 544 | 545 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 546 | :param id: 文件或目录的 id,优先级高于 path 547 | :param path: 文件或目录的路径,优先级高于 path2 548 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 549 | :param web: 是否使用 web 接口获取下载链接。如果文件被封禁,但小于 115 MB,启用此选项可成功下载文件 550 | """ 551 | resp = await get_url.__wrapped__(request, pickcode, id, path, path2, web=web) 552 | url = resp["url"] 553 | headers = resp["headers"] 554 | if web: 555 | if bytes_range := request.get_first_header(b"Range"): 556 | headers["Range"] = bytes_range.decode("utf-8") 557 | stream = await client.request(url, headers=headers, parse=None, async_=True) 558 | resp_headers = [ 559 | (k.encode("utf-8"), v.encode("utf-8")) 560 | for k, v in stream.headers.items() 561 | if k.lower() not in ("content-type", "content-disposition", "date") 562 | ] 563 | resp_headers.append((b"Content-Disposition", b'attachment; filename="%s"' % quote(url["file_name"]).encode("ascii"))) 564 | return Response( 565 | stream.status_code, 566 | headers=resp_headers, 567 | content=StreamedContent( 568 | (stream.headers.get("Content-Type") or "application/octet-stream").encode("utf-8"), 569 | partial(stream.aiter_bytes, 1 << 16), 570 | ), 571 | ) 572 | return redirect(url) 573 | 574 | 575 | @common_status_docs 576 | @route("/api/m3u8", methods=["GET", "HEAD"]) 577 | @route("/api/m3u8/{path:path2}", methods=["GET", "HEAD"]) 578 | @redirect_exception_response 579 | async def file_m3u8( 580 | request: Request, 581 | pickcode: str = "", 582 | id: int = -1, 583 | path: str = "", 584 | path2: str = "", 585 | definition: int = 4, 586 | ): 587 | """获取音视频的 m3u8 文件 588 | 589 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 590 | :param id: 文件或目录的 id,优先级高于 path 591 | :param path: 文件或目录的路径,优先级高于 path2 592 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 593 | :param definition: 分辨率,默认值 4,如果传入 0,则获取所有 .m3u8 的链接。
  3 - HD
  4 - UD 594 | """ 595 | global web_cookies 596 | user_agent = (request.get_first_header(b"User-agent") or b"").decode("utf-8") 597 | if not pickcode: 598 | pickcode = await call_wrap(fs.get_pickcode, (path or path2) if id < 0 else id) 599 | url = f"http://115.com/api/video/m3u8/{pickcode}.m3u8?definition={definition}" 600 | async with web_login_lock: 601 | if not web_cookies: 602 | if device == "web": 603 | web_cookies = client.cookies 604 | else: 605 | web_cookies = (await client.login_another_app("web", async_=True)).cookies 606 | while True: 607 | try: 608 | data = await client.request( 609 | url, 610 | headers={"User-Agent": user_agent, "Cookie": web_cookies}, 611 | parse=False, 612 | async_=True, 613 | ) 614 | break 615 | except HTTPStatusError as e: 616 | if e.response.status_code not in (403, 405): 617 | raise 618 | async with web_login_lock: 619 | web_cookies = (await client.login_another_app("web", replace=device=="web", async_=True)).cookies 620 | if not data: 621 | raise FileNotFoundError("404: .m3u8 of this file was not found") 622 | if definition == 0: 623 | return Response( 624 | 200, 625 | content=Content(b"application/x-mpegurl", data), 626 | ) 627 | url = data.split()[-1].decode("ascii") 628 | data = await client.request( 629 | url, 630 | headers={"User-Agent": user_agent}, 631 | parse=False, 632 | async_=True, 633 | ) 634 | return redirect(url) 635 | 636 | 637 | @common_status_docs 638 | @route("/api/subtitle", methods=["GET", "HEAD"]) 639 | @route("/api/subtitle/{path:path2}", methods=["GET", "HEAD"]) 640 | @redirect_exception_response 641 | async def file_subtitle( 642 | request: Request, 643 | pickcode: str = "", 644 | id: int = -1, 645 | path: str = "", 646 | path2: str = "", 647 | ): 648 | """获取音视频的字幕信息 649 | 650 | :param pickcode: 文件或目录的 pickcode,优先级高于 id 651 | :param id: 文件或目录的 id,优先级高于 path 652 | :param path: 文件或目录的路径,优先级高于 path2 653 | :param path2: 文件或目录的路径,这个直接在接口路径之后,不在查询字符串中 654 | """ 655 | global web_cookies 656 | user_agent = (request.get_first_header(b"User-agent") or b"").decode("utf-8") 657 | if not pickcode: 658 | pickcode = await call_wrap(fs.get_pickcode, (path or path2) if id < 0 else id) 659 | resp = await call_wrap(client.fs_files_video_subtitle, pickcode) 660 | return resp 661 | 662 | 663 | def main(): 664 | import uvicorn 665 | from pathlib import Path 666 | 667 | uvicorn.run( 668 | app, 669 | host=args.host, 670 | port=args.port, 671 | reload=args.reload, 672 | proxy_headers=True, 673 | forwarded_allow_ips="*", 674 | ) 675 | 676 | 677 | if __name__ == "__main__": 678 | main() -------------------------------------------------------------------------------- /server/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | import os 4 | def echo_author(): 5 | docker_version = os.environ.get("VERSION_115_FILE_LISTER") 6 | print("""开始启动 7 | 8 | ██╗ ██╗███████╗ ███████╗██╗██╗ ███████╗ ██╗ ██╗███████╗████████╗███████╗██████╗ 9 | ███║███║██╔════╝ ██╔════╝██║██║ ██╔════╝ ██║ ██║██╔════╝╚══██╔══╝██╔════╝██╔══██╗ 10 | ╚██║╚██║███████╗ █████╗ ██║██║ █████╗ ██║ ██║███████╗ ██║ █████╗ ██████╔╝ 11 | ██║ ██║╚════██║ ██╔══╝ ██║██║ ██╔══╝ ██║ ██║╚════██║ ██║ ██╔══╝ ██╔══██╗ 12 | ██║ ██║███████║ ██║ ██║███████╗███████╗ ███████╗██║███████║ ██║ ███████╗██║ ██║ 13 | ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚══════╝╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ 14 | """) 15 | print(f"Version: {docker_version}") 16 | 17 | def startup(): 18 | echo_author() 19 | print(f'程序启动成功') 20 | 21 | def main(): 22 | from server.file_lister import main 23 | startup() 24 | main() 25 | 26 | if __name__ == "__main__": 27 | from pathlib import Path 28 | from sys import path 29 | 30 | path[0] = str(Path(__file__).parents[1]) 31 | main() -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | from server.main import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | user=115FileLister 3 | PUID=${PUID:-0} 4 | PGID=${PGID:-0} 5 | #create user if not exists 6 | if id -u $user >/dev/null 2>&1 ;then 7 | echo "$user exists" 8 | else 9 | echo "create $user(${PUID}): $user(${PGID})" 10 | useradd -U -d /data -s /bin/false $user 11 | usermod -G users $user 12 | groupmod -o -g "$PGID" $user 13 | usermod -o -u "$PUID" $user 14 | fi 15 | 16 | base_path='/app' 17 | 18 | chown -R $user:$user /app 19 | 20 | supervisord -c /app/supervisord.conf -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root ; 设置 supervisord 进程以 root 用户身份运行 4 | 5 | [program:115FileLister] 6 | user = 115FileLister 7 | directory = /app 8 | command = python start.py 9 | startsecs=3 10 | autostart = true 11 | autorestart = true 12 | redirect_stderr=true 13 | stdout_logfile=/dev/fd/1 14 | stdout_logfile_maxbytes=0 --------------------------------------------------------------------------------