├── .github └── workflows │ ├── deploy.yml │ └── docsearch.yml ├── .gitignore ├── .vitepress ├── config.ts └── theme │ └── index.ts ├── CNAME ├── README.md ├── archives.md ├── category.md ├── docs ├── auto-organize.md ├── cover.md ├── extend.md ├── full-sync.md ├── save-download.md ├── share-sync.md ├── tmdb.md └── vps.md ├── docsearch.json ├── donate └── index.md ├── faq └── index.md ├── flow └── index.md ├── index.md ├── install └── index.md ├── note.md ├── package-lock.json ├── package.json ├── public ├── docs-img │ ├── share-115.png │ └── share-cid.png ├── faq │ └── auto-error.jpg ├── favicon.ico ├── install │ ├── full-sync.png │ ├── lift-1.png │ └── lift-2.png ├── profile.png ├── wx.jpg └── zfb.jpg ├── src ├── assets │ ├── HarmonyOS_Sans_SC_Bold.woff2 │ ├── HarmonyOS_Sans_SC_Medium.woff2 │ ├── HarmonyOS_Sans_SC_Regular.woff2 │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── components │ ├── AdItem.vue │ ├── LinkList.vue │ ├── Pagination.vue │ ├── PostInfoItem.vue │ ├── PostList.vue │ └── PostListLite.vue ├── composables │ ├── useAds.ts │ ├── useGroup.ts │ ├── useOutDir.ts │ └── usePosts.ts ├── index.ts ├── styles │ ├── font.less │ ├── index.less │ ├── page.less │ └── post.less ├── types.ts ├── utils │ ├── fileExists.ts │ ├── formatDate.ts │ ├── generateMd.ts │ ├── generatePages.ts │ ├── generateString.ts │ ├── removeMdPro.ts │ └── writeMd.ts └── views │ ├── GroupView.vue │ ├── HomeView.vue │ ├── PageView.vue │ └── ThemeLayout.vue ├── tags.md ├── tsconfig.json └── vercel.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy cms docs 2 | 3 | on: 4 | push: 5 | branches: [master-xxx] 6 | 7 | # 允许你从 Actions 选项卡手动运行此工作流程 8 | workflow_dispatch: 9 | 10 | # 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列 17 | # 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成 18 | concurrency: 19 | group: pages 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | # 构建工作 24 | build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 # 如果未启用 lastUpdated,则不需要 31 | # - uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消注释 32 | - name: Setup Node 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | cache: npm # 或 pnpm / yarn 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v4 39 | - name: Install dependencies 40 | run: npm ci # 或 pnpm install / yarn install / bun install 41 | - name: Build with VitePress 42 | run: npm run build # 或 pnpm docs:build / yarn docs:build / bun run docs:build 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: .vitepress/dist 47 | 48 | # 部署工作 49 | deploy: 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | needs: build 54 | runs-on: ubuntu-latest 55 | name: Deploy 56 | steps: 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/docsearch.yml: -------------------------------------------------------------------------------- 1 | name: docsearch 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | algolia: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Get the content of docsearch.json as config 15 | id: algolia_config 16 | run: echo "::set-output name=config::$(cat docsearch.json | jq -r tostring)" 17 | 18 | - name: Run algolia/docsearch-scraper image 19 | env: 20 | APPLICATION_ID: ${{ secrets.APPLICATION_ID }} 21 | API_KEY: ${{ secrets.API_KEY }} 22 | CONFIG: ${{ steps.algolia_config.outputs.config }} 23 | run: | 24 | docker run \ 25 | --env APPLICATION_ID=${APPLICATION_ID} \ 26 | --env API_KEY=${API_KEY} \ 27 | --env "CONFIG=${CONFIG}" \ 28 | algolia/docsearch-scraper 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vitepress/dist 3 | .vitepress/cache 4 | !README.md 5 | !README_en-US.md 6 | !about.md 7 | !tags.md 8 | !archives.md 9 | !category.md -------------------------------------------------------------------------------- /.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfigWithTheme } from 'vitepress'; 2 | import { usePosts } from '../src/composables/usePosts'; 3 | import type { ThemeConfig } from '../src/types'; 4 | import { MermaidMarkdown, MermaidPlugin } from 'vitepress-plugin-mermaid'; 5 | 6 | const { posts, rewrites } = await usePosts({ 7 | pageSize: 10, 8 | homepage: false, 9 | srcDir: 'posts', 10 | autoExcerpt: 150 11 | }); 12 | 13 | export default defineConfigWithTheme({ 14 | lang: 'zh-CN', 15 | title: 'CloudMediaSynC', 16 | titleTemplate: '云端媒体库同步工具', 17 | description: '云端媒体库同步工具', 18 | rewrites, 19 | cleanUrls: true, 20 | ignoreDeadLinks: true, 21 | base: '/', 22 | 23 | themeConfig: { 24 | posts, 25 | page: { 26 | max: 5 27 | }, 28 | logo: '/profile.png', 29 | //侧边栏文字更改(移动端) 30 | sidebarMenuLabel: '目录', 31 | 32 | //返回顶部文字修改(移动端) 33 | returnToTopLabel: '返回顶部', 34 | 35 | //大纲显示2-3级标题 36 | outline: { 37 | level: [2, 3], 38 | label: '摘要' 39 | }, 40 | 41 | //自定义上下页名 42 | docFooter: { 43 | prev: '上一页', 44 | next: '下一页', 45 | }, 46 | //手机端深浅模式文字修改 47 | darkModeSwitchLabel: '深浅模式', 48 | 49 | //编辑本页 50 | editLink: { 51 | pattern: 'https://github.com/imaliang/cms-docs/edit/master/:path', // 改成自己的仓库 52 | text: '在GitHub编辑本页' 53 | }, 54 | //上次更新时间 55 | lastUpdated: { 56 | text: '上次更新时间', 57 | formatOptions: { 58 | dateStyle: 'short', // 可选值full、long、medium、short 59 | timeStyle: 'medium' // 可选值full、long、medium、short 60 | }, 61 | }, 62 | nav: [ 63 | { text: '首页', link: '/' }, 64 | { text: '安装', link: '/install' }, 65 | { text: '图解', link: '/flow' }, 66 | { 67 | text: '进阶', 68 | items: [ 69 | { text: '进阶教程', link: '/docs/full-sync' }, 70 | { text: 'CMSHelp', link: 'https://github.com/guyue2005/CMSHelp/wiki' }, 71 | ] 72 | }, 73 | { text: '捐赠', link: '/donate' }, 74 | { text: 'FAQ', link: '/faq' }, 75 | 76 | ], 77 | sidebar: { 78 | '/docs': [ 79 | { 80 | text: 'CMS进阶文档', 81 | items: [ 82 | { text: '全量同步', link: '/docs/full-sync' }, 83 | { text: '自动整理', link: '/docs/auto-organize' }, 84 | { text: '转存下载', link: '/docs/save-download' }, 85 | { text: '扩展教程', link: '/docs/extend' }, 86 | ] 87 | }, 88 | { 89 | text: '插件扩展', 90 | items: [ 91 | { text: '115分享同步', link: '/docs/share-sync' }, 92 | { text: '媒体库封面生成', link: '/docs/cover' }, 93 | ] 94 | }, 95 | { 96 | text: '扩展推荐', 97 | items: [ 98 | { text: 'tmdb代理', link: '/docs/tmdb' }, 99 | { text: 'vps推荐', link: '/docs/vps' }, 100 | ] 101 | } 102 | ] 103 | }, 104 | socialLinks: [ 105 | { 106 | icon: { 107 | svg: '' 108 | }, 109 | link: 'https://github.com/imaliang/cms-docs', 110 | }, 111 | { 112 | icon: { 113 | svg: '' 114 | }, 115 | link: 'https://hub.docker.com/r/imaliang/cloud-media-sync', 116 | }, 117 | { 118 | icon: { 119 | svg: '' 120 | }, 121 | link: 'https://t.me/cloud_media_sync', 122 | } 123 | ], 124 | footer: { 125 | message: 'Docker VersionDocker PullsVercel Status', 126 | copyright: `Copyright © 2024-${new Date().getFullYear()} 今晚打老虎` 127 | }, 128 | 129 | search: { 130 | provider: 'algolia', 131 | // provider: 'local', 132 | options: { 133 | appId: 'RBF5FKZU8U', 134 | apiKey: '0813f40a2e6a4add25ed8083d89a738c', 135 | indexName: 'cms-docs', 136 | locales: { 137 | root: { 138 | placeholder: '搜索文档', 139 | translations: { 140 | button: { 141 | buttonText: '搜索文档', 142 | buttonAriaLabel: '搜索文档' 143 | }, 144 | modal: { 145 | searchBox: { 146 | resetButtonTitle: '清除查询条件', 147 | resetButtonAriaLabel: '清除查询条件', 148 | cancelButtonText: '取消', 149 | cancelButtonAriaLabel: '取消' 150 | }, 151 | startScreen: { 152 | recentSearchesTitle: '搜索历史', 153 | noRecentSearchesText: '没有搜索历史', 154 | saveRecentSearchButtonTitle: '保存至搜索历史', 155 | removeRecentSearchButtonTitle: '从搜索历史中移除', 156 | favoriteSearchesTitle: '收藏', 157 | removeFavoriteSearchButtonTitle: '从收藏中移除' 158 | }, 159 | errorScreen: { 160 | titleText: '无法获取结果', 161 | helpText: '你可能需要检查你的网络连接' 162 | }, 163 | footer: { 164 | selectText: '选择', 165 | navigateText: '切换', 166 | closeText: '关闭', 167 | searchByText: '搜索提供者' 168 | }, 169 | noResultsScreen: { 170 | noResultsText: '无法找到相关结果', 171 | suggestedQueryText: '你可以尝试查询', 172 | reportMissingResultsText: '你认为该查询应该有结果?', 173 | reportMissingResultsLinkText: '点击反馈' 174 | }, 175 | }, 176 | }, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }, 182 | 183 | 184 | //markdown配置 185 | markdown: { 186 | theme: 'one-dark-pro', 187 | //行号显示 188 | lineNumbers: true, 189 | 190 | // toc显示一级标题 191 | toc: { level: [1, 2, 3] }, 192 | 193 | // 使用 `!!code` 防止转换 194 | codeTransformers: [ 195 | { 196 | postprocess(code) { 197 | return code.replace(/\[\!\!code/g, '[!code') 198 | } 199 | } 200 | ], 201 | 202 | // 开启图片懒加载 203 | image: { 204 | lazyLoading: true 205 | }, 206 | 207 | // 组件插入h1标题下 208 | config: (md) => { 209 | md.use(MermaidMarkdown); 210 | } 211 | 212 | }, 213 | 214 | vite: { 215 | plugins: [ 216 | [MermaidPlugin()] 217 | ], 218 | optimizeDeps: { 219 | include: ['mermaid'], 220 | }, 221 | ssr: { 222 | noExternal: ['mermaid'], 223 | }, 224 | }, 225 | 226 | srcExclude: ['README.md', 'note.md'] 227 | }); 228 | -------------------------------------------------------------------------------- /.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from '../../src'; 2 | 3 | export default Theme; 4 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docs.cmscc.cc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cms-docs 2 | 3 | 地址一:[https://wiki.cmscc.cc](https://wiki.cmscc.cc) 4 | 5 | 地址二:[https://docs.cmscc.cc](https://docs.cmscc.cc) -------------------------------------------------------------------------------- /archives.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 归档 3 | layout: page 4 | --- 5 | 6 | 7 | -------------------------------------------------------------------------------- /category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 分类 3 | layout: page 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /docs/auto-organize.md: -------------------------------------------------------------------------------- 1 | # 自动整理 2 | 3 | > [!TIP] 4 | > 帮你整理 115 文件夹里的视频文件,并按照你设置的规则进行重命名和分类到指定文件夹 5 | 6 | 自动整理必须先执行完成一次全量同步,必须创建好二级分类策略,待整理文件夹必须在媒体库外!不要一次整理太多文件! 7 | 8 | ## 自动整理流程 9 | 10 | ```mermaid 11 | graph TD 12 | A(待整理文件夹) --> B{sha1已存在?} 13 | B -->|是| C(已存在文件夹) 14 | C --> Z[结束] 15 | B -->|否| D[开始识别] 16 | D -->|电视剧| G{emby中集已存在?} 17 | G -->|否| J[执行二级分类策略] 18 | G -->|是| C 19 | J --> M[整理成功] 20 | M --> Z 21 | D -->|电影| J 22 | D -->|识别失败| K(冗余文件夹) 23 | K --> Z 24 | style A fill:#e6ffd0,stroke:#a3d46b 25 | style C fill:#e6ffd0,stroke:#a3d46b 26 | style K fill:#e6ffd0,stroke:#a3d46b 27 | ``` 28 | 29 | ## 基础配置 30 | 31 | - 待整理文件夹 cid:将你的视频放到这个文件夹,就会自动整理,这个文件夹必须在你全量同步的文件夹外面 32 | - 已存在文件夹 cid:整理时如果该文件被全量同步过,就移动到这里,根据 sha1 判断;如果是剧集,还会去 emby 里判断当前集是否已有 33 | - 冗余数据文件夹 cid:整理之后剩余的数据(识别失败的、不需要处理的)将移动到该文件夹,防止自动任务重复整理 34 | 35 | ## 重命名规则 36 | 37 | 用于对视频进行重命名,请先阅读几遍变量说明和语法说明,再开始配置 38 | 39 | 文件夹命名示例 40 | 41 | ```yaml 42 | {first_letter}-{ title } - { year } - [(tmdb = { tmdb_id })]; 43 | ``` 44 | 45 | 文件命名示例 46 | 47 | ```js 48 | {title}.{year}<.{resource_pix}><.{fps}><.{resource_version}><.{resource_source}><.{resource_type}><.{resource_effect}><.{video_encode}><.{audio_encode}><-{resource_team}> 49 | ``` 50 | 51 | {变量名} 代表变量,取这个变量的值,用尖括号包围的字符串称为 块,块里 {变量名} 表示当 {变量名}不为空时,取块里的内容 52 | 53 | 自定义命名规则就是自定义多个块,也是多个 <...>,最终这些块的按顺序拼在一块 54 | 55 | 详细的变量、语法说明请在 cms 里查看 56 | 57 | ## 二级分类策略 58 | 59 | 用于对视频进行二级分类,移动到指定的文件夹 60 | 61 | 二级分类里的文件夹必须是你同步过的文件夹,否则整理时会有文件夹已存在的错误 62 | 63 | 二级分类策略为 yaml 格式,缩进很重要,同级要对齐;会从上到下依次匹配,匹配到就停止 64 | 65 | 这里面除了兜底匹配都可以删除,可以根据自己的情况进行删除或增加 66 | 67 | ::: code-group 68 | 69 | ```yaml [复杂的二级分类策略] 70 | # 配置电影的分类策略 71 | movie: 72 | # 分类名仅为标识 不起任何作用 73 | 原盘电影: 74 | ###### cid为115文件夹的cid 必须有 ###### 75 | cid: 1000000000000000001 76 | # 匹配 后缀 77 | ext: "iso" 78 | 动画电影: 79 | cid: 100 80 | # 匹配 genre_ids 内容类型,16是动漫 81 | genre_ids: "16" 82 | 华语电影: 83 | cid: 1000000000000000002 84 | # 匹配语种 85 | original_language: "zh,cn,bo,za" 86 | # 兜底匹配,必须要有一个这样没有匹配条件的策略 87 | 外语电影: 88 | cid: 1000000000000000003 89 | 90 | # 配置电视剧的分类策略 91 | tv: 92 | # 分类名仅为标识 不起任何作用 93 | 国漫: 94 | cid: 1000000000000000004 95 | genre_ids: "16" 96 | # 匹配 origin_country 国家,CN是中国大陆,TW是中国台湾,HK是中国香港 97 | origin_country: "CN,TW,HK" 98 | 日番: 99 | cid: 1000000000000000005 100 | genre_ids: "16" 101 | # 匹配 origin_country 国家,JP是日本 102 | origin_country: "JP" 103 | 纪录片: 104 | cid: 1000000000000000006 105 | # 匹配 genre_ids 内容类型,99是纪录片 106 | genre_ids: "99" 107 | 儿童: 108 | cid: 1000000000000000007 109 | # 匹配 genre_ids 内容类型,10762是儿童 110 | genre_ids: "10762" 111 | 综艺: 112 | cid: 1000000000000000008 113 | # 匹配 genre_ids 内容类型,10764 10767都是综艺 114 | genre_ids: "10764,10767" 115 | 国产剧: 116 | cid: 1000000000000000009 117 | # 匹配 origin_country 国家,CN是中国大陆,TW是中国台湾,HK是中国香港 118 | origin_country: "CN,TW,HK" 119 | 欧美剧: 120 | cid: 1000000000000000010 121 | # 匹配 origin_country 国家,主要欧美国家列表 122 | origin_country: "US,FR,GB,DE,ES,IT,NL,PT,RU,UK" 123 | 日韩剧: 124 | cid: 1000000000000000011 125 | # 匹配 origin_country 国家,主要亚洲国家列表 126 | origin_country: "JP,KP,KR,TH,IN,SG" 127 | # 兜底匹配 128 | 未分类: 129 | cid: 1000000000000000012 130 | ``` 131 | 132 | ```yaml [简单的二级分类策略] 133 | movie: 134 | 电影: 135 | cid: 1000000000000000001 136 | tv: 137 | 剧集: 138 | cid: 1000000000000000002 139 | ``` 140 | 141 | ::: 142 | 143 | ## 洗版策略 144 | 145 | 你可以设置任意多个洗版策略 146 | 147 | - `mode` 是洗版模式,代表你想用哪种策略进行洗版 148 | - `media_type` 设置这个策略用于哪个媒体类型,去掉这个字段即匹配所有 149 | - `category` 设置这个策略用于哪个`分类`(对应于二级分类策略里的分类名),去掉这个字段即匹配所有 150 | - `priority_level` 匹配优先级(就是符合这些规则的视频就会执行这个洗版策略) 151 | 152 | 优先级匹配支持所有重命名规则里的变量名 153 | 154 | ```yaml 155 | 电影洗版策略: 156 | mode: replace 157 | media_type: movie 158 | priority_level: 159 | # 匹配 2160p 分辨率 160 | - resource_pix: "2160p" 161 | # 匹配 2160p、1080p 分辨率 162 | - resource_pix: "2160p,1080p" 163 | # 排除 2160p 分辨率 164 | - resource_pix: "!2160p" 165 | ``` 166 | 167 | 一个通用的新版策略如下:排除 DV;优先 WiKi 小组;优先 2160P 168 | 169 | ```yaml 170 | # 第一级为别名,随便写 171 | 电影洗版策略: 172 | # 洗版模式: 173 | # coexist: 共存(就是多版本共存) 174 | # skip: 跳过(就是只要有一个,就不再保存了) 175 | # replace:(就是根据优先级进行洗版) 176 | # max_size:(就是在优先级相同时保留最大的) 177 | # min_size:(就是在优先级相同时保留最小的) 178 | mode: replace 179 | # 匹配媒体类型,movie/tv,去掉这个字段即匹配所有 180 | media_type: movie 181 | # 匹配二级分类策略的分类名,去掉这个字段即匹配所有 182 | #category: 华语电影,动画电影 183 | # 匹配规则优先级,上面的优先级最高 184 | priority_level: 185 | - resource_team: "WiKi" 186 | resource_effect: "!DV" 187 | - resource_pix: "2160p,4k" 188 | resource_type: "BluRay" 189 | resource_effect: "!DV" 190 | - resource_pix: "1080p" 191 | resource_type: "BluRay" 192 | - resource_pix: "2160p,4k" 193 | resource_type: "WEB-DL" 194 | resource_effect: "!DV" 195 | - resource_pix: "1080p" 196 | 197 | 剧集洗版策略: 198 | mode: replace 199 | media_type: tv 200 | priority_level: 201 | - resource_pix: "2160p,4k" 202 | resource_type: "BluRay" 203 | resource_effect: "!DV" 204 | - resource_pix: "1080p" 205 | resource_type: "BluRay" 206 | - resource_pix: "2160p,4k" 207 | resource_type: "WEB-DL" 208 | resource_effect: "!DV" 209 | - resource_pix: "1080p" 210 | 211 | 电影兜底策略: 212 | media_type: movie 213 | mode: coexist 214 | 215 | 剧集兜底策略: 216 | media_type: tv 217 | mode: skip 218 | ``` 219 | -------------------------------------------------------------------------------- /docs/cover.md: -------------------------------------------------------------------------------- 1 | # 媒体库封面自动生成插件 2 | 3 | > [!TIP] 4 | > 自动获取媒体库里的海报,然后使用这些海报生成精美的媒体库封面 5 | 6 | ## 高级配置 7 | 8 | > [!WARNING] 9 | > 必须是 JSON 格式的语法 10 | 11 | 高级配置示例(全部都是非必填) 12 | 13 | ```json 14 | { 15 | "ch_font_name": "霞鹜文楷.ttf", 16 | "ch_font_size": 180, 17 | "eng_font_name": "霞鹜文楷.ttf", 18 | "eng_font_size": 80, 19 | "background_color_rgb_left": "139,0,0", 20 | "background_color_rgb_right": "255,69,0", 21 | "poster_position": "315426987", 22 | "template_mapping": [ 23 | { 24 | "library_name": "动漫-大电影", 25 | "library_ch_name": "动漫电影", 26 | "library_eng_name": "ANIME MOVIE" 27 | }, 28 | { 29 | "library_name": "动漫-剧集", 30 | "library_ch_name": "动漫剧集", 31 | "library_eng_name": "ANIME TV", 32 | "background_color_rgb_left": "139,0,0", 33 | "background_color_rgb_right": "255,69,0" 34 | } 35 | ] 36 | } 37 | ``` 38 | 39 | | 环境变量 | 示例值 | 40 | | -------------------------- | -------------------------------------------------------------------------- | 41 | | ch_font_name | 自定义中文字体文件名(使用此配置时必须把字体文件放到你的 config 文件夹下) | 42 | | ch_font_size | 自定义中文字体大小 | 43 | | eng_font_name | 自定义英文字体文件名(使用此配置时必须把字体文件放到你的 config 文件夹下) | 44 | | eng_font_size | 自定义英文字体大小 | 45 | | background_color_rgb_left | 封面背景颜色左边(RGB 格式) | 46 | | background_color_rgb_right | 封面背景颜色右边(RGB 格式) | 47 | | poster_position | 海报在封面的位置,每三个一组,分别对应每列从上到下的位置取第几个封面 | 48 | | color_block_rgb | 色块颜色(RGB 格式) | 49 | | ch_position | 中文位置(默认 73.32,427.34) | 50 | | eng_position | 英文位置(默认 124.68,635.55) | 51 | | color_block_position | 色块位置(默认 84.38,620.06) | 52 | | library_name | 实际的媒体库名称 | 53 | | library_ch_name | 封面中文名 | 54 | | library_eng_name | 封面英文名 | 55 | 56 | 注意:`template_mapping` 里面的 `background_color_rgb_left` 优先级大于最外层的 `background_color_rgb_left` 57 | 58 | [在线取色器](https://www.jyshare.com/front-end/6210) 59 | 60 | [JSON 格式在线检测](https://www.jyshare.com/front-end/53/) 61 | -------------------------------------------------------------------------------- /docs/extend.md: -------------------------------------------------------------------------------- 1 | # 教程扩展 2 | 3 | ## CMSHelp 4 | 5 | CMSHelp:@Alan_Hu2004 6 | 7 | https://github.com/guyue2005/CMSHelp/wiki 8 | 9 | 10 | ## 群晖 11 | 12 | 群晖NAS安装配置CMS、EMBY及播放教程:@Black_Plum 13 | 14 | https://docs.qq.com/doc/DSnhac2xGc0xObmFT 15 | 16 | ## OpenWrt 17 | 18 | OpenWrt安装配置CMS、EMBY及播放教程:@Black_Plum 19 | 20 | https://docs.qq.com/doc/DSk9KckJHZUdmTFBQ 21 | 22 | ## 绿联 23 | 24 | 绿联部署CMS教程:@keba3366 25 | 26 | https://hi.keba.host/archives/ugreen-cloud-media-sync -------------------------------------------------------------------------------- /docs/full-sync.md: -------------------------------------------------------------------------------- 1 | # 全量同步 2 | 3 | 全量同步就是将一个文件夹里的视频生成 strm,将图片、字幕、NFO 等下载到本地 4 | 5 | 这个文件夹里的视频理论上应该是已经整理好的、后续变动较少的 6 | -------------------------------------------------------------------------------- /docs/save-download.md: -------------------------------------------------------------------------------- 1 | # 还没写 -------------------------------------------------------------------------------- /docs/share-sync.md: -------------------------------------------------------------------------------- 1 | # 115分享同步 2 | 3 | > [!IMPORTANT] 4 | > 用于将115分享里的文件同步到本地生成strm,使用分享直连时会自动保存文件到我的接收一份(分享密码修改时直连就会失效,建议使用自己的分享) 5 | 6 | ### 分享码 7 | 8 | ![分享码](/docs-img/share-115.png) 9 | 10 | ### 分享文件夹cid 11 | 12 | 用于你不想同步所有的场景,比如你只想同步分享里的某个文件夹,可以输入该文件夹的cid;默认为0,表示同步该分享里的所有文件 13 | 14 | ![分享文件夹cid](/docs-img/share-cid.png) 15 | -------------------------------------------------------------------------------- /docs/tmdb.md: -------------------------------------------------------------------------------- 1 | ## tmdb代理 2 | 3 | 创建一个tmdb代理,直接替代api.themoviedb.org和image.tmdb.org去访问tmdb,从而达到不需要开代理就可以流畅刮削emby的目的。 4 | 5 | 6 | ## 简介 7 | [tmdb-proxy](https://github.com/imaliang/tmdb-proxy) 8 | 9 | 这是一个利用Vercel代理tmdb接口的仓库。 10 | 11 | 完全免费,但是每月有100GB流量限制,自用的话是完全够用的。 12 | 13 | 14 | ## 部署 15 | [![Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/imaliang/tmdb-proxy) 16 | 17 | 18 | ## 使用方法 19 | 20 | 1. 部署。部署有两种方法: 21 | + 一是直接点击上方按钮一键部署。 22 | + 二是先fork本项目,再登录 [Vercel](https://vercel.com/) 选择自己的仓库新建。 23 | 24 | 25 | 2. 绑定自己的域名(必须,因为自带的域名vercel.app在国内基本不可用,这个域名最好托管在国内dns上) 26 | + 如果你没有域名,可以去 [腾讯云活动域名](https://curl.qcloud.com/ScJY3Hev) 注册一个,新用户1元1年。 27 | 28 | 3. 你自己绑定的域名就是tmdb的代理域名,会代理 api.themoviedb.org 和 image.tmdb.org 29 | -------------------------------------------------------------------------------- /docs/vps.md: -------------------------------------------------------------------------------- 1 | # VPS推荐 2 | 3 | > [!IMPORTANT] 4 | > 在不是被坑的情况下,一分价钱一分货 5 | 6 | ## 国内外VPS对比 7 | 8 | | 项目 | 国内 | 国外 | 9 | | --- | --- | --- | 10 | | 带宽 | 3mbps (较低) | 1000mbps (较高) | 11 | | 延迟 | 10ms (较低) | 150ms+ (较高) | 12 | | 域名访问 | 需要备案 | 不需要备案 | 13 | | 网络环境 | 部署一些国外服务时需要自己加代理 | 访问国外服务时不需要加代理,但是有的国内产品国外访问不友好 | 14 | 15 | 16 | ## 国内VPS推荐 17 | 18 | **直接去国内大厂找个合适又便宜的就行** 19 | 20 | + [腾讯云](https://curl.qcloud.com/ScJY3Hev) 21 | + [阿里云](https://www.aliyun.com/product/ecs) 22 | + [华为云](https://www.huaweicloud.com/product/ecs) 23 | 24 | ## 国外VPS推荐 25 | 26 | ### NETCUP 27 | 28 | **为什么推荐NETCUP** 29 | 30 | - 性价比高 31 | - 支持14天内退款 32 | - 国外大厂,有保障,相对稳定 33 | 34 | ### 官网在售的隐藏套餐 35 | 36 | - [月0.84€ 1核1G内存30G硬盘无限流量 1G带宽](https://www.netcup.com/de/server/vps/vps-piko-g11s-12m?ref=267816) 37 | - [月1.68€ 2核2G内存60G硬盘无限流量 1G带宽](https://www.netcup.com/de/server/vps/vps-nano-g11s-6m?ref=267816) 38 | - [月3.36€ 4核4G内存128G硬盘无限流量 1G带宽](https://www.netcup.com/de/server/vps/vps-mikro-g11s-3m?ref=267816) 39 | - [月5.26€ 6核8G内存256G硬盘无限流量 AMD 2.5G带宽](https://www.netcup.com/de/server/arm-server/vps-1000-arm-g11-mnz?ref=267816) 40 | - [月8.24€ 4核8G内存256G硬盘无限流量 2.5G带宽](https://www.netcup.com/de/server/root-server/rs-1000-g11-12m?ref=267816) 41 | 42 | > [!TIP] 43 | > 1. 注册时,最好关闭代理 44 | > 2. 资料填写时,邮编和地址最好和本地IP匹配 45 | > 3. 地区选择CN-China 46 | > 4. 电子邮箱写自己的 47 | > 5. 电话号码写中国的+86 48 | > 6. 下单完成后你会收到一些邮件,其中有个邮件是激活验证邮件,一定要选择支付验证 49 | > 7. 支付最好用paypal,退款时可以发工单让其原路返回 50 | 51 | ### 代金劵 52 | 53 | 5€代金劵 54 | 55 | ```bash 56 | 36nc17405563759 57 | 36nc17405563758 58 | 36nc17405563757 59 | 36nc17405563756 60 | 36nc17405563755 61 | 36nc17405563754 62 | 36nc17405563753 63 | 36nc17405563752 64 | 36nc17405563751 65 | 36nc17405563750 66 | ``` -------------------------------------------------------------------------------- /docsearch.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_name": "cms-docs", 3 | "start_urls": [ 4 | { 5 | "url": "https://wiki.cmscc.cc", 6 | "selectors_key": "" 7 | } 8 | ], 9 | "stop_urls": [], 10 | "selectors": { 11 | "default": { 12 | "lvl0": { 13 | "selector": "", 14 | "default_value": "我的文档" 15 | }, 16 | "lvl1": ".content h1", 17 | "lvl2": ".content h2", 18 | "lvl3": ".content h3", 19 | "lvl4": ".content h4", 20 | "lvl5": ".content h5", 21 | "lvl6": ".content h6", 22 | "text": ".content p, .content li", 23 | "lang": { 24 | "selector": "/html/@lang", 25 | "type": "xpath", 26 | "global": true 27 | } 28 | } 29 | }, 30 | "custom_settings": { 31 | "attributesForFaceting": [ 32 | "lang" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /donate/index.md: -------------------------------------------------------------------------------- 1 | # 捐赠 CMS 2 | 3 | > [!WARNING] 4 | > 本项目仅用于个人测试使用,请部署在局域网内使用,禁止放在公网使用,禁止任何商业行为。 5 | 6 | > [!TIP] 7 | > CMS 目前已关闭免费版使用,PRO 需捐赠 88 元开启 8 | 9 | ## 扫码支付 10 | 11 | 扫码支付捐赠`88`元,并备注你的邮箱 12 | 13 | | 支付宝 | 微信 | 14 | | :------------------------------: | :---------------------------: | 15 | | ![支付宝](/zfb.jpg){width="300"} | ![微信](/wx.jpg){width="330"} | 16 | 17 | ## 获取捐赠码 18 | 19 | - 加入[TG 群](https://t.me/cloud_media_sync) 私聊机器人 [cms_donate_code](https://t.me/cms_ticket_bot) 凭支付订单截图和邮箱获取捐赠码 20 | 21 | - 也可以等待我将捐赠码发送至您的邮箱 22 | 23 | ## 使用捐赠码 24 | 25 | 在 docker 启动变量里增加 `DONATE_CODE=CMS_XXX_XXX` ,然后重启 CMS 容器即可 26 | 27 | > [!CAUTION] 28 | > 捐赠码只允许本人使用,多个`IP`、`设备信息`激活会封禁捐赠码! 29 | 30 | ## 远程部署安装 31 | 32 | > [!INFO] 33 | > 如果你是小白,并且不想自己折腾部署、配置等;可以捐赠包安装版(直接给你装好配置好),包安装需要捐赠`188`元 34 | -------------------------------------------------------------------------------- /faq/index.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | > [!IMPORTANT] 4 | > 首先确定自己是不是最新版,不是就升级到最新版,然后看看问题是否已解决 5 | 6 | ## 启动失败 7 | 8 | #### this license key is expired 9 | 10 | ```bash 11 | RuntimeError: this license key is expired (1:11086) 12 | ``` 13 | 14 | **版本过期了,升级到最新版即可** 15 | 16 | #### HTTP 请求失败: 连接错误 17 | 18 | ```bash 19 | INFO: 2025-03-03 22:26:05,135 - main : 54 ➜ 开始校验捐赠码.... 20 | ERROR: 2025-03-03 22:26:05,142 - mhttp : 156 ➜ HTTP请求失败: 连接错误 21 | ERROR: 2025-03-03 22:26:05,146 - mhttp : 156 ➜ HTTP请求失败: 连接错误 22 | INFO: 2025-03-03 22:26:05,147 - main : 56 ➜ 校验捐赠码结束.... 23 | ``` 24 | 25 | **cms 的授权服务器在国外,国内网络有时候连不上,可以给 docker 加上代理启动,如下所示加上这些变量后启动** 26 | 27 | ```yaml 28 | - HTTP_PROXY=http://192.168.2.118:7890 29 | - HTTPS_PROXY=http://192.168.2.118:7890 30 | - NO_PROXY=localhost,127.0.0.0/8,10.0.0.0/8,172.0.0.0/8,192.168.0.0/16 31 | ``` 32 | 33 | ## 起播慢 34 | 35 | 正常起播速度在 1-3s 内 36 | 37 | 如何保证起播速度最快 38 | 39 | - 使用的是 9096 端口 40 | - emby 已经提取了视频的媒体信息 41 | - 换个第三方播放器试试(尽量不用 infuse) 42 | - 局域网更快 43 | 44 | ## 9096 打不开 45 | 46 | **你填的 emby 地址对于 cms 是不通的,换个能用的** 47 | 48 | ## 快速同步失败 49 | 50 | #### 目录不存在或已转移 51 | 52 | 去网页版 115 里校验下空间,然后重试 53 | 54 | ## 自动整理相关 55 | 56 | #### 父目录不存在 57 | 58 | 二级分类策略配置有误(比如缩进要一致、cid 要确保是对的) 59 | 60 | #### 视频整理到了已存在 61 | 62 | 自动整理必须满足以下条件: 63 | 64 | 1. 待整理文件夹必须在你全量同步的文件夹 **外面** 65 | 2. 你去同步记录里搜这个待整理文件夹的名字必须 **搜不到** 66 | 3. 二级分类策略填的 cid 必须在你全量同步的文件夹 **里面** 67 | 68 | [自动整理详细介绍](/docs/auto-organize) 69 | 70 | #### 示例 1: 71 | 72 | ![自动整理报错](/faq/auto-error.jpg) 73 | 74 | **你的重命名规则错了,要用 < > 包裹变量,请参考 [重命名规则](https://github.com/guyue2005/CMSHelp/wiki/5.%E4%B8%8A%E4%BC%A0%E4%B8%8E%E6%95%B4%E7%90%86#%E9%87%8D%E5%91%BD%E5%90%8D%E8%A7%84%E5%88%99)** 75 | 76 | 改完之后浏览器访问下下面的地址,清下缓存 77 | 78 | ```bash 79 | http://127.0.0.1:9527/api/config/clear?token=cloud_media_sync&table=rename_log 80 | ``` 81 | 82 | ## 字幕未整理 83 | 84 | #### 原因 1: 85 | 86 | **字幕文件命名错误,类似于下面的字幕命名才会被整理** 87 | 88 | ```bash 89 | 钢铁侠.2008.2160p.UHD.BluRay.x265.10bit.HDR.TrueHD.7.1-TnT.mkv 90 | 钢铁侠.2008.2160p.UHD.BluRay.x265.10bit.HDR.TrueHD.7.1-TnT.srt 91 | 钢铁侠.2008.2160p.UHD.BluRay.x265.10bit.HDR.TrueHD.7.1-TnT.chs.srt 92 | 钢铁侠.2008.2160p.UHD.BluRay.x265.10bit.HDR.TrueHD.7.1-TnT-chs.srt 93 | ``` 94 | 95 | #### 原因 2: 96 | 97 | **命名规则有问题** 你的重命名规则错了,可能为空的变量要用 < > 包裹 98 | 99 | ```js 100 | {title}.{year}.{resource_pix} 错误 101 | {title}.{year}<.{resource_pix}> 正确 102 | ``` 103 | 104 | 请参考 [重命名规则](https://github.com/guyue2005/CMSHelp/wiki/5.%E4%B8%8A%E4%BC%A0%E4%B8%8E%E6%95%B4%E7%90%86#%E9%87%8D%E5%91%BD%E5%90%8D%E8%A7%84%E5%88%99) 105 | 106 | ## 非监控文件夹 107 | 108 | **需要同步的文件或文件夹的`父文件夹`没有同步过(去同步记录里搜下父文件夹,没有就是没同步过)** 109 | 110 | `监控文件夹`:同步过的文件夹(在同步记录里可以搜到) 111 | 112 | `非监控文件夹`:同步过的文件夹(在同步记录里搜不到) 113 | 114 | ## 获取 115 登录二维码失败 115 | 116 | 原因可能有: 117 | 118 | 1. 香港 IP 119 | 2. 账号被风控了 120 | 3. 短时间内获取次数太多 121 | 4. 你断网了 122 | 123 | ## 错误码 124 | 125 | #### 403 126 | 127 | **115 禁止香港 IP 访问,请更换 VPS** 128 | 129 | #### 405 130 | 131 | **触发风控了,等待解封(一般第一次触发,换个设备扫码就行;触发了多次时间就不一定了)** 132 | 133 | #### 443 134 | 135 | **代理问题,请检查 http 代理是否正常** 136 | -------------------------------------------------------------------------------- /flow/index.md: -------------------------------------------------------------------------------- 1 | # 功能图解 2 | 3 | ## 媒体同步 4 | 5 | ```mermaid 6 | sequenceDiagram 7 | actor You 8 | participant A as CMS 9 | participant D as EMBY 10 | participant C as 企业微信/TG机器人 11 | You ->>A: 全量同步 12 | A ->> A: 生成strm到本地 13 | Note right of You:增量同步定时任务 14 | loop 定时任务 15 | A -->> A: 获取115生活事件 16 | A -->> A: 增量同步,生成strm 17 | end 18 | A ->> C: 发送同步结果消息 19 | A ->>+ D: 通知emby刷新入库 20 | D ->> D: 入库新电影 21 | D -->>- A: 通知入库成功 22 | A ->> C: 发送入库成功消息 23 | 24 | ``` 25 | 26 | ## 自动整理 27 | 28 | ```mermaid 29 | graph TD 30 | A(待整理文件夹) --> B{sha1已存在?} 31 | B -->|是| C(已存在文件夹) 32 | C --> Z[结束] 33 | B -->|否| D[开始识别] 34 | D -->|电视剧| G{emby中集已存在?} 35 | G -->|否| J[执行二级分类策略] 36 | G -->|是| C 37 | J --> M[整理成功] 38 | M --> Z 39 | D -->|电影| J 40 | D -->|识别失败| K(冗余文件夹) 41 | K --> Z 42 | style A fill:#e6ffd0,stroke:#a3d46b 43 | style C fill:#e6ffd0,stroke:#a3d46b 44 | style K fill:#e6ffd0,stroke:#a3d46b 45 | ``` 46 | 47 | ## 转存下载 48 | 49 | ```mermaid 50 | sequenceDiagram 51 | actor You 52 | participant A as 企业微信/TG机器人 53 | participant B as CMS 54 | participant C as EMBY 55 | You ->> A: 发送分享、磁力、ed2k 56 | A ->>+ B: 处理消息 57 | B ->> B: 保存分享到阿里 58 | B ->> B: 秒传到115 59 | B ->> B: 保存分享到115 60 | B -->>- A: 发送处理结果 61 | loop 整理同步 62 | B -->>B: 自动整理 63 | B -->>B: 生成strm 64 | B -->>A: 发送整理同步结果 65 | end 66 | 67 | B -->>+C: 通知emby入库 68 | C -->>C: 入库新电影 69 | C -->>-B: 入库通知 70 | B -->>A: 入库通知 71 | ``` 72 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | -------------------------------------------------------------------------------- /install/index.md: -------------------------------------------------------------------------------- 1 | # CMS 快速安装 2 | 3 | > [!WARNING] 4 | > 本项目仅用于个人测试使用,请部署在局域网内使用,禁止放在公网使用,禁止任何商业行为。 5 | 6 | > [!WARNING] 7 | > 假设你已经已安装好 docker 和 docker-compose 8 | 9 | ## 1. 配置启动参数 10 | 11 | 创建`cms`文件夹,并在 cms 文件夹下创建`cms.yml`文件,内容如下 12 | 13 | 选择一个网络模式进行安装,建议使用`bridge`网络模式 14 | 15 | ::: code-group 16 | 17 | ```yaml [bridge网络模式] 18 | services: 19 | cloud-media-sync: 20 | privileged: true 21 | container_name: cloud-media-sync 22 | image: imaliang/cloud-media-sync:latest 23 | restart: always 24 | network_mode: bridge 25 | volumes: 26 | - "./config:/config" 27 | - "./logs:/logs" 28 | - "./cache:/var/cache/nginx/emby" 29 | - "/data/media:/media" 30 | ports: 31 | - "9527:9527" 32 | - "9096:9096" 33 | environment: 34 | - PUID=0 35 | - PGID=0 36 | - UMASK=022 37 | - TZ=Asia/Shanghai 38 | - RUN_ENV=online 39 | - ADMIN_USERNAME=admin 40 | - ADMIN_PASSWORD=admin 41 | - CMS_API_TOKEN=cloud_media_sync 42 | - EMBY_HOST_PORT=http://172.17.0.1:8096 43 | - EMBY_API_KEY=xxx 44 | - DONATE_CODE=CMS_XXX_XXX 45 | ``` 46 | 47 | ```yaml [host网络模式] 48 | services: 49 | cloud-media-sync: 50 | privileged: true 51 | container_name: cloud-media-sync 52 | image: imaliang/cloud-media-sync:latest 53 | restart: always 54 | network_mode: host 55 | volumes: 56 | - "./config:/config" 57 | - "./logs:/logs" 58 | - "./cache:/var/cache/nginx/emby" 59 | - "/data/media:/media" 60 | environment: 61 | - PUID=0 62 | - PGID=0 63 | - UMASK=022 64 | - TZ=Asia/Shanghai 65 | - RUN_ENV=online 66 | - ADMIN_USERNAME=admin 67 | - ADMIN_PASSWORD=admin 68 | - CMS_API_TOKEN=cloud_media_sync 69 | - EMBY_HOST_PORT=http://172.17.0.1:8096 70 | - EMBY_API_KEY=xxx 71 | - DONATE_CODE=CMS_XXX_XXX 72 | ``` 73 | 74 | ::: 75 | 76 | | 环境变量 | 示例值 | 必填 | 描述 | 77 | | -------------------- | ---------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 78 | | `ADMIN_USERNAME` | admin | 是 | 账号 | 79 | | `ADMIN_PASSWORD` | admin | 是 | 密码 | 80 | | `CMS_API_TOKEN` | cloud_media_sync | 否 | cms 的 api token | 81 | | `EMBY_HOST_PORT` | http://172.17.0.1:8096 | 是 | EMBY 地址 | 82 | | `EMBY_API_KEY` | xxxx | 是 | EMBY_API_KEY | 83 | | `IMAGE_CACHE_POLICY` | 3 | 否 | EMBY 图片缓存策略,包括主页、详情页、图片库的原图
0: 不同尺寸设备共用一份缓存,先访问先缓存,空间占用最小但存在小屏先缓存大屏看的图片模糊问题
1: 不同尺寸设备分开缓存,空间占用适中,命中率低下,但契合 emby 的图片缩放处理
2: 不同尺寸设备共用一份缓存,空间占用最大,移除 emby 的缩放参数,直接原图高清显示
3: 关闭 nginx 缓存功能,已缓存文件不做处理 | 84 | | `DONATE_CODE` | CMS_XXX_XXX | 是 | CMS 捐赠码,捐赠后私聊机器人 @cms_ticket_bot 获得 | 85 | 86 | > [!TIP] 87 | > 如果你熟悉`emby2Alist`,可以创建 `config/constant.js` 进行高级配置,小白请忽略。 88 | 89 | ## 2. 启动容器 90 | 91 | 使用 ssh 连接到你的服务器(记得使用 root 用户),并进入到前面创建的 cms 文件夹下,使用以下命令启动 CMS 容器,等待部署完成 92 | 93 | ```sh 94 | docker-compose -f cms.yml up -d 95 | ``` 96 | 97 | 部署成功后,启动日志如下: 98 | 99 | ```sh 100 | INFO: 2025-03-02 21:33:51,091 - main : 85 ➜ cms starting success... 101 | INFO: 2025-03-02 21:33:51,091 - main : 86 ➜ Version: v0.3.5 - PRO 102 | ``` 103 | 104 | ## 3. 熟悉界面 105 | 106 | 启动成功后,访问 http://127.0.0.1:9527 ,使用 默认的账号密码`admin`登录 webui,先别急着配置,从上到下,从左到右,先把页面过一遍,知道都有啥东西。(很重要!很重要!很重要!)界面里的提示都很重要,务必仔细阅读。 107 | 108 | ## 4. 核心配置 109 | 110 | > [!IMPORTANT] 111 | > 开始配置前先去 115APP 里清空下生活事件(就是最近操作记录),不然会有很多无用的同步 112 | 113 | 访问 http://127.0.0.1:9527 进入 `核心配置` -> `115账号` ,选择一个你不使用的设备,使用手机扫码登录 115 114 | 115 | 接着完成 `STRM配置`,需要把 strm 直连域名 改为一个能访问你的 cms 访问地址 116 | 117 | 以下为常用的 strm 直连域名示例 118 | 119 | ```sh 120 | http://172.17.0.1:9527 (如果可用,推荐用这个,这个只有你的cms和emby都是bridge网络模式才可用) 121 | http://192.168.2.158:9527 (局域网IP示例) 122 | https://cms.com (你已经反代了9527端口) 123 | ``` 124 | 125 | ## 5. 全量同步 126 | 127 | 进入全量同步页面,进行配置 128 | 129 | ![全量同步配置示例](/install/full-sync.png) 130 | 131 | > 文件后缀没有时,可以先输入,再选 132 | 133 | 配置完成后点击保存,然后点击全量同步,观察日志,等待全量同步完成。 134 | 135 | > [!NOTE] 136 | > 如果你的媒体库不在一个文件夹里,就执行多次全量同步,一定要第一个文件夹同步完成后再执行下一个。 137 | > 建议先测试一个小库,彻底搞懂怎么玩后再同步大库 138 | 139 | **一个文件夹只需要执行全量同步成功一次即可** 140 | 141 | ## 6. 增量同步 142 | 143 | 全量同步完后,之后关于`你同步的文件夹`里的变动由增量同步完成 144 | 145 | 增量同步依赖 115 生活事件,所以你必须打开 115 的生活事件 146 | 147 | ![115生活事件](/install/lift-1.png){width="400"} 148 | ![115生活事件](/install/lift-2.png){width="400"} 149 | 150 | > 由于 115 的文件重命名无法产生生活事件,所以无法增量同步文件重命名;不过文件重命名后并不影响直连的获取,所以影响不大。 151 | 152 | ## 7. 扫描入库 153 | 154 | 在你的 emby 里配置媒体库,扫描 cms 全量同步生成的 strm 文件,等待 emby 刮削入库完毕 155 | 156 | **之后访问 cms 的`9096`端口,就可以 302 观影 emby 了** 157 | 158 | ## 8. 如何更新到最新版 159 | 160 | 同样使用 ssh 连接到你的服务器,并进入到前面创建的 cms 文件夹下 161 | 162 | 先停止并删除旧的 CMS 容器 163 | 164 | ```sh 165 | docker-compose -f cms.yml down 166 | ``` 167 | 168 | 再运行以下命令来拉取最新的 `imaliang/cloud-media-sync` 镜像 169 | 170 | ```sh 171 | docker pull imaliang/cloud-media-sync:latest 172 | ``` 173 | 174 | 最后重新启动 CMS 容器 175 | 176 | ```sh 177 | docker-compose -f cms.yml up -d 178 | ``` 179 | 180 | ## 9. 完成 181 | 182 | > [!TIP] 183 | > 至此,最简单的玩法已经部署完成,进阶玩法可以参考其它教程。 184 | -------------------------------------------------------------------------------- /note.md: -------------------------------------------------------------------------------- 1 | ## GitHub 风格的警报 2 | 3 | > [!NOTE] 4 | > 强调用户在快速浏览文档时也不应忽略的重要信息。 5 | 6 | > [!TIP] 7 | > 有助于用户更顺利达成目标的建议性信息。 8 | 9 | > [!IMPORTANT] 10 | > 对用户达成目标至关重要的信息。 11 | 12 | > [!WARNING] 13 | > 因为可能存在风险,所以需要用户立即关注的关键内容。 14 | 15 | > [!CAUTION] 16 | > 行为可能带来的负面影响。 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-docs", 3 | "version": "1.0.0", 4 | "description": "CMS使用文档", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vitepress dev --host", 8 | "build": "vitepress build", 9 | "preview": "vitepress preview" 10 | }, 11 | "keywords": [], 12 | "type": "module", 13 | "author": "imaliang", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/node": "^20.6.0", 17 | "@types/remove-markdown": "^0.3.1", 18 | "@waline/client": "^3.1.3", 19 | "fast-glob": "^3.3.1", 20 | "gray-matter": "^4.0.3", 21 | "less": "^4.2.0", 22 | "medium-zoom": "^1.0.8", 23 | "remove-markdown": "^0.5.0", 24 | "vitepress": "^1.6.3", 25 | "vitepress-plugin-mermaid": "^2.0.17", 26 | "vue": "^3.3.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/docs-img/share-115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/docs-img/share-115.png -------------------------------------------------------------------------------- /public/docs-img/share-cid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/docs-img/share-cid.png -------------------------------------------------------------------------------- /public/faq/auto-error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/faq/auto-error.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/favicon.ico -------------------------------------------------------------------------------- /public/install/full-sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/install/full-sync.png -------------------------------------------------------------------------------- /public/install/lift-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/install/lift-1.png -------------------------------------------------------------------------------- /public/install/lift-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/install/lift-2.png -------------------------------------------------------------------------------- /public/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/profile.png -------------------------------------------------------------------------------- /public/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/wx.jpg -------------------------------------------------------------------------------- /public/zfb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/public/zfb.jpg -------------------------------------------------------------------------------- /src/assets/HarmonyOS_Sans_SC_Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/HarmonyOS_Sans_SC_Bold.woff2 -------------------------------------------------------------------------------- /src/assets/HarmonyOS_Sans_SC_Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/HarmonyOS_Sans_SC_Medium.woff2 -------------------------------------------------------------------------------- /src/assets/HarmonyOS_Sans_SC_Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/HarmonyOS_Sans_SC_Regular.woff2 -------------------------------------------------------------------------------- /src/assets/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaliang/cms-docs/23cd53b05333abf24a0bcef3f2f332c930097ae0/src/assets/iconfont.woff2 -------------------------------------------------------------------------------- /src/components/AdItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 76 | 77 | 97 | -------------------------------------------------------------------------------- /src/components/LinkList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 67 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 68 | 69 | 111 | -------------------------------------------------------------------------------- /src/components/PostInfoItem.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /src/components/PostList.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 54 | 55 | 140 | -------------------------------------------------------------------------------- /src/components/PostListLite.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 81 | -------------------------------------------------------------------------------- /src/composables/useAds.ts: -------------------------------------------------------------------------------- 1 | import { useData } from 'vitepress'; 2 | 3 | export const useAds = () => { 4 | const { theme } = useData(); 5 | const ads = theme.value.ads; 6 | const adsense = theme.value.adsense; 7 | 8 | return { 9 | ads, 10 | adsense 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/composables/useGroup.ts: -------------------------------------------------------------------------------- 1 | import type { IPost, IGroup } from '../types'; 2 | 3 | export const useGroup = (posts: IPost[], type: string) => { 4 | const data: IGroup = {}; 5 | posts.forEach((post) => { 6 | if (type === 'archives') { 7 | const year = new Date(post.datetime).getFullYear(); 8 | addToData(data, year, post); 9 | } else if (type === 'category') { 10 | const category = post.category; 11 | addToData(data, category, post); 12 | } else if (type === 'tags') { 13 | const tags = post.tags; 14 | tags && tags.forEach((tag) => addToData(data, tag, post)); 15 | } 16 | }); 17 | 18 | const keys = Object.keys(data).sort((a, b) => { 19 | if (type === 'tags' || type === 'category') { 20 | return a.localeCompare(b); 21 | } 22 | return parseInt(b) - parseInt(a); 23 | }); 24 | 25 | return { keys, data }; 26 | }; 27 | 28 | function addToData(obj: IGroup, key: any, value: IPost) { 29 | if (key) { 30 | if (!obj[key]) { 31 | obj[key] = []; 32 | } 33 | obj[key].push(value); 34 | } 35 | } -------------------------------------------------------------------------------- /src/composables/useOutDir.ts: -------------------------------------------------------------------------------- 1 | import { useData } from 'vitepress'; 2 | 3 | export const useOutDir = () => { 4 | const { theme } = useData(); 5 | let outDir: string = theme.value?.page?.outDir || ''; 6 | if (outDir && !outDir.startsWith('/')) { 7 | outDir = '/' + outDir; 8 | } 9 | return { outDir }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/composables/usePosts.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import matter from 'gray-matter'; 4 | import fg from 'fast-glob'; 5 | import { IPost } from '../types'; 6 | import { generatePages } from '../utils/generatePages'; 7 | import { generateString } from '../utils/generateString'; 8 | import { generateMd } from '../utils/generateMd'; 9 | import { removeMdPro } from '../utils/removeMdPro'; 10 | import { writeMd } from '../utils/writeMd'; 11 | import { formatDate } from '../utils/formatDate'; 12 | 13 | export const usePosts = async ({ 14 | pageSize = 10, 15 | homepage = true, 16 | srcDir = 'posts', 17 | outDir = '', 18 | lang = 'zh', 19 | autoExcerpt = 0, 20 | prev = true, 21 | next = true 22 | }) => { 23 | const rewrites = {}; 24 | try { 25 | const paths = await fg(`${srcDir}/**/*.md`); 26 | let categoryFlag = false; 27 | let tagFlag = false; 28 | const posts = await Promise.all( 29 | paths.map(async (postPath) => { 30 | const { data, excerpt, content } = matter.read(postPath, { 31 | excerpt: true, 32 | excerpt_separator: '' 33 | }); 34 | 35 | data.category && (categoryFlag = true); 36 | data.tags && (tagFlag = true); 37 | 38 | let flag = false; 39 | if (!data.title) { 40 | data.title = path.basename(postPath, path.extname(postPath)); 41 | flag = true; 42 | } 43 | 44 | if (!data.datetime) { 45 | const stats = await fs.stat(postPath); 46 | data.datetime = formatDate(stats.birthtime); 47 | flag = true; 48 | } 49 | 50 | if (!data.permalink) { 51 | data.permalink = `/${srcDir}/${generateString(6)}`; 52 | flag = true; 53 | } 54 | 55 | // avoid null / undefined 56 | if (data.tags) { 57 | data.tags.forEach((tag, index) => { 58 | data.tags[index] = tag + ''; 59 | }); 60 | } 61 | 62 | // writeMarkdown 63 | flag && (await writeMd(postPath, content, data)); 64 | 65 | // rewrites 66 | rewrites[postPath.replace(/[+()]/g, '\\$&')] = `${data.permalink}.md`.slice(1).replace(/[+()]/g, '\\$&'); 67 | 68 | // excerpt 69 | const contents = data.description || removeMdPro(excerpt + '') || removeMdPro(content).slice(0, autoExcerpt); 70 | 71 | return { 72 | ...data, 73 | excerpt: contents 74 | } as IPost; 75 | }) 76 | ); 77 | 78 | // sort posts by datetime 79 | posts.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime()); 80 | 81 | // prev / next 82 | paths.map(async (postPath) => { 83 | const { data, content } = matter.read(postPath); 84 | 85 | // remove pinned posts prev / next 86 | if (data.order) { 87 | if (data.prev || data.next) { 88 | delete data.prev; 89 | delete data.next; 90 | await writeMd(postPath, content, data); 91 | return; 92 | } else { 93 | return; 94 | } 95 | } 96 | 97 | const postIndex = posts.findIndex((post) => post.permalink === data.permalink); 98 | let prevPostIndex = postIndex - 1; 99 | let nextPostIndex = postIndex + 1; 100 | 101 | // Find the previous post that is not pinned 102 | while (prevPostIndex >= 0 && posts[prevPostIndex].order) { 103 | prevPostIndex--; 104 | } 105 | 106 | // Find the next post that is not pinned 107 | while (nextPostIndex < posts.length && posts[nextPostIndex].order) { 108 | nextPostIndex++; 109 | } 110 | 111 | const prevPost = posts[prevPostIndex]; 112 | const nextPost = posts[nextPostIndex]; 113 | 114 | const prevDiff = data?.prev?.text !== prevPost?.title || data?.prev?.link !== prevPost?.permalink; 115 | const nextDiff = data?.next?.text !== nextPost?.title || data?.next?.link !== nextPost?.permalink; 116 | 117 | let flag = true; 118 | if (prev && prevPost && prevDiff && !prevPost.order) { 119 | data.prev = { text: prevPost.title, link: prevPost.permalink }; 120 | flag = true; 121 | } else if (!prev || !prevPost || prevPost.order) { 122 | delete data.prev; 123 | flag = true; 124 | } 125 | 126 | if (next && nextPost && nextDiff) { 127 | data.next = { text: nextPost.title, link: nextPost.permalink }; 128 | flag = true; 129 | } else if (!next || !nextPost) { 130 | delete data.next; 131 | flag = true; 132 | } 133 | 134 | flag && (await writeMd(postPath, content, data)); 135 | }); 136 | 137 | tagFlag && (await generateMd('tags', outDir, lang)); 138 | categoryFlag && (await generateMd('category', outDir, lang)); 139 | 140 | await generatePages(outDir, lang, pageSize, homepage, paths.length); 141 | 142 | return { posts, rewrites }; 143 | } catch (e) { 144 | console.log(e); 145 | await generatePages(); 146 | return { posts: [], rewrites }; 147 | } 148 | }; 149 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, watch, nextTick } from 'vue'; 2 | import { useRoute } from 'vitepress'; 3 | import DefaultTheme from 'vitepress/theme'; 4 | import mediumZoom from 'medium-zoom'; 5 | import Home from './views/HomeView.vue'; 6 | import Group from './views/GroupView.vue'; 7 | import Page from './views/PageView.vue'; 8 | import ThemeLayout from './views/ThemeLayout.vue'; 9 | import './styles/index.less'; 10 | 11 | export default { 12 | extends: DefaultTheme, 13 | Layout: ThemeLayout, 14 | enhanceApp({ app }) { 15 | app.component('Home', Home); 16 | app.component('Group', Group); 17 | app.component('Page', Page); 18 | }, 19 | setup() { 20 | const route = useRoute(); 21 | const initZoom = () => { 22 | mediumZoom('.main img', { background: 'rgba(0,0,0,0.2)' }); 23 | }; 24 | onMounted(() => { 25 | initZoom(); 26 | }); 27 | watch( 28 | () => route.path, 29 | () => nextTick(() => initZoom()) 30 | ); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/styles/font.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'HarmonyOS Sans'; 3 | src: url('../assets/HarmonyOS_Sans_SC_Bold.woff2') format('woff2'); 4 | font-weight: 700; /* Bold */ 5 | font-style: normal; 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'HarmonyOS Sans'; 11 | src: url('../assets/HarmonyOS_Sans_SC_Medium.woff2') format('woff2'); 12 | font-weight: 500; /* Medium */ 13 | font-style: normal; 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: 'HarmonyOS Sans'; 19 | src: url('../assets/HarmonyOS_Sans_SC_Regular.woff2') format('woff2'); 20 | font-weight: 400; /* Regular */ 21 | font-style: normal; 22 | font-display: swap; 23 | } 24 | 25 | @font-face { 26 | font-family: 'iconfont'; /* Project id 4531970 */ 27 | src: url('../assets/iconfont.woff2') format('woff2'), 28 | url('../assets/iconfont.woff') format('woff'), 29 | url('../assets/iconfont.ttf') format('truetype'); 30 | 31 | } 32 | 33 | .iconfont { 34 | font-family: 'iconfont' !important; 35 | font-size: 16px; 36 | font-style: normal; 37 | -webkit-font-smoothing: antialiased; 38 | -moz-osx-font-smoothing: grayscale; 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './post.less'; 2 | @import './font.less'; 3 | :root { 4 | // 白天模式下 代码块背景颜色 5 | --vp-code-block-bg: rgb(30, 30, 30); 6 | --vp-code-block-divider-color: rgb(30, 30, 30); 7 | --vp-code-lang-color: rgba(235, 235, 245, 0.38); 8 | --vp-code-line-number-color: rgba(235, 235, 245, 0.38); 9 | --shiki-light: #d4d4d4; 10 | --vp-font-family-base: 'HarmonyOS Sans', 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 11 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 12 | --font-family-number: 'HarmonyOS Sans', 'Noto Sans', 'Roboto Mono', 'Roboto', 'Microsoft YaHei', 'Arial', 'sans-serif'; 13 | // Nav 透明度 14 | --vp-nav-bg-color: rgba(255, 255, 255, 0.7); 15 | --vp-local-nav-bg-color: rgba(255, 255, 255, 0.7); 16 | --vp-code-tab-active-text-color: #e5e5e5; 17 | --vp-code-tab-hover-text-color: #c2c2c2; 18 | } 19 | 20 | .dark { 21 | // Nav 透明度 22 | --vp-nav-bg-color: rgba(27, 27, 31, 0.7); 23 | --vp-local-nav-bg-color: rgba(27, 27, 31, 0.7); 24 | } 25 | 26 | // 整体布局 27 | .Layout { 28 | min-height: 100dvh !important; 29 | } 30 | 31 | // 滚动条 32 | :root::-webkit-scrollbar, 33 | .VPSidebar::-webkit-scrollbar, 34 | pre::-webkit-scrollbar { 35 | width: 6px; 36 | height: 6px; 37 | 38 | &-thumb { 39 | border-radius: 6px; 40 | background-color: rgba(192, 192, 192, 0.8); 41 | 42 | &:hover { 43 | background-color: rgba(128, 128, 128, 0.8); 44 | } 45 | } 46 | } 47 | 48 | // 图片放大样式 49 | .medium-zoom-overlay { 50 | z-index: 30; 51 | } 52 | 53 | .medium-zoom-image { 54 | border-radius: 10px; 55 | z-index: 31; 56 | } 57 | 58 | // 去除 Nav Footer 边框/过渡 59 | .VPNavBar, 60 | .VPFooter { 61 | transition: none !important; 62 | border: none !important; 63 | } 64 | 65 | // Nav 高斯模糊 / 分割线 66 | .VPLocalNav { 67 | backdrop-filter: blur(10px); 68 | } 69 | 70 | .VPNavBar { 71 | &:not(.has-sidebar) { 72 | backdrop-filter: blur(10px); 73 | } 74 | 75 | &.has-sidebar { 76 | .content-body { 77 | backdrop-filter: blur(10px); 78 | } 79 | } 80 | } 81 | 82 | @media (min-width: 960px) { 83 | .VPNavBar { 84 | &:not(.has-sidebar) { 85 | &:not(.top) { 86 | box-shadow: 0 0.3125rem 0.3125rem -0.3125rem rgba(0, 0, 0, 0.117); 87 | } 88 | 89 | .divider { 90 | display: none; 91 | } 92 | } 93 | } 94 | } 95 | 96 | // Footer 链接样式 97 | .VPFooter a { 98 | text-decoration: none !important; 99 | transition: none !important; 100 | 101 | &:hover { 102 | color: var(--vp-c-brand) !important; 103 | } 104 | } 105 | 106 | // layout: doc 107 | .vp-doc { 108 | // 去除 a 下划线 109 | a { 110 | text-decoration: none; 111 | &:hover { 112 | text-decoration: underline dotted; 113 | text-underline-offset: 0.3rem; 114 | } 115 | } 116 | 117 | img { 118 | display: block; 119 | margin: 0 auto; 120 | border-radius: 10px; 121 | box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.2); 122 | max-width: 75%; 123 | } 124 | 125 | p { 126 | // margin: 12px 0; 127 | } 128 | 129 | li p { 130 | margin: 8px 0; 131 | } 132 | 133 | // 标题样式 134 | h2 { 135 | margin-top: 20px; 136 | padding: 0 0 14px; 137 | border: none; 138 | border-bottom: 1px solid var(--vp-c-divider); 139 | } 140 | 141 | h3 { 142 | // font-size: 18px; 143 | margin-top: 16px; 144 | } 145 | 146 | // 标题前 # 的位置 147 | h1, 148 | h2, 149 | h3 { 150 | font-weight: 700; 151 | .header-anchor { 152 | top: auto; 153 | } 154 | 155 | a { 156 | &:not(.header-anchor) { 157 | font-weight: 700; 158 | color: var(--vp-c-text-1); 159 | 160 | &:hover { 161 | color: var(--vp-c-brand-2); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/styles/page.less: -------------------------------------------------------------------------------- 1 | .ZCContent { 2 | max-width: 60rem; 3 | margin: 0 auto; 4 | } 5 | 6 | @media (max-width: 768px) { 7 | .ZCContainer { 8 | padding: 0 24px; 9 | } 10 | } 11 | 12 | @media (min-width: 768px) and (max-width: 1030px) { 13 | .ZCContainer { 14 | padding: 0 32px; 15 | } 16 | } -------------------------------------------------------------------------------- /src/styles/post.less: -------------------------------------------------------------------------------- 1 | .VPDoc { 2 | // 无 sidebar 有 aside 3 | &:not(.has-sidebar) { 4 | &.has-aside, 5 | &:not(.has-aside) { 6 | .container { 7 | max-width: 60rem; 8 | 9 | .aside { 10 | position: fixed; 11 | top: 0; 12 | left: 50%; 13 | transform: translateX(32rem); 14 | z-index: 15; 15 | } 16 | 17 | & > .content { 18 | max-width: 100%; 19 | padding: 0; 20 | 21 | .content-container { 22 | max-width: 100%; 23 | } 24 | } 25 | } 26 | 27 | .VPDocFooter { 28 | margin: 32px 0 16px; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'vitepress/theme'; 2 | import { 3 | WalineLocale, 4 | WalineEmojiInfo, 5 | WalineEmojiPresets, 6 | WalineCommentSorting, 7 | WalineImageUploader, 8 | WalineHighlighter, 9 | WalineTeXRenderer, 10 | WalineSearchOptions 11 | } from '@waline/client'; 12 | 13 | export interface IPost { 14 | title: string; 15 | datetime: string; 16 | permalink: string; 17 | order: number; 18 | pinned: boolean; 19 | tags?: string[]; 20 | category?: string; 21 | excerpt: string; 22 | } 23 | 24 | export interface IPage { 25 | max?: number; 26 | pinned?: string; 27 | outDir?: string; 28 | } 29 | 30 | export interface ICommnet { 31 | serverURL?: string; 32 | lang?: string; 33 | locale?: WalineLocale; 34 | emoji?: (WalineEmojiInfo | WalineEmojiPresets)[] | false; 35 | commentSorting?: WalineCommentSorting; 36 | meta?: string[]; 37 | requiredMeta?: string[]; 38 | login?: string; 39 | wordLimit?: number | [number, number]; 40 | pageSize?: number; 41 | imageUploader?: WalineImageUploader | false; 42 | highlighter?: WalineHighlighter | false; 43 | texRenderer?: WalineTeXRenderer | false; 44 | search?: WalineSearchOptions | false; 45 | copyright?: boolean; 46 | recaptchaV3Key?: string; 47 | turnstileKey?: string; 48 | reaction?: boolean | string[]; 49 | } 50 | 51 | export interface IAd { 52 | title: string; 53 | img: string; 54 | link?: string; 55 | } 56 | 57 | export interface IAds { 58 | sidebarNavBefore?: (IAd | IAd[])[]; 59 | sidebarNavAfter?: (IAd | IAd[])[]; 60 | asideOutlineBefore?: (IAd | IAd[])[]; 61 | asideOutlineAfter?: (IAd | IAd[])[]; 62 | docBefore?: (IAd | IAd[])[]; 63 | docAfter?: (IAd | IAd[])[]; 64 | } 65 | 66 | export interface IAdsense { 67 | client?: string; 68 | sidebarNavBefore?: string; 69 | sidebarNavAfter?: string; 70 | asideOutlineBefore?: string; 71 | asideOutlineAfter?: string; 72 | docBefore?: string; 73 | docAfter?: string; 74 | } 75 | 76 | export interface IGroup { 77 | [key: string]: IPost[]; 78 | } 79 | 80 | export interface ThemeConfig extends DefaultTheme.Config { 81 | posts?: IPost[]; 82 | page?: IPage; 83 | comment?: ICommnet; 84 | ads?: IAds; 85 | adsense?: IAdsense; 86 | } 87 | -------------------------------------------------------------------------------- /src/utils/fileExists.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | 3 | export const fileExists = async (filePath: string) => { 4 | try { 5 | await fs.access(filePath, fs.constants.F_OK); 6 | return true; 7 | } catch { 8 | return false; 9 | } 10 | }; -------------------------------------------------------------------------------- /src/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (date: string | Date) => { 2 | if (typeof date === 'string') { 3 | date = new Date(date); 4 | } 5 | 6 | let year = date.getFullYear(); 7 | let month = String(date.getMonth() + 1).padStart(2, '0'); 8 | let day = String(date.getDate()).padStart(2, '0'); 9 | let hours = String(date.getHours()).padStart(2, '0'); 10 | let minutes = String(date.getMinutes()).padStart(2, '0'); 11 | let seconds = String(date.getSeconds()).padStart(2, '0'); 12 | 13 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/generateMd.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import { fileExists } from './fileExists'; 4 | 5 | export const generateMd = async (type: string, outDir: string, lang: string) => { 6 | const filePath = path.resolve(outDir, `${type}.md`); 7 | if (await fileExists(filePath)) return; 8 | 9 | const titles = { 10 | archives: { zh: '归档', en: 'Archives' }, 11 | category: { zh: '分类', en: 'Category' }, 12 | tags: { zh: '标签', en: 'Tags' } 13 | }; 14 | 15 | const page = ` 16 | --- 17 | title: ${titles[type][lang]} 18 | layout: page 19 | --- 20 | 21 | 22 | `.trim(); 23 | await fs.writeFile(filePath, page); 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/generatePages.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import { fileExists } from './fileExists'; 4 | 5 | export const generatePages = async ( 6 | outDir?: string, 7 | lang?: string, 8 | pageSize?: number, 9 | homepage?: boolean, 10 | total?: number 11 | ) => { 12 | const indexPath = path.resolve(outDir, 'index.md'); 13 | 14 | const indexExist = await fileExists(indexPath); 15 | const pageTotal = total > 0 ? Math.ceil(total / pageSize) : 0; 16 | 17 | for (let i = 1; i <= pageTotal; i++) { 18 | const title = i === 1 && homepage ? '' : lang === 'zh' ? `\ntitle: 第${i}页` : `\ntitle: Page ${i}`; 19 | const page = ` 20 | ---${title} 21 | layout: page 22 | --- 23 | 24 | 25 | `.trim(); 26 | const pagePath = i === 1 && homepage ? indexPath : path.resolve(outDir, `page-${i}.md`); 27 | await fs.writeFile(pagePath, page); 28 | } 29 | 30 | if ((total === 0 || !homepage) && !indexExist) { 31 | const page = ` 32 | --- 33 | layout: page 34 | --- 35 | 36 | `.trim(); 37 | await fs.writeFile(indexPath, page); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/generateString.ts: -------------------------------------------------------------------------------- 1 | export const generateString = (length: number) => { 2 | const charset = '0123456789abcdef'; 3 | let randomCode = ''; 4 | 5 | for (let i = 0; i < length; i++) { 6 | const randomIndex = Math.floor(Math.random() * charset.length); 7 | randomCode += charset[randomIndex]; 8 | } 9 | 10 | return randomCode; 11 | }; -------------------------------------------------------------------------------- /src/utils/removeMdPro.ts: -------------------------------------------------------------------------------- 1 | import removeMd from 'remove-markdown'; 2 | 3 | export const removeMdPro = (str: string) => { 4 | return removeMd(str.replace(/```.*?```/gs, '').replace(/^#+\s.*$/gm, '')) // 移除代码块、标题 5 | .trim() // 移除空格 6 | .split(/\r\n|\n|\r/) 7 | .join(' ') // 移除换行 8 | .replace(/\s{2,}/g, ' ') 9 | .replace(/:::.*?:::/gs, '') // 移除 ::: 10 | .trim(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/writeMd.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import matter from 'gray-matter'; 3 | 4 | export const writeMd = async (path: string, content: string, data: { [key: string]: any }) => { 5 | const matters = ['title', 'datetime', 'permalink', 'outline', 'order', 'pinned', 'description', 'category', 'tags', 'prev', 'next']; 6 | const newMarkdown = matter.stringify(content, data, { 7 | // @ts-ignore 8 | sortKeys: (a: string, b: string) => matters.indexOf(a) - matters.indexOf(b) 9 | }); 10 | await fs.writeFile(path, newMarkdown, 'utf8'); 11 | }; 12 | -------------------------------------------------------------------------------- /src/views/GroupView.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 75 | 76 | 94 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 57 | 58 | 114 | -------------------------------------------------------------------------------- /src/views/PageView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /src/views/ThemeLayout.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 65 | -------------------------------------------------------------------------------- /tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 标签 3 | layout: page 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["es2021", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "es2022" /* Specify what module code is generated. */, 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["**/*.ts", "**/*.d.ts", "**/*.vue"] 110 | } 111 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true 3 | } --------------------------------------------------------------------------------