笔记 - 搭建 ExactLyrics 网站

TOC

1. 整体构建思路

前因

网站提供校对后的歌词文件并提供下载。

有这个想法是因为现在正在做youtube上的歌曲合集,但注意到youtube的盈利里好像明确写明了这种类型的频道是不能盈利的,所以想着,那就在这个基础上做一个网站,利用网站来尽可能实现广告盈利(当然需要后期通过Adsense审核才行)。

计划提供的功能

  1. 网站多语言:暂时计划包括英语,简体中文和繁体中文,后期可以考虑日语,韩语等其他语种。
  2. 歌词文件提供多语言,多格式下载(算是一种特别的功能)。比如一首中文歌曲,可以提供简体中文,繁体中文,英语(如果有对应的翻译版本)等;格式可以支持txt, lrc, srt等。
  • 语言的翻译不是问题,简繁转换很好处理(OpenCC),对应的其他语言有了 Deepseek 和 GPT 也可以快速生成。

  • 格式的转换也可以通过程序生成,计划还是在本地生成好每首歌的这些文件

    • zh-hans.srt
    • zh-hans.lrc
    • zh-hans.txt
    • zh-hant.srt
    • zh-hant.lrc
    • zh-hant.txt
  1. 提供按照歌手,作曲,作词三类的合集分类功能;
  2. 如果有对应的youtube视频,那么就放上对应的视频;
  3. 歌曲封面能从youtube上找到的话,就直接链接到该图片;
  4. 网站外观,参考smashingmagazine

实现功能的步骤

  1. 网站多语言不是问题,Hugo提供的i18n可以很好的支持,并且方便后期扩展其他语言;

  2. 歌词的语言转换利用已经安装到本地的OpenCC也可以快速实现简繁转换,同时需要在data文件夹下准备好相应的数据记录,暂时的数据结构如下(/data/songs.yaml)

    songs:
     HpOnhRDfQ7:
       title: 星月落
       number:
       - 1
       - 1
       artists:
       - 浮生梦
       lyricists:
       - 萧燃
       composers:
       - 萧燃
       originalArtist: null
       originalLang: zh-hans
       cover: https://lh3.googleusercontent.com/Fpw8v8z6IrYgf205vR8yToF98icxF0lB2xMnXV0slHp1zTR1_S2ZWH-zTaY2ts5A_1_DMOBg-DB-DvsGNg
       youtubeVideo: a-vaqsEpATo
       lyrics:
         txt:
         - zh-hans
         - zh-hant
         lrc:
         - zh-hans
         - zh-hant
         srt:
         - zh-hans
         - zh-hant
     Tjrf2MTkRO:
       title: 谪仙
       number:
       - 1
       - 2
       artists:
       - 叶里
       lyricists:
       - 王莹
       composers:
       - 王中易
       originalArtist: null
       originalLang: zh-hans
       cover: https://lh3.googleusercontent.com/UFEI94VPLJ2KBSIIMmNMbcWlxcrG4rKmPV3c0T6NBXC0opVe1TXr2cjQqXJOscFJcPVh4UrCreK7_BdB_w
       youtubeVideo: Y1ri361Hx1Q
       lyrics:
         txt:
         - zh-hans
         - zh-hant
         lrc:
         - zh-hans
         - zh-hant
         srt:
         - zh-hans
         - zh-hant
    
  3. 因为存在大量的歌曲,涉及不同的歌曲名称,歌手,作词,作曲等,所以需要对其进行编码,这样避免url中重复,这点使用Sqids来本地实现,不过需要做好统计,避免出现问题,考虑到如何避免这个问题,计划这样:

    • 使用数组提供数据给 Sqids,eg: [type, number];
    • 歌曲对应 type = 1, number = 收录的第几首歌曲, 这样如果是第一首收录的歌曲,就提供 [1, 1]给 Sqids,第二首就是[1, 2]
    • 歌手对应 type = 2, number = 收录的第几个歌手, 这样如果是第一个收录的歌手,就提供 [2, 1]给 Sqids,第二个人就是[2, 2]
    • 作词对应 type = 3, number = 收录的第几个作词者, 这样如果是第一个收录的作词者,就提供 [3, 1]给 Sqids,第二个人就是[3, 2]
    • 作曲对应 type = 4, number = 收录的第几个作曲者, 这样如果是第一个收录的作曲者,就提供 [4, 1]给 Sqids,第二个人就是[4, 2]
    • 原唱对应 type = 2, number = 收录的第几个歌手, 这一步和歌手保持一致,因为原唱也是歌手,所以如果出现翻唱的情况,那么歌手的统计应该至少会增加2。
    • /data/songs.yaml这个文件中,歌手,作词,作曲,原唱这4个都按照数组提供,因为存在一首歌曲有多个歌手(作词,作曲,原唱)的情况,所以用数组形式有利于灵活添加。

update 2025-06-29: 实际上只确定了歌曲对应的id,其他分类做不到,因为使用的hugo内置的taxonomy,它会自动使用对应的对象,所以不能对歌手,作词,作曲进行id应用,不过这从某方面也是一件好事,降低了工作量,不用再人为进行id分类。

  1. 歌手,作曲,作词的分类功能,Hugo这方面的Taxonomies 可以很好的处理。
  2. 视频和封面图就是youtube上有就提供,没找到就不提供。封面图可以按照youtube的模式提供一个备选的没找到的图片。

2. 构建过程

1. 新建网站和主题

xdl@MacBook-Air ~ % cd /Users/xdl/Documents/github
xdl@MacBook-Air ~/Documents/github % hugo version 
hugo v0.147.7+extended+withdeploy darwin/arm64 BuildDate=2025-05-31T12:41:12Z VendorInfo=brew
xdl@MacBook-Air ~/Documents/github % hugo new site exactlyrics --format yaml
Congratulations! Your new Hugo site was created in /Users/xdl/Documents/github/exactlyrics.

Just a few more steps...

1. Change the current directory to /Users/xdl/Documents/github/exactlyrics.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Or, install a theme from https://themes.gohugo.io/
1. Edit hugo.yaml, setting the "theme" property to the theme name.
2. Create new content with the command "hugo new content <SECTIONNAME>/<FILENAME>.<FORMAT>".
3. Start the embedded web server with the command "hugo server --buildDrafts".

See documentation at https://gohugo.io/.
xdl@MacBook-Air ~/Documents/github % cd exactlyrics
xdl@MacBook-Air ~/Documents/github/exactlyrics % hugo new theme lyrics
Creating new theme in /Users/xdl/Documents/github/exactlyrics/themes/lyrics
  • cd /Users/xdl/Documents/github - 第一步首先是确定在哪里创建网站,我就放在我电脑的/Users/xdl/Documents/github下边了。
  • hugo version - 给你看下我使用的hugo版本, 显示是v0.147.7.
  • hugo new site exactlyrics --format yaml - 创建新网站,我习惯yaml这种格式,hugo默认的是toml格式,关于这两种格式,可以看wiki YAML wiki TOML
  • cd exactlyrics - 根据反馈的说明,先切换到刚创建的新网站这个文件夹下(exactlyrics)
  • hugo new theme lyrics - 创建一个新的主题,方便我从头搭建。

2. 编辑网站配置文件

使用VS Code打开exactlyrics这个文件夹,编辑hugo.yaml:

baseURL: https://funlobo.com/
title: Funlobo
theme: lyrics

# language
defaultContentLanguage: en-us
defaultContentLanguageInSubdir: true
languages:
  en-us:
    contentDir: content/en-us
    disabled: false
    languageCode: en-US
    languageDirection: ltr
    languageName: English
    weight: 10
  zh-hant:
    contentDir: content/zh-hant
    disabled: false
    languageCode: zh-Hant
    languageDirection: ltr
    languageName: 繁體中文
    weight: 20
  zh-hans:
    contentDir: content/zh-hans
    disabled: false
    languageCode: zh-Hans
    languageDirection: ltr
    languageName: 简体中文
    weight: 30

# menu
menus:
  main:
    # - identifier: home
    #   name: Home
    #   url: /
    #   weight: 100
    - identifier: songs
      name: Songs
      pageRef: /songs/
      weight: 200
    - identifier: artists
      name: Artists
      pageRef: /artists/
      weight: 300
    - identifier: lyricists
      name: Lyricists
      pageRef: /lyricists/
      weight: 400
    - identifier: composers
      name: Composers
      pageRef: /composers/
      weight: 500

# 其他参数
params:
  description: head_description
  keywords: head_keyword
  assets:
    faviconIco: /assets/images/favicon/favicon.ico
    faviconSvg: /assets/images/favicon/favicon.svg
    appleTouchIcon: /assets/images/favicon/apple-touch-icon.png
    logo: /assets/images/lyrics_48dp_FFFFFF_FILL1_wght700_GRAD200_opsz48.svg
    songCover: /assets/images/genres.svg
    artistCover: /assets/images/artist.svg
    lyricistCover: /assets/images/lyrics.svg
    composerCover: /assets/images/music_note.svg
    language: /assets/images/language_36dp_FFFFFF_FILL1_wght700_GRAD200_opsz40.svg
    search: /assets/images/search.svg
    close: /assets/images/close.svg
    darkMode: /assets/images/dark_mode_36dp_FFFFFF_FILL1_wght700_GRAD200_opsz40.svg
    lightMode: /assets/images/light_mode_36dp_FFFFFF_FILL1_wght700_GRAD200_opsz40.svg
    home: /assets/images/home.svg
    hot: /assets/images/hot.svg
    trending: /assets/images/trending.svg
    moreVert: /assets/images/more_vert.svg
    moreHoriz: /assets/images/more_horiz.svg
  svgImgSize: 
    title: 64 # SVG-img 图标大小 - title 级别
    label: 24 # SVG-img 图标大小 - label 级别
  paginate:
    songs: 10
    taxonomies: 20

taxonomies:
  artists: artists
  lyricists: lyricists
  composers: composers

pagination:
  disableAliases: false
  pagerSize: 1
  path: page


server:
  headers: null
  redirects:
  - force: false
    from: /**
    fromHeaders: null
    fromRe: ""
    status: 404
    to: /404.html

outputs:
  home:
    - HTML
    - JSON

# not wanted
disableHugoGeneratorInject: true
disableRSS: true

3. 编辑主题文件

文件路径:exactlyrics/themes/lyrics/layouts/:

1. 删除无关文件

该文件夹下当前文件结构如下,这是新建theme后hugo自动创建的,除了hugo.yaml外其他的我还没有变动过:

xdl@MacBook-Air ~/Documents/github/exactlyrics % tree
.
├── archetypes
│   └── default.md
├── assets
├── content
├── data
├── hugo.yaml
├── i18n
├── layouts
├── static
└── themes
    └── lyrics
        ├── archetypes
        │   └── default.md
        ├── assets
        │   ├── css
        │   │   └── main.css
        │   └── js
        │       └── main.js
        ├── content
        │   ├── _index.md
        │   └── posts
        │       ├── _index.md
        │       ├── post-1.md
        │       ├── post-2.md
        │       └── post-3
        │           ├── bryce-canyon.jpg
        │           └── index.md
        ├── data
        ├── hugo.toml
        ├── i18n
        ├── layouts
        │   ├── _partials
        │   │   ├── footer.html
        │   │   ├── head
        │   │   │   ├── css.html
        │   │   │   └── js.html
        │   │   ├── head.html
        │   │   ├── header.html
        │   │   ├── menu.html
        │   │   └── terms.html
        │   ├── baseof.html
        │   ├── home.html
        │   ├── page.html
        │   ├── section.html
        │   ├── taxonomy.html
        │   └── term.html
        └── static
            └── favicon.ico

23 directories, 26 files

可以看到hugo自动创建了一些内容(/themes/lyrics/content下边的内容),我先将其删除掉,删除的内容包括:

  • /themes/lyrics/hugo.toml: 不需要这里hugo生成的演示用的配置文件。
  • /themes/lyrics/archetypes/default.md: 不需要这个模板文件,后期会在/exactlyrics/archetypes/下创建。
  • /themes/lyrics/assets/:不需要主题下的css和js文件,因为后期会直接在/exactlyrics/assets/下创建。
  • /themes/lyrics/content:不需要该文件夹下生成的演示用的内容,后期会在/exactlyrics/content/下创建。
  • /themes/lyrics/static/favicon.ico:不需要这里的静态图标,后期会在/exactlyrics/static/下创建。

删除后的文件结构如下:

xdl@MacBook-Air ~/Documents/github/exactlyrics % tree
.
├── archetypes
│   └── default.md
├── assets
├── content
├── data
├── hugo.yaml
├── i18n
├── layouts
├── static
└── themes
    └── lyrics
        ├── archetypes
        ├── assets
        ├── content
        ├── data
        ├── i18n
        ├── layouts
        │   ├── _partials
        │   │   ├── footer.html
        │   │   ├── head
        │   │   │   ├── css.html
        │   │   │   └── js.html
        │   │   ├── head.html
        │   │   ├── header.html
        │   │   ├── menu.html
        │   │   └── terms.html
        │   ├── baseof.html
        │   ├── home.html
        │   ├── page.html
        │   ├── section.html
        │   ├── taxonomy.html
        │   └── term.html
        └── static

19 directories, 15 files

2. 确认baseof.html文件内容

该文件结构如下,这个文件是以后生成的每个页面的基础,我们只需要填充其中的部分就可以:

<!DOCTYPE html>
<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
  {{ partial "head.html" . }}
</head>
<body>
  <header>
    {{ partial "header.html" . }}
  </header>
  <main>
    {{ partial "part/search-results.html" . }}
    {{ block "main" . }}{{ end }}
  </main>
  <footer>
    {{ partial "footer.html" . }}
  </footer>
</body>
</html>

但是这里有个不一样的地方{{ partial "part/search-results.html" . }},这是因为我想要在各个页面都实现查询效果,所以添加了这样一个组件。

3. 编辑head.html文件

浏览器的头部信息文件(用户不可见,给浏览器使用的)。我们的meta, js, css等信息就在这里编辑。

目前该文件内容为:

{{ partial "head/extend_top.html" . }}
{{ partial "head/meta.html" . }}
{{ partial "head/theme.html" . }}
{{ partial "head/css.html" . }}
{{ partial "head/js.html" . }}
{{ partial "head/extend_bottom.html" . }}

根据我的需求:

  • meta.html 用来存放meta相关的信息;
  • 网站提供明暗主题,所以需要加上一个theme.html;
  • 全站共用一个main.css,特定页面再加上自己的custome-page.css;
  • 全站共用一个main.js,特定页面再加上自己的custome-page.js;
  • 考虑到后期扩展,所以需要在顶部加上一个extend_top.html, 底部加上一个extend_bottom.html. - 比如后期要加载谷歌的统计代码,广告代码等,就可以在扩展的内容里边添加,不用更改其他部分。

根据需求,编辑head.html内容如下:

{{ partial "head/extend_top.html" . }}
{{ partial "head/meta.html" . }}
{{ partial "head/theme.html" . }}
{{ partial "head/css.html" . }}
{{ partial "head/js.html" . }}
{{ partial "head/extend_bottom.html" . }}

根据上边的需求,创建以下文件:

xdl@MacBook-Air ~/Documents/github/exactlyrics/themes/lyrics/layouts/_partials/head % tree
.
├── css.html
├── extend_bottom.html
├── extend_top.html
├── js.html
├── meta.html
└── theme.html

1 directory, 6 files

我一一说明下这6个文件。

1. extend_top.html

这个文件是用来扩展的,目前没有需要添加的代码,所以没有内容,只有注释,以后需要天际的时候在这里添加就可以:

{{- /* 在head最前边插入代码 */ -}}

{{- /* end */ -}}
2. extend_bottom.html

和上边同理。

{{- /* 在head最后边插入代码 */ -}}

{{- /* end */ -}}
3. meta.html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="keywords" content="{{- with site.Params.keywords -}}{{- T . -}}{{- end -}}">
<meta name="description" content="{{- with .Params.description -}}{{ . }}{{- else -}}{{- with site.Params.description -}}{{- T . -}}{{- end -}}{{- end -}}">
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>
{{- /* Favicons */}}
<link rel="apple-touch-icon" sizes="180x180" href="{{ site.Params.assets.appleTouchIcon | absURL }}">
<link rel="icon" href="{{ site.Params.assets.faviconIco | absURL }}">
<link rel="icon" href="{{ site.Params.assets.faviconSvg | absURL }}" type="image/svg+xml">

<meta name="keywords">提供有关关键字的内容,因为计划的是一个多语言的网站,所以可以针对每种语言有不同的翻译。

<meta name="description">提供有关网站描述的内容,与关键字类似,只是多了一个步骤:如果各个页面想有自己独特的描述内容,那么就优先提供各个页面自己的描述内容(不需要翻译是对的,因为如果是自定义的内容,那么写的时候就会按照自己想要的语言写)。 至于说各个语言对应的翻译,在hugo.yaml中已经写了,然后在i18n下创建对应的翻译文件就好。

然后是网站标题,首页的话就只显示网站标题,如果是子页面,就提供类似eaxmple songs | Eaxct Lyrics这种的标题。

下边的Favicons提供网站的图标信息,我展开说说:

  • 我选取的图片来自Google Icons ,你可以直接点击复制标签复制代码,也可以保存为svg or png 格式,我这里保存的是:

    • favicon.svg, 48px;
    • apple-touch-icon.png, 180px;
  • /exactlyrics/static/下新建/images/favicon/,把刚保存的两张图片放进去,使用imagemagick生成一个.ico文件。

xdl@MacBook-Air ~/Documents/github/exactlyrics/static/images/favicon % magick favicon.svg -quality 100 favicon.ico
xdl@MacBook-Air ~/Documents/github/exactlyrics/static/images/favicon % tree
.
├── apple-touch-icon.png
├── favicon.ico
└── favicon.svg

1 directory, 3 files
  • hugo.yaml中编辑,这里使用变量来定位文件位置,是方便后期如果更改了图片位置,不用在代码里去一一更改,只需要改动配置文件就可以:
params:
 assets:
     faviconIco: /images/favicon/favicon.ico
     faviconSvg: /images/favicon/favicon.svg
     appleTouchIcon: /images/favicon/apple-touch-icon.png
4. theme.html

提供明暗主题的切换:

{{/*
    主题设置;
    使用DOMContentLoaded确保安全;
*/}}
<script>
    (function () {
        const savedTheme = localStorage.getItem('theme') || 'light';
        document.documentElement.setAttribute('data-theme', savedTheme);
        function addThemeClass() {
            document.body.classList.add('theme-loaded');
        }
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', addThemeClass);
        } else {
            addThemeClass();
        }
    })();
</script>

这个文件是配合css的_var.css一起使用的。

5. css.html

按照前边说的需求,代码如下:

{{- /* 加载全局 CSS */}}
{{- with resources.Get "css/main.css" | postCSS }}
{{- if eq hugo.Environment "development" }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- end }}
{{- end }}

{{- /* 加载页面自定义 CSS */}}
{{- if .Params.css }}
{{- range .Params.css }}
{{- with resources.Get . | postCSS }}
{{- if eq hugo.Environment "development" }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

这里特别说明下,因为考虑到代码复用,所以尽量将代码模块化,针对hugo中的css进行模块化,并且适应我的需求:可导入 — 在一个文件中写的内容可以被导入到另外的文件中;

因此需要安装插件,具体的实施步骤可以查看Hugo中模块化css

我这里就直接记载自己的操作步骤:

xdl@MacBook-Air ~/Documents/github/exactlyrics % npm install postcss postcss-cli postcss-import autoprefixer --save-dev

added 68 packages in 12s

22 packages are looking for funding
  run `npm fund` for details

xdl@MacBook-Air ~/Documents/github/exactlyrics % tree -L 1
.
├── archetypes
├── assets
├── content
├── data
├── hugo.yaml
├── i18n
├── layouts
├── node_modules
├── package-lock.json
├── package.json
├── static
└── themes

10 directories, 3 files

可以看到多了一个文件夹node_modules和另外两个.json文件。

在根目录~/Documents/github/exactlyrics/下新建一个文件postcss.config.js。编辑其内容为:

module.exports = {
  plugins: [
      require('postcss-import')({
          path: ['assets/css'] // 指定 CSS 模块的基准路径
      }),
      require('autoprefixer') // 可选:添加浏览器前缀
  ]
}

至此就完成了Hugo中模块化css需要的插件,以后就可以使用@import来模块化使用css文件了。

上边的代码指定了 CSS 模块的基准路径,因此我们在~/Documents/github/exactlyrics/assets/css/创建这些文件夹,并创建如下文件。(说明:我知道这里有点本末倒置的感觉,实际上应该是我想把内容建立在哪里,代码来指明该位置。我这里这样写因为我明确知道我会在这个路径下创建相应的内容,写在这里只是为了记录。)

xdl@MacBook-Air ~/Documents/github/exactlyrics/assets/css % tree
.
├── _basic.css
├── _customize.css
├── _reset.css
├── _typography.css
├── _var.css
└── main.css

1 directory, 6 files

关于上边创建的内容以及相应的原因,可以阅读笔记 - 模块化CSS

1. _reset.css

这部分内容其实上边的链接中包括了,只是出于方便还是在这里再记录一下:

/*
  Modern Reset
  See https://hankchizljaw.com/wrote/a-modern-css-reset/

  Note: This file uses some CSS variables and thus cannot be wholly updated via copy+paste.
*/

/* Box sizing rules */
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* Remove default margin */
* {
    margin: 0;
}

ul,
ol {
    list-style: none;
    padding: 0;
}

/* Set core root defaults */
html {
    scroll-behavior: smooth;
}

/* body {
    -webkit-font-smoothing: antialiased;
} */

/* A elements that don't have a class get default styles */
a:not([class]) {
    text-decoration-skip-ink: auto;
}

a {
    text-decoration: none;
}

/* Make images easier to work with */
img,
picture,
video,
canvas,
svg {
    display: block;
    max-width: 100%;
}

/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
    font: inherit;
}


/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
    }
}


@media (prefers-reduced-motion: no-preference) {
    html {
        interpolate-size: allow-keywords;
    }
}


p,
h1,
h2,
h3,
h4,
h5,
h6 {
    overflow-wrap: break-word;
    hyphens: auto;
}

p {
    text-wrap: pretty;
}

h1,
h2,
h3,
h4,
h5,
h6 {
    text-wrap: balance;
}
2. _typography.css

针对英文字体我下载了三个我喜欢的,中文以及其他语种的就先算了,东亚的字体文件因为数量多的原因,字体文件的体积都很大,所以就不给他们准备了。

根据我的理解,css会针对遇到的每个字符匹配字体,如果不匹配,会自己向后一直寻找,一直找到用户本地系统,所以应用文字的时候不用担心比如中文没有相应的预设字体,它会自己向后查询。

我把在网上找的三款英文字体放在了/exactlyrics/static/fonts/下边:

xdl@MacBook-Air ~/Documents/github/exactlyrics/static/fonts % tree
.
├── ElenaWebBold-subset-v2.woff2
├── ElenaWebRegular-subset-v2.woff2
└── Mija_Bold-webfont-subset-v2.woff2

1 directory, 3 files

所以我就编辑_typography.css的内容如下:

@font-face {
  font-family: 'Elena';
  src: url('/fonts/ElenaWebRegular-subset-v2.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-007F; /* 覆盖 ASCII 的 0-127 号字符 */
}

@font-face {
  font-family: 'Elena';
  src: url('/fonts/ElenaWebBold-subset-v2.woff2') format('woff2');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-007F;
}

@font-face {
  font-family: 'Mija';
  src: url('/fonts/Mija_Bold-webfont-subset-v2.woff2') format('woff2');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-007F;
}

update 2025-06-29: 网站实际只使用了Mija这个格式,因为这个字体感觉更好看一点。

3. _var.css

存放自己设置的变量:

@import "_typography.css";

/* 默认 Light 主题 */
:root {
    
    /* Typography */
  --font-fallback:
      /* 现代系统优先 */
    -apple-system, BlinkMacSystemFont, 
    /* Windows 现代 + 旧版 */
    "Segoe UI", system-ui, 
    /* Linux/Android */
    Roboto, Ubuntu, 
    /* 通用回退 */
    "Helvetica Neue", Arial, sans-serif;

  /* --font-body: Elena, var(--font-fallback); */
  --font-body: Mija, var(--font-fallback); 
  --font-heading: Mija, var(--font-fallback);
  --font-code: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;

  --font-content-line-height: 1.75;

  --type-heading-h1-font-size: 2.488rem;
  --type-heading-h2-font-size: 2.074rem;
  --type-heading-h3-font-size: 1.728rem;
  --type-heading-h4-font-size: 1.44rem;
  --type-heading-h5-font-size: 1.2rem;
  --type-heading-h6-font-size: 1.1rem;
  --type-base-font-size-rem: 1rem;
  --type-smaller-font-size: 0.833rem;
  --type-tiny-font-size: 0.694rem;

  --type-heading-h1-font-size-mobile: 1.802rem;
  --type-heading-h2-font-size-mobile: 1.602rem;
  --type-heading-h3-font-size-mobile: 1.424rem;
  --type-heading-h4-font-size-mobile: 1.266rem;
  --type-heading-h5-font-size-mobile: 1.2rem;
  --type-heading-h6-font-size-mobile: 1.1rem;

  /* screen width */
  --screen-sm: 426px;
  --screen-md: 769px;
  --screen-lg: 992px;
  --screen-xl: 1200px;
  --screen-xxl: 1441px;

  /* main width */
  --header-width: 100%;
  --main-width-pc: 80%;
  --main-width-mobile: 90%;

  /* theme light  */
  --text-black-primary: #333333;
  --text-black-secondary: #666666;
  --text-white-primary: #FFFFFF;
  --text-link: #2288bb;
  --red-01: #d33a2c;
  --red-02: #FFE7E7;
  --green-01: #f1fdf8;
  --green-02: #18a46F;
  --purple-01: #fbecf9;
  --purple-02: #aa1994;
  --orange-01: #fef1f0;
  --orange-02: #f55700;
  --blue-01:#e7f8ff;
  --blue-02: #006fc6;
  --grey-01: #eee;
  --grey-02: #a1a1a1;
  --bg-grey-01: var(--grey-01);

  --text-body-color: var(--text-black-primary);
  --background-primary: var(--text-white-primary);
  --header-background-color: var(--red-01);
  --text-header-color: var(--text-white-primary);
  --search-background-color: var(--text-white-primary);
  --text-search-input: var(--text-black-primary);

  --type-heading-h1: var(--type-heading-h1-font-size);
  --type-heading-h2: var(--type-heading-h2-font-size);
  --type-heading-h3: var(--type-heading-h3-font-size);
  --type-heading-h4: var(--type-heading-h4-font-size);
  --type-heading-h5: var(--type-heading-h5-font-size);
  --type-heading-h6: var(--type-heading-h6-font-size);

}


/* Dark 主题 */
[data-theme="dark"] {

  --text-link: #7FCFFF;
  --red-01: #d33a2c;
  --red-02: #FFE7E7;
  --bg-grey-01: #19313c;

  --text-body-color: var(--text-white-primary);
  --article-background-color: linear-gradient(-45deg, #162c35 70%, #0c252f 100%);
  --background-primary: var(--article-background-color);
  --header-background-color: var(--red-01);
  --text-header-color: var(--text-white-primary);
  --search-background-color: var(--text-black-primary);
  --text-search-input: var(--text-white-primary);
}



@media screen and (max-width: 768px) {
  :root {
    --type-heading-h1: var(--type-heading-h1-font-size-mobile);
    --type-heading-h2: var(--type-heading-h2-font-size-mobile);
    --type-heading-h3: var(--type-heading-h3-font-size-mobile);
    --type-heading-h4: var(--type-heading-h4-font-size-mobile);
    --type-heading-h5: var(--type-heading-h5-font-size-mobile);
    --type-heading-h6: var(--type-heading-h6-font-size-mobile);
  }
}
4. _basic.css

基础设置:

@import "_var.css";

/* Set core body defaults */
body {
    font-family: var(--font-body);
    line-height: var(--font-content-line-height);
    text-rendering: optimizeSpeed;
    color: var(--text-body-color);
    background: var(--background-primary);
}

h1,
h2,
h3,
h4,
h5,
h6 {
    font-family: var(--font-heading);
    line-height: var(--font-content-line-height);
}

h1 {
    font-size: var(--type-heading-h1);
}
h2 {
    font-size: var(--type-heading-h2);
}
h3 {
    font-size: var(--type-heading-h3);
}
h4 {
    font-size: var(--type-heading-h4);
}
h5 {
    font-size: var(--type-heading-h5);
}   
h6 {
    font-size: var(--type-heading-h6);
}


a {
    color: var(--text-link);
}

ul ul,
ul ol,
ol ul,
ol ol {
    padding-left: 0.75rem; 
}

article {

    h2, h3, h4, h5, h6 {
        margin: 1rem 0;
    }

    p {
        margin: 0.5rem 0;
    }

    ol {
        list-style: decimal;
    
        ::marker {
            color: var(--text-link);
            font-weight: bold;
        }

        li {
            margin: 0.5rem 0;
        }
    }
    
    ul {
        
        ::marker {
            content: " - ";
            color: var(--text-link);
            font-weight: bold;
        }

        li {
            margin: 0.5rem 0;
        };
    }

    pre {
        padding: 1rem;
        margin: 1rem auto;
        max-height: 40vh;
        overflow: auto;
        border-radius: 0.5rem;
        font-family: var(--font-code);
        font-size: 1rem;
        background-color: var(--code-background-block);
    }


    code:not(pre code),
    kbd,
    samp {
        padding: 0.25em;
        border-radius: 0.25rem;
        font-size: 1rem;
        word-break: break-all;
        color: var(--text-primary-red);
        background-color: var(--code-background-block);
    }

    hr {
        margin: 1rem 0;
    }

    del {
        text-decoration-color: var(--text-primary-red);
        text-decoration-thickness: 1px;
        text-decoration-style: double;
    }   
    mark {
        background-color: var(--highlight-bg);
        color: var(--text-primary);
        font-weight: var(--font-body-strong-weight);
    }

    dl {
        margin: 1rem 0;

        dt {
            font-weight: bold;
            margin: 0.5rem 0;
        }

        dd {
            padding-inline-start: 0.75rem;
        }

        dd~dd,
        dt~dt {
            margin: 0.5rem 0;
        }
    }

    blockquote {
        margin: 1rem 0;
        padding: 0 1rem;
        border-inline-start: 0.2rem solid var(--text-primary-red);
    }

    table {
        margin: 1rem auto;
        border-collapse: collapse;
        width: 100%;
        max-width: 100%;
        overflow: auto;
        border-radius: 0.5rem;
        background-color: var(--background-secondary);

        thead {
            background-color: var(--background-information);
            border-top: 2px solid var(--text-link);
            border-bottom: 2px solid var(--text-link);
            font-weight: bold;
        }

        tbody {

            tr {
                border-bottom: 1px solid var(--text-primary);
            }
            
        }

        th,
        td {
            padding: 0.5rem;
            text-align: left;
        }
    }

    details {
        margin: 0.5rem 0;
        background-color: var(--background-information);
        border-radius: 0.3rem;
        padding: 0.5rem;

        summary {
            color: red;
            font-weight: bold;
        }
    }
}
5. main.css
@import "_reset.css";
@import "_basic.css";
@import "_customize.css";


body {
    transition: background-color 0.3s, color 0.3s;
}

body.theme-loaded {
    transition: background-color 0.3s ease;
}

[data-theme] {
    transition: background-color 0.3s ease, color 0.3s ease;
}

header {

    background-color: var(--header-background-color);
    color: var(--text-header-color);
    padding: 1rem;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.15);

    /* nav */
    nav {

        margin: 0 auto;
        color: var(--text-white-primary);
        display: grid;
        align-items: center;
        grid-template-areas: "logo menusPrimary functions menusSecondary search";
        grid-template-columns: 48px minmax(200px, 4fr) 112px 0 minmax(100px, 300px);
        gap: 8px;

        a {
            color: var(--text-header-color);
            font-family: var(--font-heading);
            font-weight: bold;
        }

        .logo {

            grid-area: logo;

            svg {
                width: 48px;
                height: 48px;
            }
        }

        .menus.primary {

            grid-area: menusPrimary;
            display: block;

            ul {
                display: flex;
                flex-wrap: wrap;

                .menu-item a {

                    padding: 10px 16px;
                    border-radius: 8px;
                    font-size: 1.2rem;

                    &:hover {
                        background-color: rgba(0, 0, 0, .15);
                    }
                }
            }
        }

        .btn {

            cursor: pointer;

            svg {
                width: calc(36px + 10px * 2);
                height: calc(36px + 10px * 2);
                padding: 10px;
                border-radius: 8px;

                &:hover {
                    background-color: rgba(0, 0, 0, .15);
                }
            }
        }

        .functions {
            grid-area: functions;
            display: grid;
            grid-template-columns: repeat(2, 1fr);

            .language {

                .other-languages {
                    display: none;
                    position: relative;

                    ul:has(> li) {
                        display: flex;
                        flex-direction: column;
                        gap: 0.5rem;
                        width: max-content;
                        position: absolute;
                        top: 5px;
                        background-color: var(--header-background-color);
                        padding: 1rem;
                        border-radius: 10px;

                        li {
                            border-bottom: 2px dotted white;
                        }
                    }
                }

                .other-languages[data-visible="true"] {
                    display: block;
                }
            }

            .theme {

                .btn-light-mode {
                    display: none;
                }
            }

            .menu-icon {

                display: none;

                .btn-close-menu {
                    display: none;
                }
            }
        }

        .menus.secondary {
            grid-area: menusSecondary;
            display: none;

            ul {
                display: flex;
                flex-wrap: wrap;

                .menu-item a {

                    padding: 10px 16px;
                    border-radius: 8px;
                    font-size: 1.2rem;
                }
            }
        }

        .search {
            grid-area: search;
            display: flex;
            justify-content: center;

            .search-box {
                max-width: 100%;
                appearance: none;
                border: none;
                border-radius: 0.7rem;
                outline: none;
                background-color: var(--text-primary);
                color: var(--text-secondary);
                font-family: var(--font-heading);
                font-weight: bold;
                font-size: 1.2rem;
                padding: 0.7rem 1rem;
                padding-left: 3rem;
                color: var(--text-search-input);
                background-color: var(--search-background-color);
                background-image: url(/assets/images/search.svg);
                background-repeat: no-repeat;
                background-size: 1.5rem;
                background-position: 1rem 45%;

                &:not(:focus, :active)::-webkit-search-cancel-button {
                    display: none;
                }

                &::placeholder {
                    color: var(--text-body-color);
                    /* 设置占位符颜色 */
                    opacity: 0.7;
                    /* 设置占位符透明度 */
                }
            }
        }
    }
}

/* 大屏幕(默认) */
header nav {
    width: 80%;
}

@media screen and (max-width: 1200px) {
    header nav {
        width: 100%;
    }
}

/* 中等屏幕(990px 以下) */
/* @media screen and (max-width: 990px) {
    header nav {
        width: 100%;
    }
} */

/* 小屏幕(768px 以下) */
@media screen and (max-width: 768px) {

    header {
        nav {
            width: 100%;
            grid-template-areas:
                "logo functions"
                "menusSecondary menusSecondary"
                "search search";
            grid-template-columns: minmax(48px, 3fr) minmax(168px, 1fr);

            .menus.primary {

                display: none;
            }

            .functions {

                grid-template-columns: repeat(3, 1fr);

                .menu-icon {

                    display: block;
                }
            }

            .search {

                .search-box {
                    width: 100%;
                }
            }
        }
    }
}


main {
    width: var(--main-width-pc);
    margin: 0.3rem auto;
    padding: 0.3rem 0;
    min-height: 80vh;
    /* border-bottom: 2px solid var(--text-secondary); */
}


footer {
    border-bottom: none;
}

@media screen and (max-width: 768px) {

    main {
        width: var(--main-width-mobile);
    }
}

/* general */

.box-shadow {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.15);
}

.box-shadow-2:hover {
    box-shadow: 0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,3%);
    transition: ease-in-out .2s;
}



/* part */
/* btn return to top */
#btn-return-to-top {
    opacity: 0;
    visibility: hidden;
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 99;
    cursor: pointer;
    padding: 0.5rem;
    border-radius: 50%;
    background-color: var(--background-secondary);
    /* box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12); */
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.15);
    transition: opacity 0.3s ease, visibility 0.3s ease;

    &:hover {
        background-color: var(--background-tertiary);
    }

    &.show {
        opacity: 1;
        visibility: visible;
    }
}



#search-results {
    list-style: none;
    padding: 0;
    margin: 1rem auto;
    width: 80%;
    max-height: 50vh;
    overflow: auto;

    li {
        padding: 0.5rem 0;
        border-bottom: 1px dotted var(--text-primary);
        ;
    }

    a {
        display: block;
        padding: 0.5rem 0;
        color: var(--text-primary);
    }

    .book-meta {
        display: flex;
        gap: 1rem;
        font-size: 0.9rem;
    }
}

.search-highlight {
    background-color: var(--highlight-bg);
    font-weight: var(--font-body-strong-weight);
}

@media screen and (max-width: 768px) {
    .search-input-area {
        width: 100%;
    }

    #search-results {
        margin: 0.5rem auto;
        width: 100%;
    }
}

.search-results-box {
    display: none;
    border-radius: 0.8rem;
    max-height: 40vh;
    overflow-y: auto;
    padding: 1rem;
    background-color: var(--bg-grey-01);
    font-family: 'Mija';
}

/* .search-results {
    list-style: none;
    padding: 0;
    margin: 0;
} */

.search-result-item {
    padding: 0.5rem;
    border-bottom: 2px solid var(--text-body-color);
}

.search-result-item p {
    color: var(--text-body-color);
}

.no-results {
    padding: 1rem;
    text-align: center;
}

.search-highlight {
    background-color: #ffeb3b; /* 亮黄色背景 */
    color: #000; /* 黑色文字 */
    font-weight: bold;
    padding: 0 3px;
    border-radius: 5px;
}

/* 分页 */
.pagination-default {

    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0;
    margin: 1rem auto;

    .page-item {

        a {
            display: block;
            padding: 0.5rem;
        }

        &.active a,
        &.disabled a {
            color: var(--text-secondary);
            cursor: not-allowed;
        }
    }
}

/* external-link */
a:has(.external-link-icon) {
    display: inline-flex;

    svg {
        width: 1rem;
        height: 1rem;
        margin-left: 0.2rem;
        fill: var(--text-secondary);
    }
}

/* 404 */
.page-not-found {
    margin: 1rem auto;
    text-align: center;

}


/* song section */
.song-section {

    .song-info {

        display: flex;
        align-items: center;
        gap: 1rem;
        margin: 1rem 0;
    }

    .song-lists {

        ul li {

            font-size: 1.2rem;
            font-weight: bold;
            padding: 0.5rem;
            border-bottom: 2px solid var(--text-body-color);

            .song-info-full {

                display: grid;
                grid-template-columns: minmax(150px, 2fr) minmax(100px, 1fr) minmax(100px, 1fr) minmax(100px, 1fr);
                gap: 1rem;
                width: 100%;
                overflow-x: auto;

                .song-title,
                .artists,
                .lyricists,
                .composers {

                    display: flex;
                    gap: 0.5rem;
                    align-items: center;
                    overflow: auto;

                    ul {
                        display: flex;
                        align-items: center;
                        gap: 0.5rem;

                        li {
                            padding: none;
                            border: none;
                        }
                    }
                }
            }
        }

        ul ul {
            padding-left: 0;
        }
    }
}

@media screen and (max-width: 768px) {

    .song-section .song-lists ul li {

        font-size: 1.1rem;
        .song-info-full {

            grid-template-columns: minmax(150px, 1fr) minmax(100px, 1fr);

            .lyricists,
            .composers {
                display: none;
            }
        }
    }
}

/* taxonomy */
.taxonomy-container {

    .header {
        display: flex;
        align-items: center;
        gap: 1rem;
        margin: 1rem 0;
    }

    .content {

        font-size: 1.2rem;
        font-weight: bold;
        display: flex;
        gap: 0.5rem;
        align-items: center;
        flex-wrap: wrap;
        ul {

            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 0.5rem;

            li {
                
                a {
                    display: flex;
                    align-items: center;
                    gap: 0.5rem;
                    margin: 0.5rem;
                    padding: 0.5rem 0.8rem;
                    border-radius: 0.5rem;

                    &.artists {
                        color: var(--green-02);
                        background-color: var(--green-01);
                    }

                    &.lyricists {
                        color: var(--orange-02);
                        background-color: var(--orange-01);
                    }

                    &.composers {
                        color: var(--purple-02);
                        background-color: var(--purple-01);
                    }
                }
            }
        }
    }
}

/* taxonomy term */
.term-container {

    .term-info {

        display: flex;
        align-items: center;
        gap: 1rem;
        margin: 1rem 0;
    }

    .term-lists {

        ul li {

            font-size: 1.2rem;
            padding: 0.5rem;
            border-bottom: 2px solid var(--text-body-color);

            .song-info-term {

                display: grid;
                grid-template-columns: minmax(150px, 2fr) minmax(100px, 1fr) minmax(100px, 1fr);
                width: 100%;
                overflow-x: auto;

                .song-title,
                .artists,
                .lyricists,
                .composers {

                    display: flex;
                    gap: 0.5rem;
                    align-items: center;

                    ul {
                        display: flex;
                        align-items: center;
                        gap: 0.5rem;

                        li {
                            padding: none;
                            border: none;
                        }
                    }
                }
            }
        }

        ul ul {
            padding-left: 0;
        }
    }
}


@media screen and (max-width: 768px) {

    .term-container .term-lists ul li {

        font-size: 1.1rem;
        .song-info-term {

            grid-template-columns: minmax(150px, 1fr) minmax(100px, 1fr);

            .term-02 {
                display: none;
            }
        }
    }
}

/* song page */
.song-page {

    .song-info {
        display: flex;
        align-items: center;
        gap: 1rem;
        margin-bottom: 1rem;

        .cover img {
            border-radius: 0.5rem;
        }
    }


    .song-meta {


        ul ul {
            padding-left: 0;
        }

        margin-bottom: 1rem;

        .meta-item {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 1rem;
            align-items: center;
            max-width: 100%;
            overflow: auto;

            .terms {
                font-size: 1.2rem;
                font-weight: bold;
                display: flex;
                align-items: center;
                width: max-content;
                max-width: 100%;
                overflow: auto;
                text-wrap: nowrap;

                .label {
                    padding: 0.5rem;
                    border-top-left-radius: 0.5rem;
                    border-bottom-left-radius: 0.5rem;
                    /* color: var(--text-black-secondary); */
                    opacity: 0.7;
                    background-color: var(--bg-grey-01);
                }

                ul {
                    padding: 0.5rem;
                    display: flex;
                    border-bottom-right-radius: 0.5rem;
                    border-top-right-radius: 0.5rem;
                }

                a {
                    margin: 0 0.5rem;
                }

                &.artists {

                    ul {
                        background-color: var(--green-01);
                    }

                    a {
                        color: var(--green-02);
                    }
                }

                &.lyricists {

                    ul {
                        background-color: var(--orange-01);
                    }

                    a {
                        color: var(--orange-02);
                    }
                }

                &.composers {

                    ul {
                        background-color: var(--purple-01);
                    }

                    a {
                        color: var(--purple-02);
                    }
                }
            }
        }
    }

    .lyrics-container {

        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        gap: 1rem;
        align-items: center;
        margin-bottom: 1rem;

        .lyrics-format {
            padding: 1rem;
            border-radius: 0.5rem;
            display: flex;
            flex-direction: column;
            gap: 1rem;

            pre {
                color: var(--text-black-secondary);
            }

            &.lrc {
                background-color: var(--green-01);

                h2 {
                    color: var(--green-02);
                }

                .btn-download {
                    background-color: var(--green-02);
                }
            }

            &.srt {
                background-color: var(--orange-01);

                h2 {
                    color: var(--orange-02);
                }

                .btn-download {
                    background-color: var(--orange-02);
                }
            }

            &.txt {
                background-color: var(--purple-01);

                h2 {
                    color: var(--purple-02);
                }

                .btn-download {
                    background-color: var(--purple-02);
                }
            }
        }

        h2 {
            text-align: center;
        }

        .lyrics-content {
            height: 30vh;
            overflow-y: auto;
            margin-bottom: 1rem;
        }

        .download-lyrics {

            margin: 1rem auto;
            .btn-download {
                padding: 1rem 2rem;
                border-radius: 0.7rem;
                font-weight: bold;
                font-size: 1.5rem;
                color: var(--text-white-primary);
                
            }
        }

        
    }

    .youtube-embed {
        border-radius: 1rem;
        margin: 2rem auto;
    }
}

footer {

    margin: 1rem auto;
    padding: 1rem 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-top: 2px solid var(--text-body-color);
    
}

/* home page */
.home {

    display: grid;
    grid-template-areas: "left right";
    gap: 1rem;
    grid-template-columns: 3fr 1fr;

    .title {
        color: var(--text-body-color);
    }

    .left {
        grid-area: left;

        .song-info {
            justify-content: space-between;

            .left-area {
                display: flex;
                align-items: center;
                gap: 1rem;
            }
        }
    }

    .right {
        grid-area: right;
        display: grid;
        gap: 1rem;
        grid-template-areas: 
        "artists"
        "lyricists"
        "composers"
        ;

        .header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 1rem;
            margin: 1rem 0;

            .left-area {
                display: flex;
                align-items: center;
                gap: 1rem;
            }
        }


        .taxonomy-container.artists {
            grid-area: artists;
        }

        .taxonomy-container.lyricists {
            grid-area: lyricists;
        }

        .taxonomy-container.composers {
            grid-area: composers;
        }
    }
}

@media screen and (max-width: 768px) {

    .home {

        grid-template-areas: 
        "left left"
        "right right"
        ;
    }
}

/* pagination */
.pagination {

    margin: 2rem auto;
    width: 100%;
    font-family: "Mija";
    font-size: 1.1rem;
    font-weight: bold;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;

    

    .prev,
    .next {
        width: 66px;
        height: 56px;
        padding: 10px 15px;
        border-radius: 0.5rem;
        background-color: var(--blue-01);

        svg {
            width: 36px;
            height: 36px;
            color: var(--blue-02);
        }


        &.disabled {

            svg {
                color: var(--grey-02);
            }
            cursor: not-allowed;
        }
    }
}

这里的main.css是我根据网站需要自己写的,所以像其他几个css文件可以直接使用

6. js.html

按照前边说的需求,代码如下:

{{- /* 加载全局 JS */}}
{{- with resources.Get "js/main.js" }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build (dict "target" "es2018") }}
<script type="module" src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true "target" "es2018" }}
{{- with . | js.Build $opts | fingerprint }}
<script type="module" src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}

{{- /* 加载页面自定义 JS */}}
{{- if .Params.js }}
{{- range .Params.js }}
{{- with resources.Get . }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build (dict "target" "es2018") }}
<script type="module" src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true "target" "es2018" }}
{{- with . | js.Build $opts | fingerprint }}
<script type="module" src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

js模块化不需要额外的插件,只需要指明module,这里因为网站需要的js代码极少,所以没有使用到外部的代码。如果需要,可以参考笔记 - 模块化JS

/Users/xdl/Documents/github/exactlyrics/assets/js下创建一个main.js文件:

document.addEventListener('DOMContentLoaded', () => {
  // 初始化菜单功能
  initMenu();

  // 初始化语言功能
  initLang();

  // 初始化主题功能
  initTheme();

  // 初始化搜索功能
  initSearch();

  // 初始化返回顶部按钮功能
  initReturnToTop();
});

// 初始化菜单功能
function initMenu() {
  const btnOpenMenu = document.querySelector('.btn-open-menu');
  const btnCloseMenu = document.querySelector('.btn-close-menu');
  const menus = document.querySelector('.menus.secondary');

  if (btnOpenMenu && btnCloseMenu && menus) {
    // 打开菜单函数
    const openMenu = () => {
      menus.style.display = 'block';
      btnOpenMenu.style.display = 'none';
      btnCloseMenu.style.display = 'block';
    };

    // 关闭菜单函数
    const closeMenu = () => {
      menus.style.display = 'none';
      btnOpenMenu.style.display = 'block';
      btnCloseMenu.style.display = 'none';
    };

    // 点击菜单按钮
    btnOpenMenu.addEventListener('click', (e) => {
      e.stopPropagation(); // 防止触发document的点击事件
      openMenu();
    });

    // 点击关闭按钮
    btnCloseMenu.addEventListener('click', closeMenu);

    // 点击页面其他地方关闭
    document.addEventListener('click', (e) => {
      // 如果点击的不是菜单本身,也不是菜单按钮
      if (!menus.contains(e.target) && e.target !== btnOpenMenu) {
        closeMenu();
      }
    });

    // 按ESC键关闭
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && menus.style.display === 'block') {
        closeMenu();
      }
    });
  }
}

// 初始化语言功能
function initLang() {
    const btnLanguage = document.querySelector('.btn-language');
    const otherLanguages = document.querySelector('.other-languages');

    // 点击语言按钮切换
    btnLanguage.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleLanguageMenu();
    });

    // 点击外部关闭
    document.addEventListener('click', (e) => {
        // 如果点击的是菜单本身或其子元素,不关闭
        if (e.target.closest('.other-languages')) return;
        otherLanguages.dataset.visible = 'false';
    });

    // 按 Esc 键关闭
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            otherLanguages.dataset.visible = 'false';
        }
    });

    function toggleLanguageMenu() {
        const isVisible = otherLanguages.dataset.visible === 'true';
        otherLanguages.dataset.visible = !isVisible;
    }
}

// 初始化主题功能
function initTheme() {
  const btnLightMode = document.querySelector('.btn-light-mode');
  const btnDarkMode = document.querySelector('.btn-dark-mode');

  if (btnLightMode && btnDarkMode) {
    // 绑定主题切换事件
    btnLightMode.addEventListener('click', () => toggleTheme('light'));
    btnDarkMode.addEventListener('click', () => toggleTheme('dark'));

    // 初始化主题
    const currentTheme = localStorage.getItem('theme') || 'light';
    setTheme(currentTheme);
  }
}

// 切换主题
function toggleTheme(theme) {
  setTheme(theme);
  localStorage.setItem('theme', theme); // 保存主题到 localStorage
}

// 设置主题
function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);

  const btnLightMode = document.querySelector('.btn-light-mode');
  const btnDarkMode = document.querySelector('.btn-dark-mode');

  if (btnLightMode && btnDarkMode) {
    if (theme === 'light') {
      btnLightMode.style.display = 'none';
      btnDarkMode.style.display = 'block';
    } else {
      btnLightMode.style.display = 'block';
      btnDarkMode.style.display = 'none';
    }
  }
}


// 初始化搜索功能
function initSearch() {
    const searchInput = document.querySelector('.search-box');
    const searchResultsBox = document.querySelector('.search-results-box');
    const noResultsDiv = searchResultsBox.querySelector('.no-results');
    const resultsList = searchResultsBox.querySelector('.search-results');
    
    let searchIndex = [];
    let currentLanguage = 'en-us';

    // 更可靠的语言检测函数
    function getCurrentLanguage() {
        // 方法1:从URL路径检测
        const path = window.location.pathname;
        const langFromPath = path.match(/\/([a-z]{2}-[a-z]{2})(\/|$)/);
        if (langFromPath) return langFromPath[1];
        
        // 方法2:从html标签检测
        const htmlLang = document.documentElement.lang;
        if (htmlLang) return htmlLang.toLowerCase();
        
        // 默认值
        return 'en-us';
    }

    // 获取翻译文本(简化版)
    function getTranslations(lang) {
        const translations = {
            'en-us': {
                artists: 'Artists',
                lyricists: 'Lyricists',
                composers: 'Composers'
            },
            'zh-hans': {
                artists: '歌手',
                lyricists: '作词',
                composers: '作曲'
            },
            'zh-hant': {
                artists: '歌手',
                lyricists: '作詞',
                composers: '作曲'
            }
        };
        return translations[lang] || translations['en-us'];
    }

    // 初始化语言设置
    currentLanguage = getCurrentLanguage();
    const translations = getTranslations(currentLanguage);

    // 修正链接语言
    function correctLinkLanguage(link) {
        if (!link) return link;
        return link.replace(/\/([a-z]{2}-[a-z]{2})\//, `/${currentLanguage}/`);
    }

    // 加载搜索索引
    function loadSearchIndex() {
        const indexUrl = `/${currentLanguage}/index.json`;
        
        return fetch(indexUrl)
            .then(response => {
                if (!response.ok) throw new Error('Network response was not ok');
                return response.json();
            })
            .then(data => {
                // 修正索引中的链接语言
                searchIndex = data.map(item => ({
                    ...item,
                    permalink: correctLinkLanguage(item.permalink)
                }));
            })
            .catch(error => {
                console.error('Error loading search index:', error);
                if (currentLanguage !== 'en-us') {
                    return fetch('/en-us/index.json')
                        .then(response => response.json())
                        .then(data => {
                            searchIndex = data.map(item => ({
                                ...item,
                                permalink: correctLinkLanguage(item.permalink)
                            }));
                        });
                }
            });
    }

    // 搜索函数(带去重功能)
    function performSearch(query) {
        const trimmedQuery = query.trim();
        
        if (!trimmedQuery) {
            searchResultsBox.style.display = 'none';
            return;
        }
        
        searchResultsBox.style.display = 'block';
        
        const queryLower = trimmedQuery.toLowerCase();
        const uniqueResults = new Map();
        
        searchIndex.forEach(item => {
            const songMatch = item.songs && item.songs.toLowerCase().includes(queryLower);
            const artistsMatch = item.artists && item.artists.some(artist => artist.toLowerCase().includes(queryLower));
            const lyricistsMatch = item.lyricists && item.lyricists.some(lyricist => lyricist.toLowerCase().includes(queryLower));
            const composersMatch = item.composers && item.composers.some(composer => composer.toLowerCase().includes(queryLower));
            
            if (songMatch || artistsMatch || lyricistsMatch || composersMatch) {
                if (!uniqueResults.has(item.permalink)) {
                    uniqueResults.set(item.permalink, item);
                }
            }
        });
        
        displayResults(Array.from(uniqueResults.values()), trimmedQuery);
    }

    // 显示结果函数
    function displayResults(results, query) {
        resultsList.innerHTML = '';
        
        if (results.length === 0) {
            noResultsDiv.style.display = 'block';
            return;
        }
        
        noResultsDiv.style.display = 'none';
        
        function highlightText(text, query) {
            if (!query || !text) return text;
            
            const queryLower = query.toLowerCase();
            const textLower = text.toLowerCase();
            let startIndex = 0;
            let result = '';
            
            while (startIndex < text.length) {
                const matchIndex = textLower.indexOf(queryLower, startIndex);
                
                if (matchIndex === -1) {
                    result += text.substring(startIndex);
                    break;
                }
                
                result += text.substring(startIndex, matchIndex);
                result += `<span class="search-highlight">${text.substring(matchIndex, matchIndex + query.length)}</span>`;
                startIndex = matchIndex + query.length;
            }
            
            return result || text;
        }
        
        results.forEach(item => {
            const li = document.createElement('li');
            li.className = 'search-result-item';
            
            const link = document.createElement('a');
            link.href = item.permalink;
            
            const highlightedSong = highlightText(item.songs || 'Untitled', query);
            let artistsHtml = '';
            let lyricistsHtml = '';
            let composersHtml = '';
            
            if (item.artists) {
                const highlightedArtists = item.artists.map(artist => highlightText(artist, query));
                artistsHtml = `<p>${translations.artists}: ${highlightedArtists.join(', ')}</p>`;
            }
            
            if (item.lyricists) {
                const highlightedLyricists = item.lyricists.map(lyricist => highlightText(lyricist, query));
                lyricistsHtml = `<p>${translations.lyricists}: ${highlightedLyricists.join(', ')}</p>`;
            }
            
            if (item.composers) {
                const highlightedComposers = item.composers.map(composer => highlightText(composer, query));
                composersHtml = `<p>${translations.composers}: ${highlightedComposers.join(', ')}</p>`;
            }
            
            link.innerHTML = `
                <h3>${highlightedSong}</h3>
                ${artistsHtml}
                ${lyricistsHtml}
                ${composersHtml}
            `;
            
            li.appendChild(link);
            resultsList.appendChild(li);
        });
    }

    // 初始化搜索
    loadSearchIndex().then(() => {
        searchInput.addEventListener('input', (e) => {
            performSearch(e.target.value);
        });
        
        document.addEventListener('click', (e) => {
            if (!searchResultsBox.contains(e.target) && e.target !== searchInput) {
                searchResultsBox.style.display = 'none';
            }
        });
        
        searchInput.addEventListener('focus', () => {
            if (searchInput.value.trim()) {
                searchResultsBox.style.display = 'block';
            }
        });

        // 新增:输入框失去焦点时清空内容
        searchInput.addEventListener('blur', () => {
            // 使用setTimeout确保点击搜索结果链接能正常跳转
            setTimeout(() => {
                searchInput.value = '';
                searchResultsBox.style.display = 'none';
            }, 200);
        });
    }).catch(error => {
        console.error('Search initialization failed:', error);
    });
}


// 初始化返回顶部按钮功能
function initReturnToTop() {
  const btnReturnToTop = document.getElementById('btn-return-to-top');

  if (btnReturnToTop) {
    // 监听页面滚动事件
    window.addEventListener('scroll', () => {
      if (window.scrollY > 100) {
        btnReturnToTop.classList.add('show');
      } else {
        btnReturnToTop.classList.remove('show');
      }
    });

    // 监听按钮点击事件
    btnReturnToTop.addEventListener('click', () => {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    });
  }
}

主要的提供的功能就是以下几个:

  • 初始化菜单功能 initMenu(): 提供手机界面的汉堡菜单功能;

  • 初始化语言功能 initLang(): 语言按钮的功能;

  • 初始化主题功能 initTheme(): 明暗主题的切换;

  • 初始化搜索功能 initSearch(): 搜索功能;

  • 初始化返回顶部按钮功能 initReturnToTop(): 返回顶部,在网站中实际没应用上。

4. 编辑header.html文件

{{ $svgcolor := "#FF0000" }}

{{ $svgImgWidth := 36 }}

{{ $logoSvg := site.Params.assets.logo }}
{{ $langSvg := site.Params.assets.language }}
{{ $darkModeSvg := site.Params.assets.darkMode }}
{{ $lightModeSvg := site.Params.assets.lightMode }}
{{ $searchSvg := site.Params.assets.search }}
{{ $closeSvg := site.Params.assets.close }}

{{ $currentLangLower := lower .Language }}

<nav class="top">

    <div class="logo">
        <a href="{{ site.Home.RelPermalink | absLangURL }}">
            {{ partial "svg/lyrics.html" "white" }}
        </a>
    </div>

    <div class="menus primary">
        <ul>
            {{ range site.Menus.main }}
                <li class="menu-item">
                    <a href="{{ .PageRef | relLangURL }}">
                        {{- lower .Name | T | strings.FirstUpper -}}
                    </a>
                </li>
            {{ end }}
        </ul>
    </div>
    
    <div class="functions">

        <div class="language">

            <div class="btn btn-language">
                {{ partial "svg/language.html" "white" }}
            </div>

            <div class="other-languages">

                {{ with .AllTranslations }}
                    <ul>
                        {{- range . }}
                            {{ if ne (lower .Language.LanguageCode) $currentLangLower}}
                                <li>
                                    <a href="{{ .RelPermalink }}" hreflang="{{ .Language.LanguageCode }}">{{- .Language.LanguageName -}}</a>
                                </li>
                            {{ end }}
                        {{ end -}}
                    </ul>
                {{ end }}

            </div>
        </div>

        <div class="theme">
            <div class="btn btn-dark-mode">
                {{ partial "svg/dark-mode.html" "white" }}
            </div>

            <div class="btn btn-light-mode">
                {{ partial "svg/light-mode.html" "white" }}
            </div>
        </div>

        <div class="menu-icon">
            <div class="btn btn-open-menu">
                {{ partial "svg/menu.html" "white" }}
            </div>
            <div class="btn btn-close-menu">
                {{ partial "svg/close.html" "white" }}
            </div>
        </div>

    </div>

    <div class="menus secondary"> 
        <ul>
            {{ range site.Menus.main }}
                <li class="menu-item">
                    <a href="{{ .PageRef | relLangURL }}">
                        {{- lower .Name | T | strings.FirstUpper -}}
                    </a>
                </li>
            {{ end }}
        </ul>
    </div>

    <div class="search">
        <input type="search" placeholder="{{- T "search" | strings.FirstUpper -}}..." id="search-input" class="search-box box-shadow" aria-label="{{- T "search" | strings.FirstUpper -}}..." />
    </div>
    
</nav>

可以看到提供的功能就是网站logo, 菜单栏,语言,明暗主题以及搜索框。

这里的菜单栏提供了两次,是因为在pc等宽屏上可以直接展示菜单,那么css隐藏掉手机端的菜单栏就好,手机端与此相反。

5. 编辑part/search-results.html文件

我们开始编辑<main>下边的内容,第一个就是搜索结果。

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/_partials/part/search-results.html

{{- /*
    Search box partial template

    Purpose:
    Display a search results box for users.
    - if the search box is empty, it will display "Oh oh no result".
    - if the search box is not empty, it will display search results.

    Usage:
    {{ partial "part/search-results.html" . }}

    Has been used in:
    - themes/lyrics/layouts/baseof.html

*/ -}}

<div class="search-results-box">
    <div class="no-results">
        <h2>{{ T "no-search-result-title" }}</h2>
        <p>{{ T "no-search-result-content" }}</p>
    </div>

    <ul class="search-results"></ul>
</div>

可以看到内容不多,因为实际查询的时候是结合js代码进行动态生成的,当然这也和生成的index.json有关。

6. 编辑index.json文件。

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/index.json

{{- $.Scratch.Add "index" slice -}}
{{- range site.RegularPages -}}
  {{- $.Scratch.Add "index" (dict
    "songs" .Title
    "artists" .Params.artists
    "lyricists" .Params.lyricists
    "composers" .Params.composers
    "permalink" .Permalink
  ) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

可以看到收集的页面就是每首歌曲的页面,数据包括:

  • 歌曲标题;
  • 歌手;
  • 作词;
  • 作曲
  • 网页链接

有了这几个内容,那么结果js的搜索查询,我们就可以实现用户查询的关键字只要匹配上方的任意内容就可以通过链接跳转到相应的网页。

7. 编辑songs文件

我们需要提供歌曲对应的单页文件(每首歌曲对应的网页),也就是page.html(以前的版本叫single.html),和对应的聚合网页,section.html(也就是以前的list.html)。

所以:

xdl@MacBook-Air ~/Documents/github/exactlyrics/themes/lyrics/layouts/songs % tree
.
├── page.html
└── section.html

1 directory, 2 files
1. page.html
{{ define "main" }}

{{ $currentLanguageCode := .Language.LanguageCode | lower }}
{{ $songTitle := .Title }}
{{ $artists := .Params.artists }}
{{ $lyricists := .Params.lyricists }}
{{ $composers := .Params.composers }}
{{ $cover := .Params.cover | default site.Params.assets.songCover }}
{{ $youtubeVideo := .Params.youtubeVideo }}
{{ $originalLang := .Params.originalLang }}
{{ $lyrics := .Params.lyrics }}


{{/* 定义所有taxonomy类型 */}}
{{ $allTaxonomies := slice "artists" "lyricists" "composers" }}

  <div class="song-page">

    <div class="song-info">

      <div class="cover">
        {{ partial "part/svg-img.html" (dict 
          "src" $cover
          "alt" "song cover"
          "class" "song-cover"
          "loading" "eager"
          "width" "64")
        }}
      </div>

      <div class="title">
        <h1>{{- $songTitle -}}</h1>
      </div>

    </div>

    <div class="song-meta">

        <ul class="meta-item">
          {{ $page := . }}
          {{ range $allTaxonomies }}
            {{ with . }}
              <li>
                {{ partial "terms.html" (dict "taxonomy" . "page" $page) }}
              </li>
            {{ end }}
          {{ end }}
        </ul>

    </div>

    <div class="lyrics-container">

      {{ range $lyricsFormat, $lyricsFormatContents := $lyrics }}

        <div class="lyrics-format {{ $lyricsFormat }}">

          <h2>{{ $lyricsFormat | upper }}</h2>

          {{ $lyricsFormatCurrentLang := index $lyricsFormatContents $currentLanguageCode }}
          {{ $lyricsFormatOriginalLang := index $lyricsFormatContents $originalLang }}

          {{/* 与当前页面语言一致的歌词优先,如果没有就显示该歌的原始语言歌词 */}}
          {{ $lyricsFormatContentLang := $lyricsFormatCurrentLang | default $lyricsFormatOriginalLang }}

          {{ with resources.Get $lyricsFormatContentLang }}

            <div class="lyrics-content">
              <pre>{{ .Content | safeHTML }}</pre>
            </div>
            
            <div class="download-lyrics">
              <a href="{{ .RelPermalink }}" class="btn-download"
                download="{{- site.Title }} - {{ $songTitle -}}.{{- $lyricsFormat -}}">
                {{- i18n "download" | strings.FirstUpper }} {{"."}} {{ $lyricsFormat | lower }} {{ i18n "file" | strings.FirstUpper -}}
              </a>
            </div>

          {{ end }}

        </div>

      {{ end }}

    </div>

    {{ with $youtubeVideo }}
      {{ partial "part/youtube" (dict "id" $youtubeVideo) }}
    {{ end }}
  </div>

{{ end }}

有了这个文件,每个歌曲就有了自己的页面。比如:/en-us/songs/hponhrdfq7/.

2.section.html
{{ define "main" }}

  {{ $coverImages := dict
    "songs" site.Params.assets.songCover
    "artists" site.Params.assets.artistCover
    "lyricists" site.Params.assets.lyricistCover
    "composers" site.Params.assets.composerCover
  }}

  <div class="song-section">

    <div class="song-info">

      <div class="cover">
        {{ partial "part/svg-img.html" (dict 
          "src" site.Params.assets.songCover
          "alt" "song cover"
          "class" "song-cover"
          "loading" "eager"
          "width" site.Params.svgImgSize.title)
        }}
      </div>

      <div class="title">
        <h1>{{- .Title -}}</h1>
      </div>

    </div>
    
    {{ $paginator := .Paginate .Pages.ByDate.Reverse site.Params.paginate.songs }}

    <div class="song-lists">

      <ul>

        <li>
          <div class="song-info-full">

            <div class="song-title">

                <span class="label-img">

                    {{ partial "part/svg-img.html" (dict 
                        "src" $coverImages.songs 
                        "alt" "song thumb"
                        "loading" "eager"
                        "width" site.Params.svgImgSize.label)
                    }}

                </span>

                <div>
                    <p>{{- T "songs" | strings.FirstUpper -}}</p>
                </div>
            </div>

            {{- /* 定义要显示的taxonomy类型 */ -}}
            {{- $allTaxonomies := slice "artists" "lyricists" "composers" -}}
            
            {{- range $allTaxonomies -}}
                {{ $taxonomy := . -}}
                {{ $coverImage := index $coverImages . }}
                    <div class="{{- . -}}">
                        <span class="label-img">
                            {{ partial "part/svg-img.html" (dict 
                                "src" $coverImage
                                "alt" "Description of image"
                                "class" "additional-class"
                                "loading" "eager"
                                "width" site.Params.svgImgSize.label)
                            }}
                        </span>

                        <div>
                          <p>{{- T $taxonomy | strings.FirstUpper -}}</p>
                        </div>
                    </div>

            {{- end -}}
          </div>
        </li>

        {{ range $paginator.Pages }}
      
          <li>
            {{ partial "part/link-song-info-full.html" . }}
          </li>

        {{ end }}

      </ul>

    </div>

  </div>
  
  

  {{ partial "pagination.html" . }}

{{ end }}

有了这个文件,就有了歌曲的聚合网页。比如:/en-us/songs/.

说明一下, 这两个文件不见的一定要放到songs下边,也可以直接编辑layouts/page.htmllayouts/section.html这两个文件,因为我们创建的网站只会有songs这一个section,所以可以直接编辑,不用创建layouts/songs/。但如果有多个section,并且每个还都不一样,那么按照这里的每个section都有自己的page.htmlsection.html有助于明确的划分。

8. 编辑taxonomy.html文件

我们前边说过,每首歌曲都有自己的歌手,作词,作曲(如果没有那就空白不提供就好了),我们可以据此进行分类,所以网站上只有这三个分类。

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/taxonomy.html:

{{ define "main" }}
  {{ $coverImages := dict
    "songs" site.Params.assets.songCover
    "artists" site.Params.assets.artistCover
    "lyricists" site.Params.assets.lyricistCover
    "composers" site.Params.assets.composerCover
  }}

  {{ $titleLower := .Title | lower }}
  {{ $imgAlt := printf "%s cover" $titleLower }}
  {{ $coverImage := index $coverImages $titleLower }}

  <div class="taxonomy-container">
    <div class="header">
      <div class="cover">
        {{ partial "part/svg-img.html" (dict 
          "src" $coverImage
          "alt" $imgAlt
          "loading" "eager"
          "width" site.Params.svgImgSize.title)
        }}
      </div>
      <div class="title">
        <h1>{{ i18n $titleLower | strings.FirstUpper }}</h1>
      </div>
    </div>

    <div class="content">

      {{ $pages := slice }}
      {{ range $term := .Data.Terms.ByCount }}
        {{ $pages = $pages | append $term.Page }}
      {{ end }}        
        
        {{/* 对页面集合进行分页 */}}
        {{ $paginator := .Paginate $pages site.Params.paginate.taxonomies }} 
        
        <ul>
          {{ range $paginator.Pages }}
            {{/* 获取当前页面对应的term计数 */}}
            {{ $termCount := index (index site.Taxonomies $titleLower) (lower .Title )| len }}
            <li>
              <a href="{{ .RelPermalink }}" class="{{- $titleLower }} box-shadow box-shadow-2">
                {{ .Title }} ({{ $termCount }})
              </a>
            </li>
          {{ end }}
        </ul>

    </div>

    {{ partial "pagination.html" . }}
    {{/* template "_internal/pagination.html" . */}}

  </div>
{{ end }}

需要注意的是,我这里的分页和内容的排序是自己写的,原因是:我想实现每个term(taxonomy的子对象),比如歌手这个分类的页面下有100个歌手,我想按照每个歌手的歌曲数量按照从多到少进行排序,然后每页就放20个歌手,多了就分页,所以,上边的代码就是按照这个来实现的。至于分页没有使用默认的分页,也是这个原因,另外就是我不想使用默认的分页样式。

比如: /en-us/artists/这个页面就对应歌手的分类页面。

9. 编辑 term.tml文件

比如刚才说的歌手的分类,下边有很多个歌手,比如一个是Zhou Shen,那么用户点击Zhou Shen这个标签后就跳转到:/en-us/artists/zhou-shen/,这个页面就该展示周深这位歌手的所有歌曲,这就是term,它是taxonomy下的一个对象。

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/term.html:

{{ define "main" }}
  
  {{/* 初始化变量 */}}
  {{ $parentTitleLower := .Parent.Title | lower }}
  {{ $currentLang := .Language.Lang }}
  
  {{/* 使用字典映射封面图片 */}}
  {{ $coverImages := dict
    "songs" site.Params.assets.songCover
    "artists" site.Params.assets.artistCover
    "lyricists" site.Params.assets.lyricistCover
    "composers" site.Params.assets.composerCover
  }}

  {{ $coverImage := index $coverImages $parentTitleLower }}
  {{ $imgAlt := printf "%s cover" $parentTitleLower }}

  {{/* 过滤当前语言页面 */}}
  {{ $filteredPages := where .Pages "Language.Lang" $currentLang }}

  {{/* 分页 */}}
  {{ $paginator := .Paginate $filteredPages.ByDate site.Params.paginate.songs }}

  <div class="term-container">
    <div class="term-info">


      <div class="cover">
        {{ partial "part/svg-img.html" (dict 
          "src" $coverImage
          "alt" $imgAlt
          "loading" "eager"
          "width" site.Params.svgImgSize.title)
        }}
      </div>


      <div class="title">
        <h1>{{- T (lower .Parent.Title) | strings.FirstUpper }}{{ ": " }}{{ .Title -}}</h1>
      </div>



    </div>

    <div class="term-lists">

      <ul>

        <li>
          <div class="song-info-term">

              <div class="song-title">

                  <span class="label-img">

                      {{ partial "part/svg-img.html" (dict 
                          "src" $coverImages.songs
                          "alt" "song thumb"
                          "loading" "eager"
                          "width" site.Params.svgImgSize.label)
                      }}

                  </span>

                  <div>
                      {{ T "songs" | strings.FirstUpper }}
                  </div>
              </div>

              {{- /* 定义要显示的taxonomy类型 */ -}}
              {{- $allTaxonomies := slice "artists" "lyricists" "composers" -}}
              {{- $counter := 0 -}}

              {{- range $taxonomy := $allTaxonomies -}}
                {{ if ne $taxonomy $parentTitleLower }}
                  {{- $counter = add $counter 1 -}}
                  {{- $coverImage := index $coverImages $taxonomy -}}
                  {{- $termClass := printf "term-%02d" $counter -}}

                  <div class="{{ $taxonomy }} {{ $termClass }}">
                    <span class="label-img">
                      {{ partial "part/svg-img.html" (dict 
                          "src" $coverImage
                          "alt" $taxonomy
                          "loading" "eager"
                          "width" site.Params.svgImgSize.label)
                      }}
                    </span>

                    <div>
                      {{ T $taxonomy | strings.FirstUpper }}
                    </div>
                  </div>
                {{ end }}
              {{- end -}}
          </div>
        </li>

        {{ range $paginator.Pages }}

          <li>

            <div class="song-info-term">

              <div class="song-title">
                  <a href="{{ .RelPermalink }}">
                      <p>{{- .LinkTitle -}}</p>
                  </a>
              </div>

              {{- /* 定义要显示的taxonomy类型 */ -}}
              {{- $allTaxonomies := slice "artists" "lyricists" "composers" -}}
              {{- $counter := 0 -}}
              {{ $page := . }}
              {{- range $allTaxonomies -}}

                  {{ if ne . $parentTitleLower }}
                    {{- $counter = add $counter 1 -}}
                    {{- $coverImage := index $coverImages . -}}
                    {{- $termClass := printf "term-%02d" $counter -}}

                        {{ $taxonomy := . }}
                        {{ $coverImage := index $coverImages . }}

                        {{ with $page.GetTerms . }}

                          <div class="{{- $taxonomy }} {{ $termClass -}}">
                              <ul>
                                  {{- range . -}}
                                      <li>
                                          <a href="{{ .RelPermalink }}">
                                              <p>{{- .LinkTitle -}}</p>
                                          </a>
                                      </li>
                                  {{- end -}}
                              </ul>
                          </div>

                        {{ end }}

                      {{ end }}
              {{- end -}}
            </div>

          </li>

        {{ end }}

      </ul>

    </div>
  </div>

  

  {{ partial "pagination.html" . }}


{{ end }}

10. 编辑home.html文件

首页,想要给用户展示网站能够提供的信息,所以是这样计划的:

  • 左侧展示20首最新收录的歌曲;
  • 右侧第一展示10位歌手;
  • 右侧第二展示10位作词;
  • 右侧第三展示10位作曲;

在手机端则改成上下结构。

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/home.html:

{{ define "main" }}

  {{ $coverImages := dict
    "songs" site.Params.assets.songCover
    "artists" site.Params.assets.artistCover
    "lyricists" site.Params.assets.lyricistCover
    "composers" site.Params.assets.composerCover
  }}

  {{- $allTaxonomies := slice "artists" "lyricists" "composers" -}}
  {{- $currentLang := .Lang -}}

  <div class="home">

    <div class="left">

      <div class="song-section">

        <div class="song-info">

          <div class="left-area">

            <a href="/{{- $currentLang -}}/songs/">

              <div class="cover">
                {{ partial "part/svg-img.html" (dict 
                  "src" site.Params.assets.songCover
                  "alt" "song cover"
                  "class" "song-cover"
                  "loading" "eager"
                  "width" site.Params.svgImgSize.title)
                }}
              </div>

            </a>

            <a href="/{{- .Lang -}}/songs/">

              <div class="title">
                <h2>{{- T "songs" | strings.FirstUpper -}}</h2>
              </div>

            </a>

          </div>

          

          <div class="right-area">

            <a href="/{{- $currentLang -}}/songs/">

              <div class="more">
                {{ partial "part/svg-img.html" (dict 
                  "src" site.Params.assets.moreHoriz
                  "alt" "song cover"
                  "class" "song-cover"
                  "loading" "eager"
                  "width" site.Params.svgImgSize.label)
                }}
              </div>

            </a>

          </div>
          

          
        </div>

        {{ $pages := first 20 site.RegularPages }}

        <div class="song-lists">

          <ul>

            <li>
              <div class="song-info-full">

                <div class="song-title">

                    <span class="label-img">

                        {{ partial "part/svg-img.html" (dict 
                            "src" $coverImages.songs 
                            "alt" "song thumb"
                            "loading" "eager"
                            "width" site.Params.svgImgSize.label)
                        }}

                    </span>

                    <div>
                        <p>{{- T "songs" | strings.FirstUpper -}}</p>
                    </div>
                </div>

                {{- /* 定义要显示的taxonomy类型 */ -}}
                {{- $allTaxonomies := slice "artists" "lyricists" "composers" -}}
                
                {{- range $allTaxonomies -}}
                    {{ $taxonomy := . -}}
                    {{ $coverImage := index $coverImages . }}
                        <div class="{{- . -}}">
                            <span class="label-img">
                                {{ partial "part/svg-img.html" (dict 
                                    "src" $coverImage
                                    "alt" "Description of image"
                                    "class" "additional-class"
                                    "loading" "eager"
                                    "width" site.Params.svgImgSize.label)
                                }}
                            </span>

                            <div>
                              <p>{{- T $taxonomy | strings.FirstUpper -}}</p>
                            </div>
                        </div>

                {{- end -}}
              </div>
            </li>

            {{ range $pages }}
          
              <li>
                {{ partial "part/link-song-info-full.html" . }}
              </li>

            {{ end }}

          </ul>

        </div>

      </div>
    </div>


    <div class="right">

      {{ range $allTaxonomies }}

        {{ $currentTaxonomy := . }}
        {{ $imgAlt := printf "%s cover" . }}
        {{ $coverImage := index $coverImages . }}

        <div class="taxonomy-container {{ . -}}">

          <div class="header">

            <div class="left-area">

              {{- $coverImage := index $coverImages . -}}
              {{- $imgAlt := printf "%s cover" . -}}

              <a href="/{{- $currentLang -}}/{{- . -}}/">

                <div class="cover">
                  {{ partial "part/svg-img.html" (dict 
                    "src" $coverImage
                    "alt" $imgAlt
                    "loading" "eager"
                    "width" site.Params.svgImgSize.title)
                  }}
                </div>

              </a>
              
              <a href="/{{- $currentLang -}}/{{- . -}}/">

                <div class="title">
                  <h2>{{ T . | strings.FirstUpper }}</h2>
                </div>

              </a>

            </div>

            <div class="right-area">

              <a href="/{{- $currentLang -}}/{{- . -}}/">

                <div class="more">
                  {{ partial "part/svg-img.html" (dict 
                    "src" site.Params.assets.moreHoriz
                    "alt" $imgAlt
                    "loading" "eager"
                    "width" site.Params.svgImgSize.label)
                  }}
                </div>

              </a>
            </div>

            

            

          </div>

          <div class="content">

            {{ with index site.Taxonomies .}}
              <ul>
                {{ range first 10 .ByCount }} 
                  <li>
                    <a href="{{ .Page.RelPermalink }}" class="{{- $currentTaxonomy }} box-shadow box-shadow-2">
                      {{ .Page.Title }} ({{ .Count }})
                    </a>
                  </li>
                {{ end }}
              </ul>
            {{ end }}

          </div>

        </div>

      {{ end }}

    </div>
     
  </div>

{{ end }}

11. 编辑404.html文件

没有的网页,或者用户输入的链接有错之类的,反正就是网页不存在的情况下,展示给用户的界面.

文件位置:/Users/xdl/Documents/github/exactlyrics/themes/lyrics/layouts/404.html

{{ define "main" }}

    <div class="page-not-found">

        <h1>{{- T "404-title" -}}</h1>
        <p>{{- T "404-content" -}}</p>
        <p>
            <a href="{{ .Site.Home.RelPermalink }}">
            <h2>{{- T "404-back-to-home" -}}</h2>
            </a>
        </p>

    </div>
  
{{ end }}

至此,我们完成了所有的架构工作,接下来要做的就是给网站填充内容了。

3. 增加网站内容

整体而言,通过程序实现,手工实现一个是慢,一个是容易出错,另外如果有变动的话,手动改都不见的能够全部改好,所以最后用程序实现。

说明一下,相关的python程序我都放在本地的/Users/xdl/Documents/github/sqids下边。

1. 数据收集整理

之前我们说过在/Users/xdl/Documents/github/exactlyrics/data/songs.yaml中准备数据,内容类似:

songs:
 HpOnhRDfQ7:
   title: 星月落
   number:
   - 1
   - 1
   artists:
   - 浮生梦
   lyricists:
   - 萧燃
   composers:
   - 萧燃
   originalArtist: null
   originalLang: zh-hans
   cover: https://lh3.googleusercontent.com/Fpw8v8z6IrYgf205vR8yToF98icxF0lB2xMnXV0slHp1zTR1_S2ZWH-zTaY2ts5A_1_DMOBg-DB-DvsGNg
   youtubeVideo: a-vaqsEpATo
   lyrics:
     txt:
     - zh-hans
     - zh-hant
     lrc:
     - zh-hans
     - zh-hant
     srt:
     - zh-hans
     - zh-hant
 Tjrf2MTkRO:
   title: 谪仙
   number:
   - 1
   - 2
   artists:
   - 叶里
   lyricists:
   - 王莹
   composers:
   - 王中易
   originalArtist: null
   originalLang: zh-hans
   cover: https://lh3.googleusercontent.com/UFEI94VPLJ2KBSIIMmNMbcWlxcrG4rKmPV3c0T6NBXC0opVe1TXr2cjQqXJOscFJcPVh4UrCreK7_BdB_w
   youtubeVideo: Y1ri361Hx1Q
   lyrics:
     txt:
     - zh-hans
     - zh-hant
     lrc:
     - zh-hans
     - zh-hant
     srt:
     - zh-hans
     - zh-hant

因为歌曲的数据每首都不一样,所以我目前是手动完成的,不过我也写了个程序用来把通用的部分自动写上。

1. 生成数列

首先在terminal(mac的zsh)中:

for i in {1..2000}; do echo "[1, $i]"; done > input.txt

用这个命令生成一个input.txt文件,文件内容类似:

[1, 1]
[1, 2]
[1, 3]
[1, 4]
[1, 5]
[1, 6]
...
[1, 1999]
[1, 2000]

2. 生成数列对应的序列号

使用sqid的python版,也就是利用数字生成序列号,名字为run.py,关于这一点,可以参看从数字生成短的唯一标识符:

import argparse
from sqids import Sqids

def main():
    # 初始化 Sqids(修正后的字母表)
    sqids = Sqids(
        min_length=10,
        alphabet="F69xnXMkBNcuhs1AvjW3Co7l2RePyY8DwaU0TztfHQrqSVKdpi4mLGIJOgb5ZE",
        blocklist={"fuck", "shit"}  # 可选:屏蔽不雅词
    )

    # 设置命令行参数解析
    parser = argparse.ArgumentParser(
        description="Sqids 编解码工具 (min_length=10, 自定义字母表)",
        formatter_class=argparse.RawTextHelpFormatter
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("-e", "--encode", nargs="+", type=int, 
                      help="编码一个或多个数字(例如:-e 100 或 -e 100 200 300)")
    group.add_argument("-d", "--decode", type=str, 
                      help="解码 Sqid 字符串(例如:-d 'XMjW3Co7l2')")
    args = parser.parse_args()

    # 编码或解码
    if args.encode:
        try:
            id = sqids.encode(args.encode)
            print(id)  # 直接输出编码结果
        except Exception as e:
            print(f"Error: {str(e)}")
    elif args.decode:
        try:
            numbers = sqids.decode(args.decode)
            if numbers:
                print(numbers[0] if len(numbers) == 1 else numbers)  # 直接输出解码结果
            else:
                print("Invalid Sqid")
        except Exception as e:
            print(f"Error: {str(e)}")

if __name__ == "__main__":
    main()

这是以前写的程序,所以现在需要做的是就是读取input.txt的内容然后不断执行run.py,在这里就是2000次。所以我们写个程序generate_sqids.py

# save as process.py
import subprocess

with open("input.txt", "r") as f_in, open("output.txt", "w") as f_out:
    for line in f_in:
        line = line.strip()
        if not line:
            continue
        
        # 提取 x 和 y(例如 "[123, 456]" → x=123, y=456)
        x, y = map(int, line.strip("[]").split(", "))
        
        # 运行命令并获取输出
        result = subprocess.check_output(
            ["python", "run.py", "-e", str(x), str(y)],
            text=True
        ).strip()
        
        # 写入结果
        f_out.write(f"{line} -- {result}\n")
        

print("Done! Results saved to output.txt.")

然后在zsh中执行:

python generate_sqids.py

之后就得到了output.txt:

[1, 1] -- HpOnhRDfQ7
[1, 2] -- Tjrf2MTkRO
[1, 3] -- tHajrqHKmd
[1, 4] -- P1CbowGyEf
...
[1, 1999] -- mUtS5z7gwy
[1, 2000] -- jgQiUOMowO

这样,因为我们在songs.yaml中给每首歌都有对应的数字列表(number),那么现在就可以得到每首歌的序列号。

3. 增加songs.yaml的通用数据

一个python程序update_songs_yaml.py:

import yaml
from pathlib import Path
import argparse

def parse_song_entries(txt_file):
    """解析output.txt文件,返回歌曲条目字典"""
    entries = {}
    with open(txt_file, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            # 解析格式如 "[1, 1] -- HpOnhRDfQ7"
            if '--' in line:
                number_part, song_id = line.split('--', 1)
                song_id = song_id.strip()
                number_part = number_part.strip()
                # 解析数字部分如 "[1, 1]"
                try:
                    number = eval(number_part)  # 安全风险提示:实际使用中应考虑更安全的解析方式
                    entries[song_id] = number
                except:
                    print(f"无法解析行: {line}")
                    continue
    return entries

def update_songs_yaml(yaml_file, new_entries, start=None, end=None):
    """更新songs.yaml文件,添加新的歌曲条目"""
    # 读取现有的YAML文件
    if yaml_file.exists():
        with open(yaml_file, 'r', encoding='utf-8') as f:
            data = yaml.safe_load(f) or {}
    else:
        data = {}
    
    songs = data.get('songs', {})
    
    # 添加新条目
    added_count = 0
    for song_id, number in new_entries.items():
        # 检查范围
        if start is not None and number < start:
            continue
        if end is not None and number > end:
            continue
        
        # 如果条目不存在,则添加
        if song_id not in songs:
            songs[song_id] = {
                'title': '',
                'number': number,
                'artists': [],
                'lyricists': [],
                'composers': [],
                'originalArtist': '',
                'originalLang': 'zh-hans',
                'cover': '',
                'youtubeVideo': '',
                'lyrics': {
                    'txt': ['zh-hans', 'zh-hant'],
                    'lrc': ['zh-hans', 'zh-hant'],
                    'srt': ['zh-hans', 'zh-hant']
                }
            }
            added_count += 1
    
    # 如果有添加新条目,则保存文件
    if added_count > 0:
        data['songs'] = songs
        with open(yaml_file, 'w', encoding='utf-8') as f:
            yaml.dump(data, f, allow_unicode=True, sort_keys=False)
        print(f"已添加 {added_count} 个新条目到 {yaml_file}")
    else:
        print("没有新条目需要添加")

def main():
    # 设置命令行参数
    parser = argparse.ArgumentParser(description='根据output.txt更新songs.yaml')
    parser.add_argument('start', nargs='?', type=int, help='起始编号')
    parser.add_argument('end', nargs='?', type=int, help='结束编号')
    args = parser.parse_args()
    
    # 文件路径
    current_dir = Path.cwd()
    txt_file = current_dir / 'output.txt'
    yaml_file = Path('/Users/xdl/Documents/github/exactlyrics/data/songs.yaml')
    
    # 解析歌曲条目
    entries = parse_song_entries(txt_file)
    
    # 准备范围参数
    start_range = None
    end_range = None
    if args.start is not None:
        start_range = [1, args.start]
    if args.end is not None:
        end_range = [1, args.end]
    
    # 更新YAML文件
    update_songs_yaml(yaml_file, entries, start_range, end_range)

if __name__ == "__main__":
    main()

这个程序的目的是生成通用的部分,比如我现在要生成[1, 93]到[1, 95]这三首歌的通用数据,那么在zsh中:

(venv) xdl@MacBook-Air ~/Documents/github/sqids % python update_songs_yaml.py 93 95
已添加 3 个新条目到 /Users/xdl/Documents/github/exactlyrics/data/songs.yaml

可以看到/Users/xdl/Documents/github/exactlyrics/data/songs.yaml的最后三个就是:

rQo7FUSeDQ:
  title: ''
  number:
  - 1
  - 93
  artists: []
  lyricists: []
  composers: []
  originalArtist: ''
  originalLang: zh-hans
  cover: ''
  youtubeVideo: ''
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant
qYdsPwNaZJ:
  title: ''
  number:
  - 1
  - 94
  artists: []
  lyricists: []
  composers: []
  originalArtist: ''
  originalLang: zh-hans
  cover: ''
  youtubeVideo: ''
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant
NI5y4jHNk8:
  title: ''
  number:
  - 1
  - 95
  artists: []
  lyricists: []
  composers: []
  originalArtist: ''
  originalLang: zh-hans
  cover: ''
  youtubeVideo: ''
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant

4. 在songs.yaml中手动填充歌曲数据

这样,我接下来要做的就是把三首歌的数据手动填充一下:

rQo7FUSeDQ:
  title: 心事
  number:
  - 1
  - 93
  artists: 
  - 周深
  lyricists: 
  - 冉然
  composers: 
  - 杨礼
  - 蓝天晓
  originalArtist: ''
  originalLang: zh-hans
  cover: ''
  youtubeVideo: DS63MyJovDI
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant
qYdsPwNaZJ:
  title: 海市蜃楼
  number:
  - 1
  - 94
  artists: 
  - 三叔说
  lyricists: 
  - 张志宇
  - 智伟彪
  composers: 
  - 宋子楚
  originalArtist: ''
  originalLang: zh-hans
  cover: https://lh3.googleusercontent.com/YN7dDJUEDbfVUeROuk3qpPjnPL-Vq4Y6spzlf0iXdn7x5zhQAEYXBq1lbWA8jy653PDiwjaGArxyTjQ
  youtubeVideo: iB_AN1pnXPE
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant
NI5y4jHNk8:
  title: One Night In Shanghai
  number:
  - 1
  - 95
  artists: 
  - 胡彦斌
  lyricists: 
  - 宁财神
  composers: 
  - 胡彦斌
  originalArtist: ''
  originalLang: zh-hans
  cover: https://lh3.googleusercontent.com/VmI72HKkDkcoL_gIQWncR2eho_66QKDBp4q7rT83Z_UO16fFNCVrqI9Zgu50H2WsSya3kZxIcbLtyV82
  youtubeVideo: 1Yyr0BVFeH0
  lyrics:
    txt:
    - zh-hans
    - zh-hant
    lrc:
    - zh-hans
    - zh-hant
    srt:
    - zh-hans
    - zh-hant

5. 新增歌曲对应的歌词文件夹

然后我需要创建这三个数据对应的歌词文件夹用来储存歌词文件,写个程序create_lyrics_folders.py:

import os
import yaml
from pathlib import Path

def create_lyrics_folders(songs_yaml_path, base_lyrics_dir):
    """根据songs.yaml创建歌词文件夹"""
    # 读取YAML文件
    with open(songs_yaml_path, 'r', encoding='utf-8') as file:
        data = yaml.safe_load(file)
    
    if not data or 'songs' not in data:
        print("songs.yaml文件格式不正确或没有歌曲数据")
        return
    
    # 确保基础目录存在
    base_lyrics_dir.mkdir(parents=True, exist_ok=True)
    
    created_count = 0
    skipped_count = 0
    
    # 为每首歌曲创建文件夹
    for song_id in data['songs'].keys():
        song_dir = base_lyrics_dir / song_id
        
        if song_dir.exists():
            print(f"文件夹已存在,跳过: {song_dir}")
            skipped_count += 1
        else:
            song_dir.mkdir()
            print(f"已创建文件夹: {song_dir}")
            created_count += 1
    
    print(f"\n操作完成: 创建了 {created_count} 个新文件夹,跳过了 {skipped_count} 个已存在的文件夹")

def main():
    # 设置文件路径
    songs_yaml_path = Path("/Users/xdl/Documents/github/exactlyrics/data/songs.yaml")
    base_lyrics_dir = Path("/Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics")
    
    # 创建歌词文件夹
    create_lyrics_folders(songs_yaml_path, base_lyrics_dir)

if __name__ == "__main__":
    main()

然后执行程序:

(venv) xdl@MacBook-Air ~/Documents/github/sqids % python create_lyrics_folders.py
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnhRDfQ7
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Tjrf2MTkRO
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHajrqHKmd
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/P1CbowGyEf
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/FR1MIVpWYl
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/6TGqz5oIeh
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/LP30oPzRKg
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdxwNaZJF
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq7CspNn8E
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/K6wrj18tIp
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/EmX3i3DecB
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/vANsEECW6h
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7Hlcqu9Xz
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/MsRd9TtPvG
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtpz7gwyT
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/jgQhOMowOd
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/lhn1mWNb7s
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/RDuDKBGw75
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/JFPKH5YPsL
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/3CJTPt6X1y
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d891eR0JlC
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Y38ibjBerG
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq7nspNn8E
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/hKbOp8CRXa
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d896eR0JlC
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/XtxfhBbiTE
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/A2IqBwYk2r
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOWhRDfQ7
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOUQMdkWj4
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/cz6VOjBXK6
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo2USeDQE
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdvwNaZJF
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5AjHNk8D
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/IVSCsb5hk1
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/GrzagaI3H9
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/QvecroD9Vz
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/7dpMm9Pznc
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/oejdLCQZFt
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHa8rqHKmd
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/nbiA8xKa6p
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtxz7gwyT
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/1uM0uxrcLu
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/7dpBm9Pznc
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/p9q2gPK31d
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOUPMdkWj4
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/zoTGSc9Q2V
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/g5v9RjurBc
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/ZcK83GYTXO
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/uysWTyTwU5
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7HWcqu9Xz
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/9LY1bjg0b2
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/xamzai0Eje
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/iZhJwpxEu0
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/5SA8QLxpRW
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/CMFbiy896i
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Sk2ZHgZ2nu
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/8JLz1kD2KY
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/bwZ5kJHCQX
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/p9qmgPK31d
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/wGcDVo8U6J
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/eNgsfSiaVv
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/2BVooOzwSI
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnbhRDfQ
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Tjrur2MTkR
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHaMRrqHKm
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/P1CA9owGyE
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/FR1A6IVpWY
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/6TGYXz5oIe
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/LP3SNoPzRK
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsdwNaZJ
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq78UspNn8
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/K6wfAj18tI
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/EmXy1i3Dec
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/vANmyEECW6
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7HQocqu9X
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/MsRng9TtPv
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtXKz7gwy
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/jgQqXOMowO
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/lhn69mWNb7
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/RDuZFKBGw7
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/JFPs9H5YPs
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/3CJq4Pt6X1
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d89v3eR0Jl
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Y38m0bjBer
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq78jspNn8
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/hKb0Pp8CRX
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d89vqeR0Jl
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/XtxbZhBbiT
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/A2IGnBwYk2
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnuhRDfQ
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOU3lMdkWj
文件夹已存在,跳过: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/cz62bOjBXK
已创建文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ
已创建文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ
已创建文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8

操作完成: 创建了 3 个新文件夹,跳过了 92 个已存在的文件夹

它会跳过已经存在的文件夹,然后创建还没有的文件夹,在这里就是我刚创建的三个。

6. 手动录入歌词文件

把我整理后的歌词文件分别放入各首歌曲对应的文件夹中(可以使用查询功能快速对应),每个文件夹下我只需要放一个zh-hans.srt文件。

这里用一个举例:

xdl@MacBook-Air ~/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ % tree
.
└── zh-hans.srt

1 directory, 1 file

7. 自动生成所有歌词的所有格式。

写个程序,生成每首歌曲对应的开头所说的:

  • zh-hans.srt
  • zh-hans.lrc
  • zh-hans.txt
  • zh-hant.srt
  • zh-hant.lrc
  • zh-hant.txt

事实上,不管放入的是中文简体的还是繁体的,抑或是srt文件,或lrc文件,它都会生成其他的五个文件。

当然,后期我会改进这个程序,因为目前如果你放入一个en-us.srt文件,它会自动生成en-us.lrcen-us.txt,但不会自动复制内容生成对应的zh-hans.*zh-hant.*. 目前保持现状也是有原因的,因为有时候就是需要翻译过后的文件,所以后期再改进,而且有必要这样改,因为复制总是对的,大不了有了新的重新覆盖就好。

程序名称generate_all_lrc.py:

import os
import subprocess
from pathlib import Path

# 需要的文件组合
required_files = [
    'zh-hans.srt',
    'zh-hans.lrc',
    'zh-hans.txt',
    'zh-hant.srt',
    'zh-hant.lrc',
    'zh-hant.txt'
]

def convert_simplified_to_traditional(input_file, output_file):
    """使用opencc进行简繁转换"""
    try:
        subprocess.run(['opencc', '-i', input_file, '-o', output_file, '-c', 's2t.json'], check=True)
        return True
    except subprocess.CalledProcessError as e:
        print(f"简繁转换失败: {e}")
        return False
    except FileNotFoundError:
        print("未找到opencc命令,请确保已安装opencc")
        return False

def process_folder(folder_path):
    """处理单个文件夹"""
    files = os.listdir(folder_path)
    
    # 检查是否有任何文件存在
    if not any(f.endswith(('.srt', '.lrc', '.txt')) for f in files):
        print(f"文件夹 {folder_path} 中没有歌词文件,跳过")
        return
    
    # 首先处理简体中文文件
    source_srt = os.path.join(folder_path, 'zh-hans.srt')
    source_lrc = os.path.join(folder_path, 'zh-hans.lrc')
    source_txt = os.path.join(folder_path, 'zh-hans.txt')
    
    # 尝试从现有文件生成简体中文的完整集合
    if 'zh-hans.srt' in files:
        # 从srt生成其他简体文件
        if 'zh-hans.lrc' not in files:
            subprocess.run([
                'python3', 'lyric_converter.py', 
                source_srt, 
                os.path.join(folder_path, 'zh-hans.lrc'),
                'srt', 'lrc'
            ])
        
        if 'zh-hans.txt' not in files:
            subprocess.run([
                'python3', 'lyric_converter.py', 
                source_srt, 
                os.path.join(folder_path, 'zh-hans.txt'),
                'srt', 'txt'
            ])
    
    elif 'zh-hans.lrc' in files:
        # 从lrc生成其他简体文件
        if 'zh-hans.srt' not in files:
            subprocess.run([
                'python3', 'lyric_converter.py', 
                source_lrc, 
                os.path.join(folder_path, 'zh-hans.srt'),
                'lrc', 'srt'
            ])
        
        if 'zh-hans.txt' not in files:
            subprocess.run([
                'python3', 'lyric_converter.py', 
                source_lrc, 
                os.path.join(folder_path, 'zh-hans.txt'),
                'lrc', 'txt'
            ])
    
    # 现在处理繁体中文文件
    # 首先检查是否已经有繁体文件存在
    if not any(f.startswith('zh-hant.') for f in files):
        # 如果没有繁体文件,从简体文件生成
        if os.path.exists(source_srt):
            convert_simplified_to_traditional(
                source_srt,
                os.path.join(folder_path, 'zh-hant.srt')
            )
        if os.path.exists(source_lrc):
            convert_simplified_to_traditional(
                source_lrc,
                os.path.join(folder_path, 'zh-hant.lrc')
            )
        if os.path.exists(source_txt):
            convert_simplified_to_traditional(
                source_txt,
                os.path.join(folder_path, 'zh-hant.txt')
            )
    else:
        # 如果已经有部分繁体文件,补充缺失的
        if 'zh-hant.srt' in files:
            if 'zh-hant.lrc' not in files:
                subprocess.run([
                    'python3', 'lyric_converter.py', 
                    os.path.join(folder_path, 'zh-hant.srt'), 
                    os.path.join(folder_path, 'zh-hant.lrc'),
                    'srt', 'lrc'
                ])
            
            if 'zh-hant.txt' not in files:
                subprocess.run([
                    'python3', 'lyric_converter.py', 
                    os.path.join(folder_path, 'zh-hant.srt'), 
                    os.path.join(folder_path, 'zh-hant.txt'),
                    'srt', 'txt'
                ])
        
        elif 'zh-hant.lrc' in files:
            if 'zh-hant.srt' not in files:
                subprocess.run([
                    'python3', 'lyric_converter.py', 
                    os.path.join(folder_path, 'zh-hant.lrc'), 
                    os.path.join(folder_path, 'zh-hant.srt'),
                    'lrc', 'srt'
                ])
            
            if 'zh-hant.txt' not in files:
                subprocess.run([
                    'python3', 'lyric_converter.py', 
                    os.path.join(folder_path, 'zh-hant.lrc'), 
                    os.path.join(folder_path, 'zh-hant.txt'),
                    'lrc', 'txt'
                ])

def main():
    base_dir = '/Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/'
    
    # 遍历所有子文件夹
    for root, dirs, files in os.walk(base_dir):
        # 跳过根目录,只处理子文件夹
        if root == base_dir:
            continue
        
        print(f"处理文件夹: {root}")
        process_folder(root)

if __name__ == "__main__":
    main()

这个程序使用了一个中间程序lyric_converter.py:

import re
from datetime import datetime, timedelta

def srt_to_lrc(srt_content):
    lines = srt_content.split('\n')
    lrc_lines = []
    i = 0
    
    while i < len(lines):
        line = lines[i].strip()
        if '-->' in line:
            start_time = line.split('-->')[0].strip()
            # 处理时间格式
            if ',' in start_time:
                time_format = '%H:%M:%S,%f'
            else:
                time_format = '%H:%M:%S.%f'
            
            try:
                start_time_obj = datetime.strptime(start_time, time_format)
                lrc_time = f"[{start_time_obj.strftime('%M:%S.%f')[:-3]}]"
                
                # 收集歌词内容(直到遇到空行或序号)
                lyrics = []
                i += 1
                while i < len(lines) and lines[i].strip() and not lines[i].strip().isdigit():
                    lyrics.append(lines[i].strip())
                    i += 1
                
                if lyrics:
                    lrc_lines.append(f"{lrc_time}{' '.join(lyrics)}")
            except ValueError:
                i += 1
        else:
            i += 1
    
    return '\n'.join(lrc_lines)

def lrc_to_srt(lrc_content):
    lines = [line.strip() for line in lrc_content.split('\n') if line.strip()]
    srt_lines = []
    time_lyrics = []
    
    # 首先解析所有时间点和歌词
    for line in lines:
        # 支持 [mm:ss.xx] 和 [mm:ss] 格式
        time_match = re.match(r'\[(\d+):(\d+)(?:\.(\d+))?\]', line)
        if time_match:
            minutes, seconds, milliseconds = time_match.groups()
            milliseconds = milliseconds or '0'
            milliseconds = milliseconds.ljust(3, '0')[:3]
            
            time_str = f"00:{minutes}:{seconds},{milliseconds}"
            try:
                time_obj = datetime.strptime(f"00:{minutes}:{seconds}.{milliseconds}", '%H:%M:%S.%f')
                lyric = line[time_match.end():].strip()
                if lyric:  # 忽略空歌词行
                    time_lyrics.append((time_obj, time_str, lyric))
            except ValueError:
                continue
    
    # 生成SRT内容
    for i in range(len(time_lyrics)):
        counter = i + 1
        time_obj, time_str, lyric = time_lyrics[i]
        
        # 确定结束时间
        if i < len(time_lyrics) - 1:
            # 不是最后一句,结束时间是下一句的开始时间
            end_time_str = time_lyrics[i+1][1]
        else:
            # 最后一句,结束时间是开始时间+5秒
            end_time_obj = time_obj + timedelta(seconds=5)
            end_time_str = end_time_obj.strftime('%H:%M:%S,%f')[:-3]
        
        srt_lines.append(str(counter))
        srt_lines.append(f"{time_str} --> {end_time_str}")
        srt_lines.append(lyric)
        srt_lines.append('')
    
    return '\n'.join(srt_lines)

def to_txt(content, input_format):
    if input_format == 'srt':
        lines = content.split('\n')
        txt_lines = []
        for line in lines:
            if line.strip() and not line.strip().isdigit() and '-->' not in line:
                txt_lines.append(line.strip())
        return '\n'.join(txt_lines)
    elif input_format == 'lrc':
        lines = content.split('\n')
        txt_lines = []
        for line in lines:
            time_match = re.match(r'\[(\d+):(\d+)(?:\.(\d+))?\]', line)
            if time_match:
                lyric = line[time_match.end():].strip()
                if lyric:
                    txt_lines.append(lyric)
        return '\n'.join(txt_lines)
    else:
        return content

def convert_file(input_file, output_file, input_format, output_format):
    try:
        with open(input_file, 'r', encoding='utf-8') as f:
            content = f.read()
    except UnicodeDecodeError:
        with open(input_file, 'r', encoding='gbk') as f:
            content = f.read()
    
    if input_format == 'srt' and output_format == 'lrc':
        converted = srt_to_lrc(content)
    elif input_format == 'lrc' and output_format == 'srt':
        converted = lrc_to_srt(content)
    elif output_format == 'txt':
        converted = to_txt(content, input_format)
    else:
        raise ValueError(f"Unsupported conversion: {input_format} to {output_format}")
    
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(converted)

if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description='Convert between lyric file formats (SRT, LRC, TXT)')
    parser.add_argument('input_file', help='Input file path')
    parser.add_argument('output_file', help='Output file path')
    parser.add_argument('input_format', choices=['srt', 'lrc', 'txt'], help='Input file format')
    parser.add_argument('output_format', choices=['srt', 'lrc', 'txt'], help='Output file format')
    
    args = parser.parse_args()
    
    try:
        convert_file(args.input_file, args.output_file, args.input_format, args.output_format)
        print(f"Conversion successful: {args.input_file} -> {args.output_file}")
    except Exception as e:
        print(f"Error during conversion: {str(e)}")

分开是有原因的,lyric_converter.py这个程序也可以应用在其他地方。

总之,执行命令:

(venv) xdl@MacBook-Air ~/Documents/github/sqids % python generate_all_lrc.py
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOWhRDfQ7
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHa8rqHKmd
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7Hlcqu9Xz
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7HWcqu9Xz
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/P1CbowGyEf
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/JFPKH5YPsL
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/XtxbZhBbiT
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq78jspNn8
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/ZcK83GYTXO
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnhRDfQ7
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtxz7gwyT
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/7dpBm9Pznc
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/6TGYXz5oIe
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/MsRd9TtPvG
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/K6wfAj18tI
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Tjrf2MTkRO
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/EmX3i3DecB
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/8JLz1kD2KY
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo2USeDQE
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq7CspNn8E
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHaMRrqHKm
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/jgQhOMowOd
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/CMFbiy896i
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/FR1A6IVpWY
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/5SA8QLxpRW
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnbhRDfQ
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/p9qmgPK31d
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/p9q2gPK31d
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/lhn69mWNb7
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/wGcDVo8U6J
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/A2IqBwYk2r
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq78UspNn8
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/9LY1bjg0b2
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8/zh-hans.lrc
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5y4jHNk8/zh-hans.txt
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/hKb0Pp8CRX
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdvwNaZJF
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/P1CA9owGyE
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/3CJq4Pt6X1
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Tjrur2MTkR
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/LP30oPzRKg
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/nbiA8xKa6p
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/XtxfhBbiTE
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mX3i3DecB
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/2BVooOzwSI
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/RDuDKBGw75
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/U7HQocqu9X
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/FR1MIVpWYl
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/LP3SNoPzRK
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdxwNaZJF
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/EmXy1i3Dec
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/HpOnuhRDfQ
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Oq7nspNn8E
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/uysWTyTwU5
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/cz62bOjBXK
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/eNgsfSiaVv
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/vANsEECW6h
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/IVSCsb5hk1
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/MsRng9TtPv
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/1uM0uxrcLu
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/K6wrj18tIp
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/tHajrqHKmd
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOUPMdkWj4
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d891eR0JlC
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/7dpMm9Pznc
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/QvecroD9Vz
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/jgQqXOMowO
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d89v3eR0Jl
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/lhn1mWNb7s
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/6TGqz5oIeh
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/vANmyEECW6
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOU3lMdkWj
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/oejdLCQZFt
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ/zh-hans.lrc
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ/zh-hans.txt
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtXKz7gwy
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/iZhJwpxEu0
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/g5v9RjurBc
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/GrzagaI3H9
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/A2IGnBwYk2
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/3CJTPt6X1y
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Y38m0bjBer
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d896eR0JlC
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Sk2ZHgZ2nu
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/d89vqeR0Jl
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/mUtpz7gwyT
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ/zh-hans.lrc
Conversion successful: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ/zh-hans.srt -> /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/rQo7FUSeDQ/zh-hans.txt
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/RDuZFKBGw7
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/cz6VOjBXK6
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/hKbOp8CRXa
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/NI5AjHNk8D
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/Y38ibjBerG
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/zoTGSc9Q2V
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/bwZ5kJHCQX
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/xamzai0Eje
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/JFPs9H5YPs
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/qYdsdwNaZJ
处理文件夹: /Users/xdl/Documents/github/exactlyrics/assets/songs/lyrics/aOUQMdkWj4

可以看到有三个文件夹被处理过了,还是以上边的例子继续:

xdl@MacBook-Air ~/Documents/github/exactlyrics/assets/songs/lyrics/qYdsPwNaZJ % tree
.
├── zh-hans.lrc
├── zh-hans.srt
├── zh-hans.txt
├── zh-hant.lrc
├── zh-hant.srt
└── zh-hant.txt

1 directory, 6 files

好了,对应的歌词文件就都有了。

8. 自动生成歌曲的md文档

hugo的内容是需要填充的,所以每首歌曲对应的网页必须要有一个文件对应,我们需要创建这个文件,在这里是对应的md文件,因为目前网站有三种语言,这就意味着我需要在en-us,zh-hans,zh-hant下分别创建一个md文件给同一首歌曲,而且每个md文件的内容需要适配语种。一步步来,先生成md文档。

写个程序,用来生成md文档,程序名称create_song_pages.py:

import os
import yaml
from pathlib import Path
import subprocess
from datetime import datetime

def convert_to_traditional(text):
    """使用opencc将简体中文转换为繁体中文"""
    try:
        # 创建一个临时文件
        temp_file = Path('/tmp/temp_simplified.txt')
        temp_file.write_text(text, encoding='utf-8')
        
        # 转换文件
        subprocess.run(['opencc', '-i', str(temp_file), '-o', '/tmp/temp_traditional.txt', '-c', 's2t.json'], check=True)
        
        # 读取转换后的内容
        traditional_text = Path('/tmp/temp_traditional.txt').read_text(encoding='utf-8')
        
        # 清理临时文件
        temp_file.unlink()
        Path('/tmp/temp_traditional.txt').unlink()
        
        return traditional_text
    except Exception as e:
        print(f"简繁转换失败: {e}")
        return text  # 如果转换失败,返回原始文本

def build_lyrics_structure(song_id, lyrics_langs):
    """根据简化的lyrics结构构建完整的lyrics数据结构"""
    lyrics = {
        'txt': {},
        'lrc': {},
        'srt': {}
    }
    
    for format in ['txt', 'lrc', 'srt']:
        if format in lyrics_langs:
            for lang in lyrics_langs[format]:
                lyrics[format][lang] = f"songs/lyrics/{song_id}/{lang}.{format}"
    
    return lyrics

def create_song_files(songs_data, base_dir):
    """根据歌曲数据创建多语言Markdown文件"""
    for song_id, song_data in songs_data.items():
        # 准备基础数据
        title = song_data['title']
        number = song_data['number']
        artists = song_data['artists']
        lyricists = song_data['lyricists']
        composers = song_data['composers']
        original_artist = song_data.get('originalArtist', '')
        original_lang = song_data['originalLang']
        cover = song_data['cover']
        youtube_video = song_data['youtubeVideo']
        lyrics_langs = song_data['lyrics']
        
        # 构建完整的lyrics数据结构
        lyrics = build_lyrics_structure(song_id, lyrics_langs)
        
        # 当前日期
        current_date = datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00")
        
        # 为每种语言创建文件
        for lang in ['en-us', 'zh-hans', 'zh-hant']:
            # 确定文件路径
            if lang == 'en-us':
                file_path = Path(base_dir) / 'content' / 'en-us' / 'songs' / f"{song_id}.md"
            elif lang == 'zh-hans':
                file_path = Path(base_dir) / 'content' / 'zh-hans' / 'songs' / f"{song_id}.md"
            else:
                file_path = Path(base_dir) / 'content' / 'zh-hant' / 'songs' / f"{song_id}.md"
            
            # 如果文件已存在,跳过
            if file_path.exists():
                print(f"文件已存在,跳过: {file_path}")
                continue
            
            # 确保目录存在
            file_path.parent.mkdir(parents=True, exist_ok=True)
            
            # 根据语言处理数据
            if lang == 'zh-hant':
                # 繁体中文需要转换
                processed_title = convert_to_traditional(title)
                processed_artists = [convert_to_traditional(a) for a in artists]
                processed_lyricists = [convert_to_traditional(l) for l in lyricists]
                processed_composers = [convert_to_traditional(c) for c in composers]
                if original_artist:
                    if isinstance(original_artist, list):
                        processed_original_artist = [convert_to_traditional(o) for o in original_artist]
                    else:
                        processed_original_artist = convert_to_traditional(original_artist)
                else:
                    processed_original_artist = original_artist
            else:
                # 简体中文和英文保持原样
                processed_title = title
                processed_artists = artists
                processed_lyricists = lyricists
                processed_composers = composers
                processed_original_artist = original_artist
            
            # 构建YAML前端内容
            front_matter = {
                'date': current_date,
                'title': processed_title,
                'number': number,
                'artists': processed_artists,
                'lyricists': processed_lyricists,
                'composers': processed_composers,
                'originalArtist': processed_original_artist,
                'originalLang': original_lang,
                'cover': cover,
                'youtubeVideo': youtube_video,
                'lyrics': lyrics
            }
            
            # 写入文件
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write('---\n')
                yaml.dump(front_matter, f, allow_unicode=True, sort_keys=False)
                f.write('---\n')
            
            print(f"已创建: {file_path}")

def main():
    # 基础目录
    base_dir = '/Users/xdl/Documents/github/exactlyrics'
    
    # 读取songs.yaml
    songs_file = Path(base_dir) / 'data' / 'songs.yaml'
    with open(songs_file, 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)
    
    # 处理歌曲数据
    songs_data = data['songs']
    create_song_files(songs_data, base_dir)

if __name__ == "__main__":
    main()

执行程序:

(venv) xdl@MacBook-Air ~/Documents/github/sqids % python create_song_pages.py
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/HpOnhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/HpOnhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/HpOnhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Tjrf2MTkRO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Tjrf2MTkRO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Tjrf2MTkRO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/tHajrqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/tHajrqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/tHajrqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/P1CbowGyEf.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/P1CbowGyEf.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/P1CbowGyEf.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/FR1MIVpWYl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/FR1MIVpWYl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/FR1MIVpWYl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/6TGqz5oIeh.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/6TGqz5oIeh.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/6TGqz5oIeh.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/LP30oPzRKg.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/LP30oPzRKg.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/LP30oPzRKg.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/qYdxwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/qYdxwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/qYdxwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Oq7CspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Oq7CspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Oq7CspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/K6wrj18tIp.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/K6wrj18tIp.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/K6wrj18tIp.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/EmX3i3DecB.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/EmX3i3DecB.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/EmX3i3DecB.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/vANsEECW6h.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/vANsEECW6h.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/vANsEECW6h.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/U7Hlcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/U7Hlcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/U7Hlcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/MsRd9TtPvG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/MsRd9TtPvG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/MsRd9TtPvG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/mUtpz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/mUtpz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/mUtpz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/jgQhOMowOd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/jgQhOMowOd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/jgQhOMowOd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/lhn1mWNb7s.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/lhn1mWNb7s.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/lhn1mWNb7s.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/RDuDKBGw75.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/RDuDKBGw75.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/RDuDKBGw75.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/JFPKH5YPsL.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/JFPKH5YPsL.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/JFPKH5YPsL.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/3CJTPt6X1y.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/3CJTPt6X1y.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/3CJTPt6X1y.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/d891eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/d891eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/d891eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Y38ibjBerG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Y38ibjBerG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Y38ibjBerG.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Oq7nspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Oq7nspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Oq7nspNn8E.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/hKbOp8CRXa.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/hKbOp8CRXa.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/hKbOp8CRXa.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/d896eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/d896eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/d896eR0JlC.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/XtxfhBbiTE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/XtxfhBbiTE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/XtxfhBbiTE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/A2IqBwYk2r.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/A2IqBwYk2r.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/A2IqBwYk2r.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/HpOWhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/HpOWhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/HpOWhRDfQ7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/aOUQMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/aOUQMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/aOUQMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/cz6VOjBXK6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/cz6VOjBXK6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/cz6VOjBXK6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/rQo2USeDQE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/rQo2USeDQE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/rQo2USeDQE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/qYdvwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/qYdvwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/qYdvwNaZJF.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/NI5AjHNk8D.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/NI5AjHNk8D.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/NI5AjHNk8D.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/IVSCsb5hk1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/IVSCsb5hk1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/IVSCsb5hk1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/GrzagaI3H9.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/GrzagaI3H9.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/GrzagaI3H9.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/QvecroD9Vz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/QvecroD9Vz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/QvecroD9Vz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/7dpMm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/7dpMm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/7dpMm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/oejdLCQZFt.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/oejdLCQZFt.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/oejdLCQZFt.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/tHa8rqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/tHa8rqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/tHa8rqHKmd.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/nbiA8xKa6p.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/nbiA8xKa6p.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/nbiA8xKa6p.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/mUtxz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/mUtxz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/mUtxz7gwyT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/1uM0uxrcLu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/1uM0uxrcLu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/1uM0uxrcLu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/7dpBm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/7dpBm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/7dpBm9Pznc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/p9q2gPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/p9q2gPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/p9q2gPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/aOUPMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/aOUPMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/aOUPMdkWj4.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/zoTGSc9Q2V.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/zoTGSc9Q2V.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/zoTGSc9Q2V.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/g5v9RjurBc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/g5v9RjurBc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/g5v9RjurBc.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/ZcK83GYTXO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/ZcK83GYTXO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/ZcK83GYTXO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/uysWTyTwU5.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/uysWTyTwU5.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/uysWTyTwU5.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/U7HWcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/U7HWcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/U7HWcqu9Xz.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/9LY1bjg0b2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/9LY1bjg0b2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/9LY1bjg0b2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/xamzai0Eje.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/xamzai0Eje.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/xamzai0Eje.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/iZhJwpxEu0.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/iZhJwpxEu0.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/iZhJwpxEu0.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/5SA8QLxpRW.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/5SA8QLxpRW.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/5SA8QLxpRW.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/CMFbiy896i.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/CMFbiy896i.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/CMFbiy896i.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Sk2ZHgZ2nu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Sk2ZHgZ2nu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Sk2ZHgZ2nu.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/8JLz1kD2KY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/8JLz1kD2KY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/8JLz1kD2KY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/bwZ5kJHCQX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/bwZ5kJHCQX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/bwZ5kJHCQX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/p9qmgPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/p9qmgPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/p9qmgPK31d.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/wGcDVo8U6J.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/wGcDVo8U6J.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/wGcDVo8U6J.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/eNgsfSiaVv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/eNgsfSiaVv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/eNgsfSiaVv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/2BVooOzwSI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/2BVooOzwSI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/2BVooOzwSI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/HpOnbhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/HpOnbhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/HpOnbhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Tjrur2MTkR.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Tjrur2MTkR.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Tjrur2MTkR.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/tHaMRrqHKm.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/tHaMRrqHKm.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/tHaMRrqHKm.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/P1CA9owGyE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/P1CA9owGyE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/P1CA9owGyE.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/FR1A6IVpWY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/FR1A6IVpWY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/FR1A6IVpWY.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/6TGYXz5oIe.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/6TGYXz5oIe.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/6TGYXz5oIe.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/LP3SNoPzRK.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/LP3SNoPzRK.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/LP3SNoPzRK.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/qYdsdwNaZJ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/qYdsdwNaZJ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/qYdsdwNaZJ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Oq78UspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Oq78UspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Oq78UspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/K6wfAj18tI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/K6wfAj18tI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/K6wfAj18tI.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/EmXy1i3Dec.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/EmXy1i3Dec.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/EmXy1i3Dec.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/vANmyEECW6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/vANmyEECW6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/vANmyEECW6.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/U7HQocqu9X.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/U7HQocqu9X.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/U7HQocqu9X.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/MsRng9TtPv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/MsRng9TtPv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/MsRng9TtPv.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/mUtXKz7gwy.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/mUtXKz7gwy.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/mUtXKz7gwy.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/jgQqXOMowO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/jgQqXOMowO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/jgQqXOMowO.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/lhn69mWNb7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/lhn69mWNb7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/lhn69mWNb7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/RDuZFKBGw7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/RDuZFKBGw7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/RDuZFKBGw7.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/JFPs9H5YPs.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/JFPs9H5YPs.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/JFPs9H5YPs.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/3CJq4Pt6X1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/3CJq4Pt6X1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/3CJq4Pt6X1.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/d89v3eR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/d89v3eR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/d89v3eR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Y38m0bjBer.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Y38m0bjBer.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Y38m0bjBer.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/Oq78jspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/Oq78jspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/Oq78jspNn8.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/hKb0Pp8CRX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/hKb0Pp8CRX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/hKb0Pp8CRX.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/d89vqeR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/d89vqeR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/d89vqeR0Jl.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/XtxbZhBbiT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/XtxbZhBbiT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/XtxbZhBbiT.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/A2IGnBwYk2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/A2IGnBwYk2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/A2IGnBwYk2.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/HpOnuhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/HpOnuhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/HpOnuhRDfQ.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/aOU3lMdkWj.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/aOU3lMdkWj.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/aOU3lMdkWj.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/cz62bOjBXK.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/cz62bOjBXK.md
文件已存在,跳过: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/cz62bOjBXK.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/rQo7FUSeDQ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/rQo7FUSeDQ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/rQo7FUSeDQ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/qYdsPwNaZJ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/qYdsPwNaZJ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/qYdsPwNaZJ.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/en-us/songs/NI5y4jHNk8.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hans/songs/NI5y4jHNk8.md
已创建: /Users/xdl/Documents/github/exactlyrics/content/zh-hant/songs/NI5y4jHNk8.md

可以看到最后创建了9个文件,每个文件对应三个语种。这一步其实已经调用OpenCC进行了简繁转换,但是中英还没有转换,所以需要再来一步。

9. 中英翻译

既然是翻译,那么就要有相应的词典文件,我已经建立了一个translations_zh_en.yaml文件用来储存中英词典文件,每次有新内容的时候还是需要手动更新。

translations:
  # 人名
  周深: Zhou Shen
  周杰伦: Jay Chou
  方文山: Vincent Fang
  张韶涵: Angela Chang
  毛不易: Mao Buyi
  郁可唯: Yisa Yu
  莫文蔚: Karen Mok
  林家谦: Terence Lam Ka Him
  胡彦斌: Anson Hu
  三叔说: San Shu Shuo

  # 作品
  有山有海有你: Mountains, Seas, and You
  家乡人: Hometown People
  一路生花: Blossoming All the Way
  行舟问柳: Rowing and Asking the Willows
  烟花易冷: Fireworks Are Easily Cold
  消夏图: Summer Painting
  星鱼: Starfish
  心同此愿: Heart's Desire
  小美满: Little Happiness
  一晌: A Moment of Bliss
  消散人潮: Dispersing Crowds
  我以渺小爱你: I Love You with My Small Love
  她说,老啦: She Said, I'm Old Now
  她: She
  听见下雨的声音: Hearing the Sound of Rain
  似夏: Like Summer
  身边: By Your Side
  若仙: If Immortal
  时间之海: Sea of Time
  山鹰和兰花花: El Cóndor Pasa y Lan Huahua
  人是_: People Are
  亲爱的旅人啊: Dear Traveler
  三餐四季: Three Meals and Four Seasons
  门: Door
  迷途: Lost
  明暗之间: Between Light and Dark
  明明: Obviously
  旅途: Journey
  千千阙歌: Thousands of Songs
  铃芽之旅: Suzume's Journey
  记得: Remember
  敬时光: Respect Time
  兰亭序: Lantingji Xu
  借过一下: Just Passing By
  借梦: Borrowing Dreams
  过客: Passerby
  花开忘忧: Flowers Bloom, Worries Forgotten
  痕迹: Traces
  好风起: Good Wind Rising
  和光同尘: Harmonizing with Light and Dust
  化身孤岛的鲸: Whale on a Lonely Island
  风吹过的晨曦: Morning Light Blown by the Wind
  非遗里的中国: China in Intangible Heritage
  非你所想: Not What You Think
  浮光: Floating Light
  大鱼: Big Fish
  共鸣: Resonance
  达拉崩吧: Dala Beng Ba
  璀璨冒险人: Dazzling Adventurer
  春雪: Spring Snow
  冰凌花: Ice Flower
  此去半生: Half a Lifetime Away
  梦见你: Dreaming of You
  桂花摇: Osmanthus Shakes
  归来: Return
  如你随行: As You Accompany
  嗨人间: Hi, Humanity
  锦鲤抄: Koi Copy
  痴情冢: Grave of Infatuation
  牵丝戏: Puppet Play
  洛春赋: Luo Chun Fu
  不问别离: No Questions About Parting
  不谓侠: Not a Hero
  长安姑娘: Chang'an Gril
  赐我: Grant Me
  虞兮叹: Yu Xi Tan
  烟雨唱扬州: Singing Yangzhou in Mist and Rain
  明月天涯: Bright Moon on the Horizon
  一笑江湖: A Smile in the Jianghu
  霜雪千年: Frost and Snow for a Thousand Years
  醉千年: Drunk for a Thousand Years
  辞九门回忆: Farewell to the Nine Gates Memories
  谪仙: Exiled Immortal
  春庭雪: Spring Courtyard Snow
  星月落: Falling Stars and Moon
  云边的风筝: Kite on the Edge of the Clouds
  云裳羽衣曲: Cloud Garment and Feather Clothes Melody
  曼陀: Mandala
  总有美好在路上: Beauty Always on the Way
  你没说的话: The Words You Left Unsaid
  知冷暖: Knowing Warmth and Cold
  牧马城市: Nomadic City
  岁月里的花: Flowers in Time
  想起他们: Echos of Reverie
  消愁: Drown Sorrows
  春日部: Kasukabe
  我想你了: I Miss You
  下潜: Dive Down
  心事: Heart
  海市蜃楼: Mirage

然后写个程序translate_songs_zh_en.py:

import yaml
from pathlib import Path
import re

def load_translations(translation_file):
    """加载翻译YAML文件"""
    with open(translation_file, 'r', encoding='utf-8') as f:
        return yaml.safe_load(f).get('translations', {})

def translate_content(content, translations):
    """翻译Markdown文件内容"""
    # 分割YAML front matter和其余内容
    parts = re.split(r'^---\n', content, maxsplit=2, flags=re.MULTILINE)
    if len(parts) < 3:
        return content  # 不符合front matter格式的文件
    
    yaml_part = parts[1]
    rest_content = parts[2]
    
    # 解析YAML内容
    try:
        data = yaml.safe_load(yaml_part)
        if not data:
            return content
    except yaml.YAMLError:
        return content
    
    # 需要翻译的字段
    translatable_fields = [
        'title', 'artists', 'lyricists', 
        'composers', 'originalArtist'
    ]
    
    # 应用翻译
    modified = False
    for field in translatable_fields:
        if field not in data:
            continue
            
        original = data[field]
        if isinstance(original, list):
            # 处理列表(如artists, lyricists等)
            translated = []
            for item in original:
                if item in translations:
                    translated.append(translations[item])
                    modified = True
                else:
                    translated.append(item)
            data[field] = translated
        elif original and original in translations:
            # 处理单个字符串
            data[field] = translations[original]
            modified = True
    
    if not modified:
        return content
    
    # 重新构建内容
    new_yaml = yaml.dump(data, allow_unicode=True, sort_keys=False)
    return f"---\n{new_yaml}---\n{rest_content}"

def process_files(translations_file, content_dir):
    """处理目录下的所有Markdown文件(跳过_index.md)"""
    translations = load_translations(translations_file)
    if not translations:
        print("错误:无法加载翻译文件或翻译内容为空")
        return
    
    processed = 0
    modified = 0
    
    for md_file in Path(content_dir).glob('*.md'):
        if md_file.name == '_index.md':
            print(f"跳过索引文件: {md_file.name}")
            continue
            
        processed += 1
        try:
            with open(md_file, 'r', encoding='utf-8') as f:
                original_content = f.read()
            
            new_content = translate_content(original_content, translations)
            
            if new_content != original_content:
                with open(md_file, 'w', encoding='utf-8') as f:
                    f.write(new_content)
                print(f"已更新: {md_file.name}")
                modified += 1
            else:
                print(f"无需更新: {md_file.name}")
        except Exception as e:
            print(f"处理文件失败 {md_file.name}: {str(e)}")
    
    print(f"\n处理完成: 共处理 {processed} 个文件,修改了 {modified} 个文件")

if __name__ == "__main__":
    # 配置路径
    translations_file = Path("/Users/xdl/Documents/github/sqids/translations_zh_en.yaml")
    content_dir = Path("/Users/xdl/Documents/github/exactlyrics/content/en-us/songs/")
    
    process_files(translations_file, content_dir)

执行程序:

(venv) xdl@MacBook-Air ~/Documents/github/sqids % python translate_songs_zh_en.py
无需更新: RDuDKBGw75.md
无需更新: tHa8rqHKmd.md
无需更新: aOU3lMdkWj.md
无需更新: P1CbowGyEf.md
无需更新: RDuZFKBGw7.md
无需更新: 7dpBm9Pznc.md
无需更新: JFPs9H5YPs.md
无需更新: HpOnuhRDfQ.md
无需更新: bwZ5kJHCQX.md
无需更新: K6wfAj18tI.md
无需更新: U7Hlcqu9Xz.md
无需更新: Y38m0bjBer.md
无需更新: Tjrf2MTkRO.md
无需更新: 6TGqz5oIeh.md
无需更新: aOUPMdkWj4.md
无需更新: aOUQMdkWj4.md
无需更新: K6wrj18tIp.md
无需更新: lhn69mWNb7.md
无需更新: mUtxz7gwyT.md
无需更新: MsRd9TtPvG.md
无需更新: Oq7nspNn8E.md
无需更新: 5SA8QLxpRW.md
无需更新: U7HQocqu9X.md
无需更新: jgQhOMowOd.md
无需更新: JFPKH5YPsL.md
无需更新: uysWTyTwU5.md
无需更新: cz62bOjBXK.md
已更新: NI5y4jHNk8.md
无需更新: Oq7CspNn8E.md
无需更新: vANmyEECW6.md
无需更新: ZcK83GYTXO.md
无需更新: d89vqeR0Jl.md
无需更新: 3CJq4Pt6X1.md
无需更新: EmX3i3DecB.md
无需更新: zoTGSc9Q2V.md
无需更新: tHajrqHKmd.md
无需更新: qYdxwNaZJF.md
无需更新: MsRng9TtPv.md
无需更新: A2IqBwYk2r.md
无需更新: FR1MIVpWYl.md
无需更新: NI5AjHNk8D.md
无需更新: mUtXKz7gwy.md
无需更新: Sk2ZHgZ2nu.md
无需更新: HpOnbhRDfQ.md
无需更新: QvecroD9Vz.md
无需更新: 6TGYXz5oIe.md
无需更新: 9LY1bjg0b2.md
无需更新: xamzai0Eje.md
无需更新: FR1A6IVpWY.md
无需更新: qYdsdwNaZJ.md
无需更新: XtxbZhBbiT.md
无需更新: IVSCsb5hk1.md
无需更新: A2IGnBwYk2.md
无需更新: Tjrur2MTkR.md
无需更新: eNgsfSiaVv.md
无需更新: CMFbiy896i.md
无需更新: HpOWhRDfQ7.md
无需更新: mUtpz7gwyT.md
无需更新: hKbOp8CRXa.md
无需更新: XtxfhBbiTE.md
无需更新: iZhJwpxEu0.md
无需更新: qYdvwNaZJF.md
无需更新: Oq78UspNn8.md
无需更新: LP30oPzRKg.md
无需更新: GrzagaI3H9.md
无需更新: 8JLz1kD2KY.md
已更新: qYdsPwNaZJ.md
无需更新: d896eR0JlC.md
无需更新: vANsEECW6h.md
无需更新: g5v9RjurBc.md
无需更新: tHaMRrqHKm.md
无需更新: U7HWcqu9Xz.md
无需更新: 7dpMm9Pznc.md
无需更新: HpOnhRDfQ7.md
无需更新: 1uM0uxrcLu.md
无需更新: d89v3eR0Jl.md
无需更新: 2BVooOzwSI.md
无需更新: p9qmgPK31d.md
无需更新: cz6VOjBXK6.md
无需更新: 3CJTPt6X1y.md
无需更新: p9q2gPK31d.md
无需更新: P1CA9owGyE.md
无需更新: d891eR0JlC.md
已更新: rQo7FUSeDQ.md
无需更新: lhn1mWNb7s.md
无需更新: oejdLCQZFt.md
无需更新: Y38ibjBerG.md
无需更新: Oq78jspNn8.md
无需更新: jgQqXOMowO.md
无需更新: EmXy1i3Dec.md
无需更新: nbiA8xKa6p.md
无需更新: hKb0Pp8CRX.md
无需更新: LP3SNoPzRK.md
跳过索引文件: _index.md
无需更新: rQo2USeDQE.md
无需更新: wGcDVo8U6J.md

处理完成: 共处理 95 个文件,修改了 3 个文件

至此,就完成了数据的更新。

10. 更新网站

使用github推送本地更新到github网页,cloudfare pages已经链接到了这个仓库,会自动更新,至此网站也就完成了更新。