起因∶网易云无故被封

促进我自建媒体库的原因,是因为网易云乱封号,两次把我的号给封了。
理由居然是——播放数据异常
真就是,想封你,随便编个理由,直接就封。纯脑残!
其实之前我就有点想搞媒体服务器了,但是由于我看番都是用种子下载看番,而且番剧看一次也就过去了,我觉得也没用留着的必要。毕竟番剧的一话,1080P 60帧,都要差不多1GB了,看番多的话,文件破TB级那是轻轻松松。
这次封号,算是坚定了我的想法。我觉得国内互联网公司绝大部分都是狗屎,让人无语。
其实是2024年10月封的号,但是我2025年1月才开始折腾媒体服务器。因为之前都在学校,课业繁忙,乐的要死,因为整理音乐库费时费力,所以我打算放假再搞。事实证明我的想法是对的,我前前后后整理了快一个星期,整理了绝大部分,但是还有相当一部分歌曲还没整理。太累了,让我歇歇。目前已经整理了1300+首歌,文件夹已经90G+了。。。

为什么选择了Jellyfin?

免费,开源。开源,开源,开源,还是他妈的开源!
我是支持GNU自由软件运动的人,自然更倾向于使用开源的解决方案,尤其是使用GPL协议的开源软件。
顺带一提,我主力操作系统早就已经换成GNU/Linux了。

部署Jellyfin

下载Jellyfin

直接前往官方网页下载即可,我用的最新版,目前是10.10.3
下载链接:repo.jellyfin.org/?path=/server/linux/latest-stable

下载FFmpeg

由于我服务器使用的是AlmaLinux8.10,是服务器发行版,其自带的FFmpeg比较老,新版的Jellyfin并不兼容。
虽然可以通过yum包管理器直接安装对应版本的Jellyfin,并且不会出现兼容问题。但是旧版的Jellyfin并没有歌词支持,对我来说比较蛋疼,因为我主要想做的就是歌曲的媒体服务器。
我下载的是7.0.2版本的,这种的话,我更倾向于使用最新版的前一个稳定版。
下载地址:ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz

自定义环境变量,以使用自定义的ffmpeg

我的做法是,把FFmpegJellyfin解压后,分别放到了不同的文件夹,然后在根目录文件夹放了一个启动脚本。
以下是启动脚本的内容:

#!/bin/bash

# 定义FFmpeg二进制文件所在的路径
FFMPEG_PATH="./ffmpeg/bin"

# 将FFmpeg路径添加到PATH环境变量的最前面
export PATH="$FFMPEG_PATH:$PATH"

# 启动Jellyfin
./jellyfin/jellyfin

脚本保存为run.sh,然后再:

chmod +x ./run.sh

试一下执行这个脚本,看看Jellyfin有没有启动成功,如果启动成功后就完事了。

配置Jellyfin

初始化配置

打开浏览器,输入服务器对应的ip+8096端口,例如:127.0.0.1:8096
按照指引,设置用户名和密码。这部过于简单,有手就行,不多赘述。

配置HTTPS访问

这个Jellyfin比较特别,需要使用pfx证书,而不是常见的pem+key的证书组合。
我们需要把pemkey证书转成pfx格式。
下面是一个shell脚本,可以便利当前目录的pem+key证书,并且转换为pfx证书放到当前目录的pfx_cert/文件夹内:

#!/bin/bash

# 定义输出目录
OUTPUT_DIR="pfx_cert"

# 创建输出目录(如果它不存在)
mkdir -p "$OUTPUT_DIR"

# 遍历所有的 .key 文件
for key in *.key; do
  # 获取不带扩展名的文件名
  base_name="${key%.key}"
  
  # 检查对应的 .pem 文件是否存在
  if [[ -f "${base_name}.pem" ]]; then
    echo "正在处理 $base_name..."

    # 转换为 PFX 格式
    openssl pkcs12 -export \
    -out "${OUTPUT_DIR}/${base_name}.pfx" \
    -inkey "$key" \
    -in "${base_name}.pem" \
    -certfile le-chain.pem
    
    echo "已生成 ${OUTPUT_DIR}/${base_name}.pfx"
  else
    echo "警告:找不到与 $key 对应的 .pem 文件"
  fi
done

把以上内容保存到to_pfx.sh,然后:

chmod +x ./to_pfx.sh
./to_pfx.sh

当要求输入密码的时候,直接回车,不设置密码。
在Jellyfin设置证书,选择得到的pfx证书文件。
记得勾上强制HTTPS允许远程连接
Screenshot_20250118_170812.png
保存后,重启Jellyfin,应该就可以通过HTTPS访问了。别忘了添加域名解析记录。
至此,基本上配置完成。

添加媒体注意事项

如果是音乐的话,最好的目录分类方式应该是:

  • 歌手 > 专辑 > 音乐文件/歌词文件

例如,如果是Taylor Swift的《Red》专辑,目录结构就长这样:

/path/to/music_library/
│
├── Taylor Swift
│   └── Red (2012)
│       ├── 01. State of Grace.mp3
│       ├── 01. State of Grace.lrc
│       ├── 02. Red.mp3
│       ├── 02. Red.lrc
│       ├── 03. Treacherous.mp3
│       ├── 03. Treacherous.lrc
│       ├── 04. I Knew You Were Trouble.mp3
│       ├── 04. I Knew You Were Trouble.lrc
│       ├── 05. All Too Well.mp3
│       ├── 05. All Too Well.lrc
│       ├── 06. 22.mp3
│       ├── 06. 22.lrc
│       ├── 07. I Almost Do.mp3
│       ├── 07. I Almost Do.lrc
│       ├── 08. We Are Never Ever Getting Back Together.mp3
│       ├── 08. We Are Never Ever Getting Back Together.lrc
│       ├── 09. Stay Stay Stay.mp3
│       ├── 09. Stay Stay Stay.lrc
│       ├── 10. The Last Time.mp3
│       ├── 10. The Last Time.lrc
│       ├── 11. Holy Ground.mp3
│       ├── 11. Holy Ground.lrc
│       ├── 12. Sad Beautiful Tragic.mp3
│       ├── 12. Sad Beautiful Tragic.lrc
│       ├── 13. Begin Again.mp3
│       ├── 13. Begin Again.lrc
│       ├── 14. Girl At Home.mp3
│       ├── 14. Girl At Home.lrc
│       └── cover.jpg           # 封面图片

整理好后,在Jellyfin添加媒体库,选择音乐,在添加上对应的文件夹路径,就可以了。
Screenshot_20250118_173647.png
如果是按照这个方法添加的话,Jellyfin是可以自动检索元数据并且添加的,可以改善音乐库的观感:
Screenshot_20250118_171942.png
歌词的效果:
Screenshot_20250118_172102.png
效果还是可以的!

音乐库去哪找?

我是用了网易云无损解析这类工具,把歌曲一首一首下回来,自己整理,工作量巨大!
好处是,有一些工具可以一并把歌词下来,比较舒服。
这种工具估计网上一搜,都一大把,很多都是网站在线解析的,并不需要下载软件。
什么?你不会找?找不到?是不是搜索引擎是垃圾百度啊?还是360搜索?如果还在用国内这种垃圾搜索引擎,还不会换,那我只能说你是赛博文盲!
当然,你也可以去某宝,花几块钱去购买并下载资源,但是大概率给你个百度网盘链接(改名百度软盘得了),然后下载龟速。2333

我的收集方法

我是用了这个:
api.toubiec.cn/wyapi.html
我是一次下一个专辑,由于下载的每首歌都是一个zip压缩包,本来下载就费时费力,整理更加费时费力。
于是,我就整了一点小工具做一下辅助:
open_zip.sh放在下载目录,负责遍历解压压缩包,文件内容如下:

#!/bin/bash

# 检查是否有zip文件存在
if [ "$(ls -A *.zip 2> /dev/null)" ]; then
    # 遍历所有.zip文件
    for zip_file in *.zip; do
        # 获取不带扩展名的文件名
        base_name="${zip_file%.zip}"

        # 创建以文件名为名的新文件夹
        mkdir -p "$base_name"

        # 解压到新建的文件夹内
        unzip -q "$zip_file" -d "$base_name"

        # 检查解压是否成功
        if [ $? -eq 0 ]; then
            echo "Extracted $zip_file into folder $base_name"
            # 如果解压成功,删除zip文件
            rm "$zip_file"
            echo "Deleted $zip_file"
        else
            echo "Failed to extract $zip_file"
        fi
    done
else
    echo "No zip files found."
fi

out_folder.sh放在下载目录,负责遍历移动子文件夹里面的内容到工作目录,文件内容如下:

#!/bin/bash

# 使用 find 命令查找所有文件并将它们移动到当前目录。
# -mindepth 2 确保我们只获取子文件夹中的文件,而不是当前目录中的文件。
# -maxdepth 2 确保我们不会深入到更深层次的子文件夹中。
# -type f 确保我们只处理文件,而不处理文件夹。
find . -mindepth 2 -maxdepth 2 -type f -exec mv {} . \;

# 查找并删除空的子文件夹
# -empty 匹配空文件或空文件夹
# -type d 确保我们只处理文件夹
# -delete 删除匹配到的文件夹
find . -mindepth 1 -maxdepth 1 -type d -empty -delete

echo "文件移动完成,并且空文件夹已被删除。"

rename_songs.py负责重命名和整理歌曲相关文件,依据截取到的html进行排序整理。需要把网易云音乐网页版的专辑曲目部分的的html代码保存到同目录的content.txt文件中,执行后会自动解析保存的html代码并依据此进行重命名和排序。
文件内容如下:

import os
from bs4 import BeautifulSoup
import re
import unicodedata

def sanitize_title(title):
    """清理标题以进行精确匹配,保留非ASCII字符"""
    # 只移除ASCII范围内的非字母数字字符,并转换为小写
    return ''.join([c for c in unicodedata.normalize('NFKC', title) if c.isalnum() or not (0 <= ord(c) < 128)]).lower()

def clean_title(title):
    """清理并标准化歌曲标题"""
    # 将HTML实体替换为空格
    title = re.sub(r'&nbsp;', ' ', title)
    # 移除多余的空白字符,但保留单词间的单个空格
    title = re.sub(r'\s+', ' ', title).strip()
    # 去除首尾可能出现的特殊字符或符号
    title = re.sub(r'^[^\w\s]+|[^\w\s]+$', '', title)
    # 统一下划线和空格
    title = re.sub(r'_', ' ', title)
    return title

def parse_html(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')
    # 查找所有的表格行,仅限于<tbody>中的行
    rows = soup.select('tbody tr')

    songs = []
    for row in rows:
        # 获取编号
        num_tag = row.find('span', class_='num')
        number = num_tag.get_text(strip=True) if num_tag else 'N/A'

        # 尝试获取歌曲标题,考虑了可能存在的多种情况
        title_tag = row.select_one('td:nth-of-type(2) b, td:nth-of-type(2) a')
        if title_tag:
            # 清理标题文本,移除干扰元素
            for unwanted in title_tag.find_all('div', class_='soil'):
                unwanted.extract()  # 移除不需要的<div>标签
            title = clean_title(title_tag.get('title') or title_tag.get_text(strip=True))
            sanitized_title = sanitize_title(title)
        else:
            title = 'Title Not Found'
            sanitized_title = sanitize_title(title)

        # 只有当number不是'N/A'且title不是'Title Not Found'时加入列表
        if number != 'N/A' and title != 'Title Not Found':
            songs.append((number, title, sanitized_title))

    print("Parsed Songs:")
    for song in songs:
        print(f"Number: {song[0]}, Title: {song[1]}, Sanitized Title: {song[2]}")

    return songs

def standardize_filename(filename):
    """标准化文件名,将下划线替换为空格,并清理多余空格"""
    base_name, ext = os.path.splitext(filename)
    standardized_base_name = re.sub(r'_', ' ', base_name)
    standardized_base_name = re.sub(r'\s+', ' ', standardized_base_name).strip()
    return f"{standardized_base_name}{ext}"

def rename_files_in_directory(directory_path, songs):
    # 遍历目录中的所有文件
    renamed_files = set()  # 用于跟踪已经重命名的文件,避免重复重命名
    for filename in os.listdir(directory_path):
        file_path = os.path.join(directory_path, filename)

        # 如果是文件并且文件名(不包括扩展名)与解析出的歌曲名称匹配,则重命名
        if os.path.isfile(file_path) and not filename.endswith('.txt') and not filename.endswith('.py'):
            standardized_filename = standardize_filename(filename)
            base_name, ext = os.path.splitext(standardized_filename)

            matched = False
            for i, (number, title, sanitized_title) in enumerate(songs, start=1):
                sanitized_base_name = sanitize_title(base_name)
                # 使用清理后的字符串进行精确匹配
                if sanitized_base_name == sanitized_title:
                    # 格式化编号为两位数
                    formatted_number = f"{i:02d}"
                    new_filename = f"{formatted_number}. {base_name}{ext}"  # 使用标准化后的文件名作为歌曲名
                    new_file_path = os.path.join(directory_path, new_filename)

                    if not os.path.exists(new_file_path):  # 检查新文件名是否已存在
                        if filename not in renamed_files:
                            print(f'Renaming "{filename}" to "{new_filename}"...')
                            try:
                                os.rename(file_path, new_file_path)
                                renamed_files.add(filename)  # 添加到已重命名集合中
                                matched = True
                            except Exception as e:
                                print(f"Error renaming '{filename}': {e}")
                    else:
                        print(f"New filename '{new_filename}' already exists.")
                    break  # 文件名匹配成功后跳出循环,避免重复重命名

            if not matched:
                print(f"No match found for file '{filename}'.")

def handle_jpg_files(directory_path):
    """处理目录中的所有 .jpg 文件,保留一个并重命名为 cover.jpg,删除其余的"""
    jpg_files = [f for f in os.listdir(directory_path) if f.endswith('.jpg')]

    if len(jpg_files) > 0:
        # 选择第一个找到的 .jpg 文件作为封面
        cover_file = jpg_files[0]
        cover_path = os.path.join(directory_path, cover_file)
        new_cover_path = os.path.join(directory_path, 'cover.jpg')

        # 如果cover.jpg不存在,则重命名第一个找到的.jpg文件为cover.jpg
        if not os.path.exists(new_cover_path):
            os.rename(cover_path, new_cover_path)
            print(f'Renamed "{cover_file}" to "cover.jpg".')

        # 删除其他的 .jpg 文件
        for jpg in jpg_files[1:]:
            jpg_path = os.path.join(directory_path, jpg)
            os.remove(jpg_path)
            print(f'Deleted "{jpg}".')
    else:
        print("No .jpg files found to process.")

if __name__ == "__main__":
    # 获取当前脚本所在的目录
    directory_path = os.path.dirname(os.path.abspath(__file__))

    # 处理 .jpg 文件
    handle_jpg_files(directory_path)

    # 读取同目录下的 content.txt 文件获取 HTML 内容
    content_file_path = os.path.join(directory_path, 'content.txt')

    try:
        with open(content_file_path, 'r', encoding='utf-8') as file:
            html_content = file.read()
    except FileNotFoundError:
        print("Error: The file 'content.txt' was not found in the current directory.")
        exit(1)
    except Exception as e:
        print(f"An error occurred while reading 'content.txt': {e}")
        exit(1)

    songs = parse_html(html_content)
    rename_files_in_directory(directory_path, songs)

    print("All processes completed.")

至于怎么获取专辑曲目部分的的html代码,直接在浏览器右键歌曲列表,检查代码,一层一层往上找,当显示是整个列表的内容的元素,那就找到了。
Screenshot_20250118_175906.png
然后,右键,复制内部html,这就是需要的东西了。把这些html代码粘贴到content.txt就行了。
注意,上述的四个文件:open_zip.shout_folder.shrename_songs.pycontent.txt都是放在专辑的下载目录下。
以上准备工作做完之后,在当前目录打开命令行,就可以施展组合拳了:

./open_zip.sh
./out_folder.sh
python rename_songs.py

什么?Python提示ModuleNotFoundError: No module named 'xxx'?缺模块就去装啊,自己去搜,看了资料还不会装的话,只能说你唐!唐完了!!!
如果一切顺利,目录就会变成上面示例目录结构专辑文件夹里面的样子了。

小福利:Taylor Swift 的专辑

为了更直观的看到是怎么组织文件目录的,我就把我整理的Taylor Swift的几个专辑分享出来吧!
分别是:《Red》《1989》《Fearless》《Lover》《Speak Now》《Taylor Swift》
以下是压缩分卷,解压密码是GTX1080Ti
Taylor Swift.part1.rar
Taylor Swift.part5.rar
Taylor Swift.part4.rar
Taylor Swift.part3.rar
Taylor Swift.part2.rar
别告诉我你不会解压缩,不然我只会骂你唐货!