├── .gitignore ├── 115sha1查重 ├── config.ini ├── 使用方法.txt ├── 基准文件夹sha1查重.py └── 指定文件夹sha1查重.py ├── Emby封面 ├── 4K Remux-封面.jpg ├── 4K Remux.jpeg ├── 儿童动漫-封面.jpeg ├── 儿童动漫.jpeg ├── 动画电影-封面.jpeg ├── 动画电影.jpeg ├── 华语电影-封面.jpeg ├── 华语电影.jpeg ├── 周星驰系列-封面.jpeg ├── 周星驰系列.jpeg ├── 国产剧-封面.jpeg ├── 国产剧.jpeg ├── 国漫-封面.jpeg ├── 国漫.jpeg ├── 日剧-封面.jpeg ├── 日剧.jpeg ├── 日漫-封面.jpeg ├── 日漫.jpeg ├── 日韩电影-封面.jpeg ├── 日韩电影.jpeg ├── 欧美剧-封面.jpeg ├── 欧美剧.jpeg ├── 欧美电影-封面.jpeg ├── 欧美电影.jpeg ├── 纪录片-封面.jpeg ├── 纪录片.jpeg ├── 蓝光电影-封面.jpeg ├── 蓝光电影.jpeg ├── 观影-封面.jpg ├── 观影.jpg ├── 追剧-封面.jpg ├── 追剧.jpg ├── 韩剧-封面.jpeg ├── 韩剧.jpeg ├── 音乐-封面.png └── 音乐.png ├── README.md ├── a.jpeg ├── b.jpeg ├── c.jpeg ├── clear_eadir_dstore.py ├── d.png ├── hlink_sync_delete.py ├── hlink_watcher.py ├── remove-eadir.py ├── remove-no-video-dir.py ├── update_host.py ├── 保留同目录最大的电影.py ├── 删除指定后缀文件.py ├── 删除重名电影.py ├── 动漫添加总集数.py ├── 去除包含指定内容的行.py ├── 字幕处理 ├── Renamer-剧集命名.ini ├── Renamer-剧集命名.py ├── Renamer.ini ├── Renamer.py ├── ass2srt.py ├── ass_cunstomer_folder.py ├── ass_customer-中英单行.ini ├── ass_customer-中英单行.py ├── ass_customer-多线程.py ├── ass_customer.py ├── ass_edit-多线程.ini ├── ass_edit-多线程.py ├── ass_edit.py ├── srt2ass_folder-多线程.ini ├── srt2ass_folder-多线程.py ├── sub_get-多线程.ini ├── sub_get-多线程.py ├── sub_get.py ├── 去除顶部字幕.py ├── 字幕平移时间轴.py ├── 字幕时间同步-input.py ├── 字幕时间同步-命令行.py ├── 字幕时间轴同步-自定偏移.ini ├── 字幕时间轴同步-自定偏移.py ├── 字幕时间轴同步.ini ├── 字幕时间轴同步.py ├── 字幕时间轴同步(原版).ini ├── 字幕时间轴同步(原版).py ├── 字幕繁体转简体.py ├── 网盘字幕提取上传.ini ├── 网盘字幕提取上传.py ├── 获取sup字幕时间轴.py ├── 获取字幕信息.py ├── 获取视频字幕信息.py ├── 重命名字幕.ini └── 重命名字幕.py ├── 寻找缺失集数.py ├── 批量扫描emby视频信息.py ├── 批量更新emby封面.py ├── 根据tmdbid查重.py ├── 清空二级文件夹.py ├── 电影文件夹加tmdbid-movie-nfo.py ├── 电影版本筛选.py ├── 电视剧文件夹加tmdbid-season-nfo.py ├── 电视剧文件夹加tmdbid-tmdb.py ├── 自动合并Emby版本.py ├── 视频封面制作 ├── CoverMaker.py ├── 动态图封面制作.py ├── 封面制作-加横幅.py ├── 封面制作-双语.py ├── 封面制作-居中.py ├── 封面制作.py ├── 拼接图片.py ├── 方正综艺简体.ttf ├── 方正超粗黑_GBK.ttf ├── 方正跃进体_GBK.ttf ├── 方正韵动特黑_GBK.ttf └── 点字倔强黑.ttf ├── 视频添加封面.py └── 集数偏移.py /.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有 __pycache__ 目录 2 | __pycache__ 3 | 4 | .vscode/ 5 | 6 | # 忽略所有 .idea 目录 7 | .idea/ 8 | 9 | download/ 10 | 11 | config/* 12 | backup/* 13 | .DS_Store 14 | *.yaml 15 | *.log -------------------------------------------------------------------------------- /115sha1查重/config.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | clouddrvie2_address=http://localhost:19798 3 | clouddrive2_account= 4 | clouddrive2_passwd= 5 | #sha1单目录查重 6 | root_path= 7 | #cd2根目录 8 | clouddrive2_root_path= 9 | #基准查重:基准 10 | base_folder= 11 | #基准查重:删除 12 | target_folder= -------------------------------------------------------------------------------- /115sha1查重/使用方法.txt: -------------------------------------------------------------------------------- 1 | 先安装依赖 2 | pip install clouddrive==0.0.10.4 3 | 4 | 然后去config.ini里填好配置 5 | 6 | python main.py -------------------------------------------------------------------------------- /115sha1查重/基准文件夹sha1查重.py: -------------------------------------------------------------------------------- 1 | import os 2 | from clouddrive import CloudDriveClient, CloudDriveFileSystem 3 | import configparser 4 | 5 | working_directory = os.path.dirname(os.path.abspath(__file__)) 6 | os.chdir(working_directory) 7 | 8 | 9 | class BaseFolderDuplicateFileFinder: 10 | def __init__(self): 11 | self.clouddrvie2_address = "" 12 | self.clouddrive2_account = "" 13 | self.clouddrive2_passwd = "" 14 | self.base_folder = "" 15 | self.target_folder = "" 16 | self.clouddrive2_root_path = "" 17 | self.fs = "" 18 | 19 | def set_config( 20 | self, 21 | clouddrvie2_address, 22 | clouddrive2_account, 23 | clouddrive2_passwd, 24 | base_folder, 25 | target_folder, 26 | clouddrive2_root_path, 27 | ): 28 | self.clouddrvie2_address = clouddrvie2_address 29 | self.clouddrive2_account = clouddrive2_account 30 | self.clouddrive2_passwd = clouddrive2_passwd 31 | self.base_folder = base_folder 32 | self.target_folder = target_folder 33 | self.clouddrive2_root_path = clouddrive2_root_path 34 | client = CloudDriveClient( 35 | clouddrvie2_address, clouddrive2_account, clouddrive2_passwd 36 | ) 37 | self.fs = CloudDriveFileSystem(client) 38 | 39 | def find_duplicate_files(self): 40 | hash_map = self.get_base_folder_hash_list() 41 | duplicate_files = [] 42 | fs_dir_path = self.target_folder.replace(self.clouddrive2_root_path, "") 43 | for foldername, subfolders, filenames in self.fs.walk_path(fs_dir_path): 44 | for filename in filenames: 45 | file_path = os.path.join(foldername, filename) 46 | file_extension = os.path.splitext(filename)[1].lower() 47 | if file_extension in [ 48 | ".mkv", 49 | ".iso", 50 | ".ts", 51 | ".mp4", 52 | ".avi", 53 | ".rmvb", 54 | ".wmv", 55 | ".m2ts", 56 | ".mpg", 57 | ".flv", 58 | ".rm", 59 | ".mov", 60 | ]: 61 | file_sha1 = self.fs.attr(file_path)["fileHashes"]["2"] 62 | print(file_path) 63 | if file_sha1 in hash_map: 64 | duplicate_files.append((hash_map[file_sha1], file_path)) 65 | try: 66 | self.fs.remove(file_path) 67 | except: 68 | pass 69 | print(f"已经删除sha1重复的文件::: {file_path}") 70 | return duplicate_files 71 | 72 | def get_base_folder_hash_list(self): 73 | hash_map = {} 74 | fs_dir_path = self.base_folder.replace(self.clouddrive2_root_path, "") 75 | for foldername, subfolders, filenames in self.fs.walk_path(fs_dir_path): 76 | for filename in filenames: 77 | file_path = os.path.join(foldername, filename) 78 | file_extension = os.path.splitext(filename)[1].lower() 79 | if file_extension in [ 80 | ".mkv", 81 | ".iso", 82 | ".ts", 83 | ".mp4", 84 | ".avi", 85 | ".rmvb", 86 | ".wmv", 87 | ".m2ts", 88 | ".mpg", 89 | ".flv", 90 | ".rm", 91 | ".mov", 92 | ]: 93 | file_sha1 = self.fs.attr(file_path)["fileHashes"]["2"] 94 | if file_sha1 not in hash_map: 95 | hash_map[file_sha1] = file_path 96 | print(file_path) 97 | return hash_map 98 | 99 | def write_results_to_file(self, results, output_file): 100 | with open(output_file, "w") as f: 101 | for duplicate_pair in results: 102 | f.write("Duplicate files:\n") 103 | f.write(f"{duplicate_pair[0]}\n") 104 | f.write(f"{duplicate_pair[1]}\n") 105 | f.write("\n") 106 | 107 | 108 | if __name__ == "__main__": 109 | # 使用示例 110 | config_file_path = "config.ini" # 你的配置文件路径 111 | config = configparser.ConfigParser() 112 | 113 | # 读取 INI 文件 114 | config.read(config_file_path) 115 | config = config["config"] 116 | 117 | clouddrvie2_address = config["clouddrvie2_address"] 118 | clouddrive2_account = config["clouddrive2_account"] 119 | clouddrive2_passwd = config["clouddrive2_passwd"] 120 | root_path = config["root_path"] 121 | base_folder = config["base_folder"] 122 | target_folder = config["target_folder"] 123 | clouddrive2_root_path = config["clouddrive2_root_path"] 124 | 125 | base_finder = BaseFolderDuplicateFileFinder() 126 | base_finder.set_config( 127 | clouddrvie2_address, 128 | clouddrive2_account, 129 | clouddrive2_passwd, 130 | base_folder, 131 | target_folder, 132 | clouddrive2_root_path, 133 | ) 134 | duplicate_files = base_finder.find_duplicate_files() 135 | # output_file = "/Users/shenxian/Desktop/res.txt" 136 | # finder.write_results_to_file(duplicate_files, output_file) 137 | # print("Duplicate files found and written to:", output_file) 138 | -------------------------------------------------------------------------------- /115sha1查重/指定文件夹sha1查重.py: -------------------------------------------------------------------------------- 1 | import os 2 | from clouddrive import CloudDriveClient, CloudDriveFileSystem 3 | import configparser 4 | 5 | working_directory = os.path.dirname(os.path.abspath(__file__)) 6 | os.chdir(working_directory) 7 | 8 | 9 | class DuplicateFileFinder: 10 | def __init__(self): 11 | self.clouddrvie2_address = "" 12 | self.clouddrive2_account = "" 13 | self.clouddrive2_passwd = "" 14 | self.root_path = "" 15 | self.clouddrive2_root_path = "" 16 | self.fs = "" 17 | 18 | def set_config( 19 | self, 20 | clouddrvie2_address, 21 | clouddrive2_account, 22 | clouddrive2_passwd, 23 | root_path, 24 | clouddrive2_root_path, 25 | ): 26 | self.clouddrvie2_address = clouddrvie2_address 27 | self.clouddrive2_account = clouddrive2_account 28 | self.clouddrive2_passwd = clouddrive2_passwd 29 | self.root_path = root_path 30 | self.clouddrive2_root_path = clouddrive2_root_path 31 | client = CloudDriveClient( 32 | clouddrvie2_address, clouddrive2_account, clouddrive2_passwd 33 | ) 34 | self.fs = CloudDriveFileSystem(client) 35 | 36 | def find_duplicate_files(self): 37 | hash_map = {} 38 | duplicate_files = [] 39 | fs_dir_path = self.root_path.replace(self.clouddrive2_root_path, "") 40 | for foldername, subfolders, filenames in self.fs.walk_path(fs_dir_path): 41 | for filename in filenames: 42 | file_path = os.path.join(foldername, filename) 43 | file_extension = os.path.splitext(filename)[1].lower() 44 | if file_extension in [ 45 | ".mkv", 46 | ".iso", 47 | ".ts", 48 | ".mp4", 49 | ".avi", 50 | ".rmvb", 51 | ".wmv", 52 | ".m2ts", 53 | ".mpg", 54 | ".flv", 55 | ".rm", 56 | ".mov", 57 | ]: 58 | file_sha1 = self.fs.attr(file_path)["fileHashes"]["2"] 59 | if file_sha1 in hash_map: 60 | existing_file = hash_map[file_sha1] 61 | duplicate_files.append((hash_map[file_sha1], file_path)) 62 | if len(existing_file) < len(file_path): 63 | try: 64 | self.fs.remove(existing_file) 65 | except: 66 | pass 67 | print(f"已经删除sha1重复的文件::: {existing_file}") 68 | hash_map[file_sha1] = file_path 69 | else: 70 | try: 71 | self.fs.remove(file_path) 72 | except: 73 | pass 74 | print(f"已经删除sha1重复的文件::: {file_path}") 75 | else: 76 | hash_map[file_sha1] = file_path 77 | return duplicate_files 78 | 79 | def write_results_to_file(self, results, output_file): 80 | with open(output_file, "w") as f: 81 | for duplicate_pair in results: 82 | f.write("Duplicate files:\n") 83 | f.write(f"{duplicate_pair[0]}\n") 84 | f.write(f"{duplicate_pair[1]}\n") 85 | f.write("\n") 86 | 87 | 88 | if __name__ == "__main__": 89 | # 使用示例 90 | config_file_path = "config.ini" # 你的配置文件路径 91 | config = configparser.ConfigParser() 92 | 93 | # 读取 INI 文件 94 | config.read(config_file_path) 95 | config = config["config"] 96 | 97 | clouddrvie2_address = config["clouddrvie2_address"] 98 | clouddrive2_account = config["clouddrive2_account"] 99 | clouddrive2_passwd = config["clouddrive2_passwd"] 100 | root_path = config["root_path"] 101 | base_folder = config["base_folder"] 102 | target_folder = config["target_folder"] 103 | clouddrive2_root_path = config["clouddrive2_root_path"] 104 | finder = DuplicateFileFinder() 105 | finder.set_config( 106 | clouddrvie2_address, 107 | clouddrive2_account, 108 | clouddrive2_passwd, 109 | root_path, 110 | clouddrive2_root_path, 111 | ) 112 | 113 | duplicate_files = finder.find_duplicate_files() 114 | # output_file = "/Users/shenxian/Desktop/res.txt" 115 | # finder.write_results_to_file(duplicate_files, output_file) 116 | # print("Duplicate files found and written to:", output_file) 117 | -------------------------------------------------------------------------------- /Emby封面/4K Remux-封面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/4K Remux-封面.jpg -------------------------------------------------------------------------------- /Emby封面/4K Remux.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/4K Remux.jpeg -------------------------------------------------------------------------------- /Emby封面/儿童动漫-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/儿童动漫-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/儿童动漫.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/儿童动漫.jpeg -------------------------------------------------------------------------------- /Emby封面/动画电影-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/动画电影-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/动画电影.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/动画电影.jpeg -------------------------------------------------------------------------------- /Emby封面/华语电影-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/华语电影-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/华语电影.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/华语电影.jpeg -------------------------------------------------------------------------------- /Emby封面/周星驰系列-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/周星驰系列-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/周星驰系列.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/周星驰系列.jpeg -------------------------------------------------------------------------------- /Emby封面/国产剧-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/国产剧-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/国产剧.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/国产剧.jpeg -------------------------------------------------------------------------------- /Emby封面/国漫-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/国漫-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/国漫.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/国漫.jpeg -------------------------------------------------------------------------------- /Emby封面/日剧-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日剧-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/日剧.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日剧.jpeg -------------------------------------------------------------------------------- /Emby封面/日漫-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日漫-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/日漫.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日漫.jpeg -------------------------------------------------------------------------------- /Emby封面/日韩电影-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日韩电影-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/日韩电影.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/日韩电影.jpeg -------------------------------------------------------------------------------- /Emby封面/欧美剧-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/欧美剧-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/欧美剧.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/欧美剧.jpeg -------------------------------------------------------------------------------- /Emby封面/欧美电影-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/欧美电影-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/欧美电影.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/欧美电影.jpeg -------------------------------------------------------------------------------- /Emby封面/纪录片-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/纪录片-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/纪录片.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/纪录片.jpeg -------------------------------------------------------------------------------- /Emby封面/蓝光电影-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/蓝光电影-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/蓝光电影.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/蓝光电影.jpeg -------------------------------------------------------------------------------- /Emby封面/观影-封面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/观影-封面.jpg -------------------------------------------------------------------------------- /Emby封面/观影.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/观影.jpg -------------------------------------------------------------------------------- /Emby封面/追剧-封面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/追剧-封面.jpg -------------------------------------------------------------------------------- /Emby封面/追剧.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/追剧.jpg -------------------------------------------------------------------------------- /Emby封面/韩剧-封面.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/韩剧-封面.jpeg -------------------------------------------------------------------------------- /Emby封面/韩剧.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/韩剧.jpeg -------------------------------------------------------------------------------- /Emby封面/音乐-封面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/音乐-封面.png -------------------------------------------------------------------------------- /Emby封面/音乐.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/Emby封面/音乐.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaHelper 2 | 搭建媒体库的过程中为了解决各种问题而写的小脚本 3 | -------------------------------------------------------------------------------- /a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/a.jpeg -------------------------------------------------------------------------------- /b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/b.jpeg -------------------------------------------------------------------------------- /c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/c.jpeg -------------------------------------------------------------------------------- /clear_eadir_dstore.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | def delete_eadir_and_dsstore(path): 5 | try: 6 | for root, dirs, files in os.walk(path, topdown=False): 7 | # 删除名为"@eaDir"的文件夹 8 | for dir_name in dirs[:]: 9 | if dir_name == "@eaDir": 10 | dir_path = os.path.join(root, dir_name) 11 | subprocess.run(['rm', '-rf', dir_path]) 12 | print(f"Deleted folder: {dir_path}") 13 | 14 | # 删除后缀为".DS_Store"的文件 15 | for file_name in files: 16 | if file_name.endswith(".DS_Store"): 17 | file_path = os.path.join(root, file_name) 18 | subprocess.run(['rm', file_path]) 19 | print(f"Deleted file: {file_path}") 20 | 21 | print("Deletion complete.") 22 | except Exception as e: 23 | print(f"Error: {str(e)}") 24 | 25 | if __name__ == "__main__": 26 | folder_list = ["/volume2/Media","/volume1/CloudNAS/CloudDrive2/115/看剧"] 27 | for folder_path in folder_list: 28 | delete_eadir_and_dsstore(folder_path) 29 | -------------------------------------------------------------------------------- /d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/d.png -------------------------------------------------------------------------------- /hlink_sync_delete.py: -------------------------------------------------------------------------------- 1 | import os 2 | from re import L 3 | import subprocess 4 | import time 5 | import logging 6 | 7 | # 配置日志 8 | log_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"hlink-sync-delete.log") 9 | logging.basicConfig(filename=log_path, level=logging.INFO, format="%(asctime)s [%(levelname)s]: %(message)s") 10 | 11 | # 记录程序运行时间 12 | current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 13 | logging.info(f"程序运行开始时间: {current_time}") 14 | 15 | def log_error(message): 16 | logging.error(message) 17 | 18 | def get_hard_links(file_path, start_folder): 19 | try: 20 | # 使用 stat 命令获取文件的 inode 号 21 | stat_result = subprocess.check_output(['stat', '-c', '%i', file_path]) 22 | inode = int(stat_result.strip()) 23 | 24 | # 使用 find 命令查找具有相同 inode 号的文件 25 | find_command = ['find', start_folder, '-inum', str(inode)] 26 | find_result = subprocess.check_output(find_command) 27 | hard_links = find_result.decode().strip().split('\n') 28 | find_command = ['find', recycle_folder, '-inum', str(inode)] 29 | find_result = subprocess.check_output(find_command) 30 | hard_links_2 = find_result.decode().strip().split('\n') 31 | if hard_links_2: 32 | hard_links.extend(hard_links_2) 33 | hard_links = [item for item in hard_links if item != ""] 34 | return hard_links 35 | except subprocess.CalledProcessError: 36 | return [] 37 | 38 | def get_file_age(file_path): 39 | current_time = time.time() 40 | file_creation_time = os.path.getctime(file_path) 41 | # 计算文件的存活时间 42 | file_age = current_time - file_creation_time 43 | return file_age 44 | 45 | def delete_files_with_single_hardlink(folder_path, index): 46 | global file_deleted,folder_deleted 47 | for root, _, files in os.walk(folder_path, topdown=False): 48 | for file in files: 49 | file_path = os.path.join(root, file) 50 | file_age = get_file_age(file_path) 51 | try: 52 | # 获取文件的硬链接数 53 | num_hardlinks = os.stat(file_path).st_nlink 54 | if num_hardlinks == 2: 55 | hard_links = get_hard_links(file_path, links_folder_list[index]) 56 | if any("#recycle" in item for item in hard_links) and file_age > file_life: 57 | os.remove(file_path) 58 | file_deleted += 1 59 | logging.info(f'已删除文件: {file_path}') 60 | for file in hard_links: 61 | os.remove(file) 62 | file_deleted += 1 63 | logging.info(f'已删除文件: {file}') 64 | elif num_hardlinks == 1 and file_age > file_life: 65 | # 如果文件的存活时间超过10分钟才删除 66 | if file_age > file_life: 67 | os.remove(file_path) 68 | file_deleted += 1 69 | logging.info(f'已删除文件: {file_path}') 70 | except Exception as e: 71 | log_error(f"文件{file_path}出现错误:{e}") 72 | for root, dirs, files in os.walk(folder_path, topdown=False): 73 | for dir in dirs: 74 | dir_path = os.path.join(root, dir) 75 | if not os.listdir(dir_path): # 文件夹为空 76 | # 检查文件夹是否在白名单中 77 | os.rmdir(dir_path) 78 | folder_deleted += 1 79 | logging.info(f'已发现空文件夹: {dir_path}') 80 | 81 | if __name__ == "__main__": 82 | folder_list = ["/volume2/Media/download/电影", "/volume2/Media/download/电视剧", "/volume2/Media/download/动漫"] 83 | links_folder_list = ["/volume2/Media/links/电影", "/volume2/Media/links/电视剧", "/volume2/Media/links/动漫"] 84 | recycle_folder = "/volume2/Media/#recycle" 85 | file_life = 1 86 | file_deleted = 0 87 | folder_deleted = 0 88 | for i, folder_to_clean in enumerate(folder_list): 89 | delete_files_with_single_hardlink(folder_to_clean, i) 90 | logging.info(f'程序运行完成:共删除{file_deleted}个文件,{folder_deleted}个文件夹.') 91 | -------------------------------------------------------------------------------- /hlink_watcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | def check_and_remove_files_with_one_hardlink(folder_path): 5 | if not os.path.exists(folder_path) or not os.path.isdir(folder_path): 6 | print("无效的文件夹路径!") 7 | return 8 | 9 | for root, dirs, files in os.walk(folder_path,topdown=False): 10 | for file in files: 11 | if not file.endswith((".mkv", ".mp4", ".rmvb", ".avi")): 12 | continue 13 | file_path = os.path.join(root, file) 14 | hard_link_count = os.stat(file_path).st_nlink 15 | if hard_link_count == 1: # 子文件夹中的文件硬链接数为1 16 | file_creation_time = os.path.getmtime(file_path) 17 | current_time = time.time() 18 | time_difference = current_time - file_creation_time 19 | hours_in_a_day = 24 20 | if time_difference > hours_in_a_day * 3600: # 创建时间大于24小时 21 | try: 22 | # os.remove(file_path) # 删除子文件夹中的文件 23 | print("发现硬链接数为1且创建时间大于24小时的文件,已删除:", file_path) 24 | except OSError as e: 25 | print("删除文件时出错:", e) 26 | 27 | for root, dirs, files in os.walk(folder_path, topdown=False): 28 | for dir_name in dirs: 29 | dir_path = os.path.join(root, dir_name) 30 | if not os.listdir(dir_path): # 检查文件夹是否为空 31 | try: 32 | os.rmdir(dir_path) # 删除空文件夹 33 | print("空文件夹已删除:", dir_path) 34 | except OSError as e: 35 | print("删除文件夹时出错:", e) 36 | 37 | if __name__ == '__main__': 38 | # 指定要检测的文件夹路径 39 | folder_path_list = ["/volume2/Media/download/电影", "/volume2/Media/download/电视剧", "/volume2/Media/download/动漫"] 40 | for folder_path in folder_path_list: 41 | check_and_remove_files_with_one_hardlink(folder_path) 42 | 43 | -------------------------------------------------------------------------------- /remove-eadir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | all_dirs = [] 5 | def show_files(path): 6 | file_list = os.listdir(path) 7 | for file in file_list: 8 | cur_path = os.path.join(path, file) 9 | if os.path.isdir(cur_path): 10 | all_dirs.append(cur_path) 11 | if "@eaDir" in cur_path: 12 | continue 13 | else: 14 | show_files(cur_path) 15 | else: 16 | continue 17 | return all_dirs 18 | 19 | dir_path = "/volume2/Media" 20 | contents = show_files(dir_path) 21 | for content in contents: 22 | if "@eaDir" in content: 23 | print(content 24 | ) #os.system('rm -rf "'+content+'"') -------------------------------------------------------------------------------- /remove-no-video-dir.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | # 指定要遍历的根文件夹 5 | root_folder = '/volume2/Media/custom/Emby' 6 | 7 | # 指定视频文件后缀名 8 | video_extensions = {'.mp4', '.avi', '.rmvb', '.wmv', '.mkv', '.ts'} 9 | 10 | def is_video_file(filename): 11 | return any(filename.endswith(ext) for ext in video_extensions) 12 | 13 | def delete_empty_folders(path): 14 | for root, dirs, files in os.walk(path, topdown=False): 15 | for dir_name in dirs: 16 | dir_path = os.path.join(root, dir_name) 17 | if not any(os.path.isdir(os.path.join(dir_path, sub_dir)) for sub_dir in os.listdir(dir_path)): 18 | if not any(is_video_file(file) for file in os.listdir(dir_path)): 19 | print(f"已删除无视频文件夹: {dir_path}") 20 | shutil.rmtree(dir_path) 21 | if __name__ == '__main__': 22 | delete_empty_folders(root_folder) 23 | -------------------------------------------------------------------------------- /update_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | import re 6 | import requests 7 | import json 8 | import platform 9 | 10 | # DOMAIN_HOST=1 to enable, 0 to disable custom domain hosts file writing 11 | # GITHUB_HOST=1 to enable, 0 to disable GitHub domain hosts file writing 12 | DOMAIN_HOST = 1 13 | GITHUB_HOST = 0 14 | 15 | # List of custom domains, separated by spaces 16 | DOMAIN_LIST = ["api.themoviedb.org", "image.tmdb.org"] 17 | 18 | GITHUB_HOST_URL = "https://hosts.gitcdn.top/hosts.txt" 19 | DNS_API = "https://networkcalc.com/api/dns/lookup" 20 | 21 | 22 | def get_hosts_file_path(): 23 | system = platform.system() 24 | if system == "Windows": 25 | return r"C:\Windows\System32\drivers\etc\hosts" 26 | elif system == "Darwin": 27 | return "/etc/hosts" 28 | elif system == "Linux": 29 | return "/etc/hosts" 30 | else: 31 | raise Exception("Unsupported operating system") 32 | 33 | 34 | HOSTS_PATH = get_hosts_file_path() 35 | 36 | 37 | def parse_dns_response(response_json): 38 | """ 39 | 解析 DNS 查询的 JSON 响应,返回 IP 地址列表. 40 | 41 | Args: 42 | response_json (str): 包含 DNS 查询结果的 JSON 字符串. 43 | 44 | Returns: 45 | list: 包含所有 A 记录 IP 地址的列表. 46 | """ 47 | try: 48 | # 解析 JSON 字符串为 Python 字典 49 | response_data = json.loads(response_json) 50 | 51 | # 检查响应状态是否为 "OK" 52 | if response_data["status"] != "OK": 53 | return [] 54 | 55 | # 提取 A 记录中的所有 IP 地址 56 | ip_addresses = [record["address"] for record in response_data["records"]["A"]] 57 | 58 | return ip_addresses 59 | 60 | except (KeyError, ValueError): 61 | # 如果 JSON 解析或访问字典键失败,返回空列表 62 | return [] 63 | 64 | 65 | def update_tmdb_host(): 66 | with open(HOSTS_PATH, "r") as hosts_file: 67 | hosts_content = hosts_file.read() 68 | 69 | # 删除之前的内容 70 | new_content = re.sub( 71 | r"# tmdb-domain-hosts begin.*?# tmdb-domain-hosts end\n", 72 | "", 73 | hosts_content, 74 | flags=re.DOTALL, 75 | ) 76 | 77 | # 写入新的内容 78 | with open(HOSTS_PATH, "w") as hosts_file: 79 | hosts_file.write(new_content) 80 | hosts_file.write("# tmdb-domain-hosts begin\n") 81 | for domain in DOMAIN_LIST: 82 | try: 83 | response = requests.get(f"{DNS_API}/{domain}") 84 | domain_ip_list = parse_dns_response(response.text) 85 | for domain_ip in domain_ip_list: 86 | hosts_file.write(f"{domain_ip}\t\t{domain}\n") 87 | except requests.exceptions.RequestException: 88 | pass 89 | hosts_file.write("# tmdb-domain-hosts end\n") 90 | 91 | 92 | update_tmdb_host() 93 | -------------------------------------------------------------------------------- /保留同目录最大的电影.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | def remove_metadata(filepath): 6 | dir_path = os.path.dirname(filepath) 7 | basename = os.path.basename(filepath) 8 | filename, ext = os.path.splitext(basename) 9 | for ext in [ 10 | ".nfo", 11 | "-clearlogo.png", 12 | "-fanart.jpg", 13 | "-landscape.jpg", 14 | "-poster.jpg", 15 | ]: 16 | file = os.path.join(dir_path, filename + ext) 17 | if os.path.exists(file): 18 | try: 19 | os.remove(file) 20 | print(f"metadata file deleted: {file}") 21 | except: 22 | pass 23 | 24 | 25 | def find_largest_mkv(directory): 26 | """ 27 | 遍历指定目录下的一级子目录,找到每个子目录中最大的mkv文件并删除其他的。 28 | """ 29 | for subdir in os.listdir(directory): 30 | subdir_path = os.path.join(directory, subdir) 31 | if os.path.isdir(subdir_path): 32 | mkv_files = [f for f in os.listdir(subdir_path) if f.endswith(".mkv")] 33 | if len(mkv_files) > 1: 34 | largest_mkv = max( 35 | mkv_files, 36 | key=lambda x: os.path.getsize(os.path.join(subdir_path, x)), 37 | ) 38 | for mkv in mkv_files: 39 | if mkv != largest_mkv: 40 | os.remove(os.path.join(subdir_path, mkv)) 41 | remove_metadata(os.path.join(subdir_path, mkv)) 42 | print(f"已删除 {os.path.join(subdir_path, mkv)}") 43 | print(f"找到最大的mkv文件: {os.path.join(subdir_path, largest_mkv)}") 44 | 45 | 46 | # 使用示例 47 | dir_list = [ 48 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/欧美电影", 49 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/华语电影", 50 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/日韩电影", 51 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/动画电影", 52 | ] 53 | if __name__ == "__main__": 54 | for source_dir in dir_list: 55 | find_largest_mkv(source_dir) 56 | -------------------------------------------------------------------------------- /删除指定后缀文件.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | working_directory = os.path.dirname(os.path.abspath(__file__)) 4 | os.chdir(working_directory) 5 | 6 | num = 0 7 | 8 | # 要删除文件的文件夹路径 9 | dir_path = input("请输入文件夹路径") 10 | 11 | # 要删除的文件后缀 12 | 13 | 14 | for root, dirs, files in os.walk(dir_path): 15 | for file in files: 16 | if file.endswith(file_ext): 17 | file_path = os.path.join(root, file) 18 | try: 19 | os.remove(file_path) 20 | num += 1 21 | print(f"File deleted::: {file} total => {num}") 22 | except: 23 | pass 24 | -------------------------------------------------------------------------------- /删除重名电影.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | 5 | 6 | def remove_duplicate_movies(base_dir, dest_dir): 7 | movie_list = get_movie_list(base_dir) 8 | for root, dirs, files in os.walk(dest_dir): 9 | for dir_name in dirs: 10 | if "(" not in dir_name: 11 | continue 12 | if any(movie_name in dir_name for movie_name in movie_list): 13 | dir_path = os.path.join(root, dir_name) 14 | print(f"发现重复的电影: {dir_path}") 15 | try: 16 | if os.path.exists(dir_path): 17 | shutil.rmtree(dir_path) 18 | except: 19 | pass 20 | with open( 21 | "/Users/shenxian/Desktop/res.txt", "a", encoding="utf-8" 22 | ) as f: 23 | f.write(f"{dir_path}\n") 24 | 25 | 26 | def get_movie_list(dir_path): 27 | movie_list = [] 28 | num = 0 29 | for root, dirs, files in os.walk(dir_path): 30 | for dir_name in dirs: 31 | if "(" not in dir_name: 32 | continue 33 | movie_name = re.findall(r"(.*)?\(", dir_name) 34 | if movie_name: 35 | num += 1 36 | print(movie_name[0], num) 37 | movie_list.append(movie_name[0].strip()) 38 | return movie_list 39 | 40 | 41 | # 调用代码 42 | remove_duplicate_movies( 43 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/FRDS电影mUHD大包", 44 | "/Users/shenxian/CloudNAS/CloudDrive2/115/我的接收/已刮削", 45 | ) 46 | -------------------------------------------------------------------------------- /动漫添加总集数.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from clouddrive import * 4 | 5 | folder_path = "/Volumes/dav/115/我的接收/日漫/银魂.2006" 6 | mkv_list = [] 7 | nfo_list = [] 8 | ass_list = [] 9 | for root, _, files in os.walk(folder_path): 10 | for file in files: 11 | if "SubBackup" in root: 12 | continue 13 | if file.endswith("mkv"): 14 | file_path = os.path.join(root, file) 15 | mkv_list.append(file_path) 16 | elif file.endswith("nfo") and "第" in file: 17 | file_path = os.path.join(root, file) 18 | nfo_list.append(file_path) 19 | elif file.endswith("ass"): 20 | file_path = os.path.join(root, file) 21 | ass_list.append(file_path) 22 | 23 | if mkv_list: 24 | mkv_list.sort( 25 | key=lambda x: int( 26 | int(re.findall("S0*(\d+)", x)[0]) * 1000 27 | + int(re.findall("第0*(\d+)集", x)[0]) 28 | ) 29 | ) 30 | if nfo_list: 31 | nfo_list.sort( 32 | key=lambda x: int( 33 | int(re.findall("S0*(\d+)", x)[0]) * 1000 34 | + int(re.findall("第0*(\d+)集", x)[0]) 35 | ) 36 | ) 37 | if ass_list: 38 | ass_list.sort( 39 | key=lambda x: int( 40 | int(re.findall("S0*(\d+)", x)[0]) * 1000 41 | + int(re.findall("第0*(\d+)集", x)[0]) 42 | ) 43 | ) 44 | 45 | from clouddrive import CloudDriveClient, CloudDriveFileSystem 46 | 47 | client = CloudDriveClient("cd2地址", "账号", "密码") 48 | fs = CloudDriveFileSystem(client) 49 | 50 | for i, file_path in enumerate(mkv_list): 51 | file_path = file_path.replace("/Volumes/dav", "") 52 | new_file_path = file_path.replace("集", f"集({i+1})") 53 | fs.rename(file_path, new_file_path) 54 | print(new_file_path) 55 | 56 | for i, file_path in enumerate(nfo_list): 57 | file_path = file_path.replace("/Volumes/dav", "") 58 | new_file_path = file_path.replace("集", f"集({i+1})") 59 | fs.rename(file_path, new_file_path) 60 | print(new_file_path) 61 | 62 | for i, file_path in enumerate(ass_list): 63 | file_path = file_path.replace("/Volumes/dav", "") 64 | new_file_path = file_path.replace("集", f"集({i+1})") 65 | fs.rename(file_path, new_file_path) 66 | print(new_file_path) 67 | -------------------------------------------------------------------------------- /去除包含指定内容的行.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | 4 | def detect_and_convert_encoding(input_file): 5 | encodings = ["utf-32", "utf-16", "utf-8", "cp1252", "gb2312", "gbk", "big5"] 6 | 7 | for enc in encodings: 8 | try: 9 | with codecs.open(input_file, mode="r", encoding=enc, errors="ignore") as fd: 10 | content = fd.read() 11 | 12 | # 如果文件内容不为空,则表示找到了正确的编码 13 | if content: 14 | with codecs.open(input_file, 'w', 'utf-8') as fd: 15 | fd.write(content) 16 | return True 17 | 18 | except Exception as e: 19 | continue 20 | # 如果所有编码都无法正常读取,则返回 False 21 | return False 22 | 23 | def clean_srt_file(file_path): 24 | # Open the SRT file for reading 25 | global ignore_words 26 | try: 27 | with open(file_path, 'r', encoding='utf-8') as file: 28 | lines = file.readlines() 29 | except: 30 | detect_and_convert_encoding(file_path) 31 | with open(file_path, 'r', encoding='utf-8') as file: 32 | lines = file.readlines() 33 | 34 | # Iterate through each line in the file and remove lines with Japanese characters 35 | cleaned_lines = [] 36 | for line in lines: 37 | if not any(i in line for i in ignore_words): 38 | cleaned_lines.append(line) 39 | # Write the result back to the original file 40 | with open(file_path, 'w', encoding='utf-8') as file: 41 | file.writelines(cleaned_lines) 42 | 43 | 44 | if __name__ == "__main__": 45 | folder_path = "/Volumes/dav/115/看剧/links/电影/系列电影/周星驰系列" # Replace with the folder path containing SRT files 46 | ignore_words = ['清水啸歌'] 47 | for root, dirs, files in os.walk(folder_path): 48 | for file in files: 49 | if file.lower().endswith(".ass"): 50 | srt_file_path = os.path.join(root, file) 51 | clean_srt_file(srt_file_path) 52 | print(f"Processed file: {srt_file_path}") 53 | -------------------------------------------------------------------------------- /字幕处理/Renamer-剧集命名.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/CloudNAS/CloudDrive2/115 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/Renamer-剧集命名.py: -------------------------------------------------------------------------------- 1 | from genericpath import isdir 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import threading 6 | import os 7 | import re 8 | import configparser 9 | 10 | 11 | class FileRenamerApp: 12 | def __init__(self, root): 13 | self.root = root 14 | self.root.title("Renamer") 15 | self.file_list = [] 16 | self.tv_name_entry = tk.StringVar() 17 | self.ep_pattern = tk.StringVar() 18 | self.ep_pattern_options = tk.StringVar() 19 | self.season_pattern = tk.StringVar() 20 | self.config = "" 21 | self.tv_name = "" 22 | self.multiple = tk.BooleanVar() 23 | self.chinse_num = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"] 24 | self.ext_list = [".mkv", ".mp4", ".srt", ".ass", ".ts", ".rmvb"] 25 | self._numbers_re = r"\d+|[一二三四五六七八九十]+" 26 | self._season_re = r"(?:第)?\s*(?:\d+|[一二三四五六七八九十]+)\s*(季)" 27 | self._season_re_2 = r"(? {new_name}") 241 | for res in previews: 242 | self.file_listbox.insert(tk.END, res) 243 | 244 | 245 | if __name__ == "__main__": 246 | root = tk.Tk() 247 | app = FileRenamerApp(root) 248 | sw = root.winfo_screenwidth() 249 | sh = root.winfo_screenheight() 250 | ww = 700 251 | wh = 500 252 | x = (sw - ww) / 2 253 | y = (sh - wh) / 2 - 60 254 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 255 | root.mainloop() 256 | -------------------------------------------------------------------------------- /字幕处理/Renamer.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Volumes/dav/115 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/Renamer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog 3 | from tkinter import ttk 4 | import threading 5 | import pyperclip 6 | import os 7 | import re 8 | import configparser 9 | 10 | class FileRenamerApp: 11 | def __init__(self,root): 12 | self.root = root 13 | self.root.title("Renamer") 14 | self.file_list = [] 15 | self.pattern_entry = tk.StringVar() 16 | self.replace_entry = tk.StringVar() 17 | self.insert_text_entry = tk.StringVar() 18 | self.config = '' 19 | self.ext_list = ['.mkv','.mp4','.srt','.ass'] 20 | self.load_config() 21 | self.folder_path = self.config.get('Settings', 'last_folder', fallback='') 22 | self.create_gui() 23 | 24 | def create_gui(self): 25 | frame_rename = ttk.LabelFrame(self.root,text="重命名选项") 26 | frame_rename.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 27 | 28 | # 添加 GUI 元素,包括模式选择、输入框、按钮等 29 | self.mode_var = tk.StringVar(value="常规替换") 30 | mode_label = tk.Label(frame_rename, text="重命名模式:") 31 | mode_optionmenu = tk.OptionMenu(frame_rename, self.mode_var, "常规替换", "正则替换","添加前缀","添加后缀","序列化") 32 | 33 | # 创建和布局 GUI 元素 34 | mode_label.grid(row=0, column=0, padx=10, pady=5) 35 | mode_optionmenu.grid(row=0, column=2, padx=10, pady=5) 36 | frame_options = ttk.LabelFrame(self.root, text="替换内容") 37 | frame_options.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 38 | 39 | tk.Label(frame_options,text="匹配文本:").grid(row=0, column=1, padx=10, pady=5) 40 | tk.Entry(frame_options,textvariable=self.pattern_entry, width=40).grid(row=0, column=2, padx=10, pady=5) 41 | 42 | tk.Label(frame_options,text="替换文本:").grid(row=1, column=1, columnspan=1, padx=10, pady=5) 43 | tk.Entry(frame_options,textvariable=self.replace_entry, width=40).grid(row=1, column=2,padx=10, pady=5) 44 | 45 | tk.Label(frame_options, text="添加内容:").grid(row=2, column=1, columnspan=1, padx=10, pady=5) 46 | tk.Entry(frame_options,textvariable=self.insert_text_entry, width=40).grid(row=2, column=2, padx=10, pady=5) 47 | 48 | # 创建文件列表框,用于显示文件列表 49 | 50 | frame_output = ttk.LabelFrame(self.root, text="文件信息") 51 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 52 | 53 | self.file_listbox = tk.Listbox(frame_output, width=50, height=10) 54 | self.file_listbox.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 55 | button_frame = tk.Frame(self.root) 56 | button_frame.pack(padx=10, pady=5) 57 | tk.Button(button_frame, text="添加文件", command=self.browse_files).pack(side=tk.LEFT, padx=10,pady=10) 58 | tk.Button(button_frame, text="添加文件夹", command=self.browse_folder).pack(side=tk.LEFT, padx=10,pady=10) 59 | tk.Button(button_frame, text="粘贴文件", command=self.get_clipboard_files).pack(side=tk.LEFT, padx=10,pady=10) 60 | tk.Button(button_frame, text="改名预览", command=self.preview_rename).pack(side=tk.LEFT, padx=10,pady=10) 61 | tk.Button(button_frame, text="重命名文件", command=self.start_to_rename).pack(side=tk.LEFT, padx=10,pady=10) 62 | tk.Button(button_frame, text="清空文件", command=self.empty_file_list).pack(side=tk.LEFT, padx=10,pady=10) 63 | 64 | def get_ini_path(self,*args): 65 | # 获取当前.py文件的绝对路径 66 | current_script = os.path.abspath(__file__) 67 | ini_path = current_script.replace('.py','.ini') 68 | return(ini_path) 69 | 70 | def load_config(self): 71 | # 创建配置文件对象 72 | if not os.path.exists(self.get_ini_path()): 73 | with open(self.get_ini_path(),'w') as f: 74 | f.write('') 75 | self.config = configparser.ConfigParser(default_section='Settings') 76 | self.config.read(self.get_ini_path()) # 读取配置文件 77 | 78 | def get_clipboard_files(self): 79 | if len(self.file_list) == 0: 80 | self.file_listbox.delete(0, tk.END) 81 | file_paths = [] 82 | clipboard = pyperclip.paste() 83 | file_list = clipboard.split('\n') 84 | if file_list: 85 | temp_list = [] 86 | for file in file_list: 87 | if os.path.exists(file): 88 | if file not in self.file_list: 89 | temp_list.append(file) 90 | else: 91 | temp_list = [] 92 | break 93 | file_paths.extend(temp_list) 94 | else: 95 | if os.path.exists(clipboard) and clipboard not in self.file_list: 96 | file_paths.append(clipboard) 97 | if file_paths: 98 | for path in file_paths: 99 | if path not in self.file_list: 100 | self.file_listbox.insert(tk.END, os.path.basename(path)) 101 | self.file_list.append(path) 102 | 103 | def browse_files(self): 104 | if len(self.file_list) == 0: 105 | self.file_listbox.delete(0, tk.END) 106 | file_paths = filedialog.askopenfilenames(initialdir=self.folder_path) 107 | if file_paths: 108 | folder_root = os.path.dirname(file_paths[0]) 109 | self.config.set('Settings', 'last_folder', folder_root) 110 | with open(self.get_ini_path(), 'w') as configfile: 111 | self.config.write(configfile) 112 | for path in file_paths: 113 | if path not in self.file_list: 114 | self.file_listbox.insert(tk.END, os.path.basename(path)) 115 | self.file_list.append(path) 116 | 117 | def browse_folder(self): 118 | if len(self.file_list) == 0: 119 | self.file_listbox.delete(0, tk.END) 120 | folder_path = filedialog.askdirectory(initialdir=self.folder_path) 121 | folder_root = os.path.dirname(folder_path) 122 | self.config.set('Settings', 'last_folder', folder_root) 123 | with open(self.get_ini_path(), 'w') as configfile: 124 | self.config.write(configfile) 125 | for root, _, files in os.walk(folder_path): 126 | for file in files: 127 | file_path = os.path.join(root,file) 128 | if os.path.splitext(file_path)[1] in self.ext_list and file_path not in self.file_list: 129 | print(file_path) 130 | self.file_listbox.insert(tk.END, os.path.basename(file_path)) 131 | self.file_list.append(file_path) 132 | 133 | def start_to_rename(self): 134 | thread = threading.Thread(target=self.rename, args=()) 135 | thread.start() 136 | 137 | def empty_file_list(self): 138 | self.file_list = [] 139 | self.file_listbox.delete(0, tk.END) 140 | 141 | def get_new_name(self,file,idx): 142 | selected_mode = self.mode_var.get() 143 | pattern = self.pattern_entry.get() 144 | replace_text = self.replace_entry.get() 145 | insert_text = self.insert_text_entry.get() 146 | file_name = os.path.basename(file) 147 | base_name, extension = os.path.splitext(file_name) 148 | if selected_mode == "常规替换": 149 | new_name = base_name.replace(pattern, replace_text) + extension 150 | elif selected_mode == "正则替换": 151 | new_name = re.sub(pattern, replace_text, base_name) + extension 152 | elif selected_mode == "序列化": 153 | new_name = f"{insert_text}{idx + 1:02d}{extension}" 154 | elif selected_mode == "添加前缀": 155 | new_name = f"{insert_text}{base_name}{extension}" 156 | elif selected_mode == "添加后缀": 157 | new_name = f"{base_name}{insert_text}{extension}" 158 | if new_name.endswith('ass') and 'zh' not in new_name and 'chs' not in new_name: 159 | new_name = new_name.replace('.ass','.zh.ass') 160 | if new_name.endswith('srt') and 'zh' not in new_name and 'chs' not in new_name: 161 | new_name = new_name.replace('.srt','.zh.srt') 162 | return file_name,new_name 163 | 164 | def rename(self): 165 | files_to_rename = self.file_list[:] 166 | self.file_listbox.delete(0, tk.END) 167 | 168 | if not files_to_rename: 169 | self.file_listbox.insert(tk.END, "未选择文件") 170 | return 171 | 172 | for idx, file in enumerate(files_to_rename): 173 | dir_path = os.path.dirname(file) 174 | file_name,new_name = self.get_new_name(file,idx) 175 | new_path = os.path.join(dir_path,new_name) 176 | os.rename(file,os.path.join(dir_path,new_path)) 177 | self.file_list.remove(file) 178 | self.file_list.append(new_path) 179 | self.file_listbox.delete(idx) 180 | self.file_listbox.insert(idx, new_name) 181 | self.file_listbox.insert(tk.END,"重命名成功") 182 | 183 | def preview_rename(self): 184 | files_to_rename = self.file_list[:] 185 | self.file_listbox.delete(0, tk.END) 186 | if not files_to_rename: 187 | self.file_listbox.insert(tk.END,"未选择文件") 188 | return 189 | previews = [] 190 | for idx, file in enumerate(files_to_rename): 191 | file_name,new_name = self.get_new_name(file,idx) 192 | previews.append(f"{file_name} => {new_name}") 193 | for res in previews: 194 | self.file_listbox.insert(tk.END,res) 195 | 196 | if __name__ == "__main__": 197 | root = tk.Tk() 198 | app = FileRenamerApp(root) 199 | sw = root.winfo_screenwidth() 200 | sh = root.winfo_screenheight() 201 | ww = 700 202 | wh = 500 203 | x = (sw - ww) / 2 204 | y = (sh - wh) / 2 - 60 205 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 206 | root.mainloop() 207 | -------------------------------------------------------------------------------- /字幕处理/ass2srt.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | 5 | class Ass2srt: 6 | def __init__(self, filename): 7 | self.filename = filename 8 | self.load() 9 | 10 | def output_name(self, tag=None): 11 | outputfile = self.filename[0:-4] 12 | if tag: 13 | outputfile = outputfile+"."+tag 14 | return outputfile+".srt" 15 | 16 | def load(self, filename=None): 17 | if filename is None: 18 | filename = self.filename 19 | 20 | with open(file=filename, mode="r", encoding="utf-8") as f: 21 | data = f.readlines() 22 | 23 | self.nodes = [] 24 | for line in data: 25 | if line.startswith("Dialogue"): 26 | line = line.lstrip("Dialogue:") 27 | node = line.split(",") 28 | node[1] = timefmt(node[1]) 29 | node[2] = timefmt(node[2]) 30 | node[9] = re.sub(r'{.*}', "", node[9]).strip() 31 | node[9] = re.sub(r'\\N', "\n", node[9]) 32 | self.nodes.append(node) 33 | # print(f"{node[1]}-->{node[2]}:{node[9]}\n") 34 | 35 | def to_srt(self, name=None, line=0, tag=None): 36 | if name is None: 37 | name = self.output_name(tag=tag) 38 | with open(file=name, mode="w", encoding="utf-8") as f: 39 | index = 1 40 | for node in self.nodes: 41 | f.writelines(f"{index}\n") 42 | f.writelines(f"{node[1]} --> {node[2]}\n") 43 | if line == 1: 44 | text = node[9].split("\n")[0] 45 | elif line == 2: 46 | tmp = node[9].split("\n") 47 | if len(tmp) > 1: 48 | text = tmp[1] 49 | else: 50 | text = node[9] 51 | f.writelines(f"{text}\n\n") 52 | index += 1 53 | # print(f"字幕转换完成:{self.filename}-->{name}") 54 | print(f"字幕转换成功") 55 | 56 | def __str__(self): 57 | return f"文件名:{self.filename}\n合计{len(self.nodes)}条字幕\n" 58 | 59 | 60 | def timefmt(strt): 61 | strt = strt.replace(".", ",") 62 | return f"{strt}0" 63 | 64 | 65 | def main(): 66 | import argparse 67 | parser = argparse.ArgumentParser() 68 | parser.add_argument("file", help=".ass file to convert") 69 | parser.add_argument("-s", "--suffix", default="zh", choices=["zh", "en", "fr", "de"], 70 | help="add suffix to subtitles name") 71 | parser.add_argument("-l", "--line", type=int, 72 | choices=[0, 1, 2], default=0, help="keep double subtitles") 73 | parser.add_argument("-i", "--info", action="store_true", 74 | help="display subtitles infomation") 75 | parser.add_argument("-o", "--out", help="output file name") 76 | 77 | args = parser.parse_args() 78 | 79 | if args.file is None: 80 | parser.print_help() 81 | 82 | app = Ass2srt(args.file) 83 | if args.info: 84 | print(app) 85 | sys.exit() 86 | 87 | line = 0 88 | if args.line: 89 | line = args.line 90 | 91 | app.to_srt(name=args.out, line=line, tag=args.suffix) 92 | 93 | 94 | if __name__ == "__main__": 95 | # main() 96 | ass_path = sys.argv[1] 97 | subtitles=Ass2srt(ass_path) 98 | subtitles.to_srt() 99 | -------------------------------------------------------------------------------- /字幕处理/ass_cunstomer_folder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import pyperclip 4 | 5 | if __name__ == '__main__': 6 | folder_path = pyperclip.paste() 7 | for root, _, files in os.walk(folder_path): 8 | for file in files: 9 | if file.endswith('.ass'): 10 | ass_path = os.path.join(root, file) 11 | with open(ass_path,'r',encoding='utf-8') as f: 12 | sub_text = f.read() 13 | # defautl_style = "Style: Default,Arial,22,&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,5,5,5,134\nStyle: Eng,Arial,13,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,2,0,0,3,1" 14 | defautl_style = "Style: Default,STKaiti,20,&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,5,5,15,134\nStyle: Eng,Microsoft YaHei,14,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,0.3,0.1,2,0,0,3,1" 15 | sub_text = re.sub("{[^}]+}", "",sub_text) 16 | sub_text = re.sub("Style: Default,.*",defautl_style,sub_text) 17 | sub_text = sub_text.replace("\\N", "\\N{\\rEng}") 18 | with open(ass_path,'w',encoding='utf-8') as f: 19 | f.write(sub_text) 20 | print(f"{ass_path}字幕修改成功") -------------------------------------------------------------------------------- /字幕处理/ass_customer-中英单行.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/ass_customer-多线程.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import re 6 | import threading 7 | import configparser 8 | 9 | class SubtitleModifier: 10 | def __init__(self, root): 11 | self.root = root 12 | self.root.title("字幕修改工具") 13 | # 创建配置文件对象 14 | if not os.path.exists(self.get_ini_path()): 15 | with open(self.get_ini_path(),'w') as f: 16 | f.write('') 17 | self.config = configparser.ConfigParser(default_section='Settings') 18 | self.config.read(self.get_ini_path()) # 读取配置文件 19 | 20 | self.folder_path = tk.StringVar() 21 | self.double_language = tk.BooleanVar() 22 | self.font_name = tk.StringVar() 23 | self.font_size = tk.StringVar() 24 | self.out_line = tk.StringVar() 25 | self.margin_v = tk.StringVar() 26 | 27 | # 初始化选项的默认值 28 | self.double_language.set(True) 29 | self.font_options = ["STKaiti", "Arial", "微软雅黑","方正黑体"] 30 | self.font_name.set("STKaiti") 31 | self.font_size.set("22") 32 | self.out_line.set("1") 33 | self.margin_v.set("35") 34 | 35 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 36 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 37 | self.create_widgets() 38 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 39 | self.double_language.trace_add("write", self.double_language_changed) 40 | 41 | 42 | def create_widgets(self): 43 | frame_file = ttk.LabelFrame(self.root, text="选择文件夹") 44 | frame_file.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 45 | 46 | tk.Label(frame_file, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 47 | tk.Entry(frame_file, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 48 | tk.Button(frame_file, text="浏览", command=self.browse_file_or_folder).grid(row=0, column=2, padx=5, pady=5) 49 | 50 | frame_options = ttk.LabelFrame(self.root, text="修改选项") 51 | frame_options.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 52 | 53 | tk.Checkbutton(frame_options, text="双语类型", variable=self.double_language).grid(row=0, column=0, padx=5, pady=5) 54 | 55 | # tk.Label(frame_options, text="字体名称:").grid(row=1, column=0, padx=5, pady=5) 56 | # tk.Entry(frame_options, textvariable=self.font_name, width=20).grid(row=1, column=1, padx=5, pady=5) 57 | tk.Label(frame_options, text="字体名称:").grid(row=1, column=0, padx=5, pady=5) 58 | tk.OptionMenu(frame_options, self.font_name, *self.font_options).grid(row=1, column=1, padx=5, pady=5) 59 | 60 | tk.Label(frame_options, text="字体大小:").grid(row=2, column=0, padx=5, pady=5) 61 | tk.Entry(frame_options, textvariable=self.font_size, width=10).grid(row=2, column=1, padx=5, pady=5) 62 | 63 | tk.Label(frame_options, text="边框大小:").grid(row=3, column=0, padx=5, pady=5) 64 | tk.Entry(frame_options, textvariable=self.out_line, width=10).grid(row=3, column=1, padx=5, pady=5) 65 | 66 | tk.Label(frame_options, text="下边距:").grid(row=4, column=0, padx=5, pady=5) 67 | tk.Entry(frame_options, textvariable=self.margin_v, width=10).grid(row=4, column=1, padx=5, pady=5) 68 | 69 | tk.Button(self.root, text="开始修改", command=self.start_modify).pack(pady=10) 70 | 71 | self.result_text = tk.Text(self.root, wrap=tk.WORD, height=8) 72 | self.result_text.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 73 | 74 | def browse_file_or_folder(self): 75 | path = filedialog.askdirectory(initialdir=self.folder_path.get()) 76 | if path: 77 | self.folder_path.set(path) 78 | 79 | def get_ini_path(self,*args): 80 | # 获取当前.py文件的绝对路径 81 | current_script = os.path.abspath(__file__) 82 | # 获取文件名(不包括后缀) 83 | # script_name = os.path.splitext(os.path.basename(current_script))[0] + '.ini' 84 | ini_path = current_script.replace('.py','.ini') 85 | return(ini_path) 86 | 87 | def folder_path_changed(self, *args): 88 | # 这个函数会在文件夹路径变化时触发 89 | folder_path = self.folder_path.get() 90 | # 保存文件夹路径到配置文件 91 | folder_root = os.path.dirname(folder_path) 92 | self.config.set('Settings', 'last_folder', folder_root) 93 | with open(self.get_ini_path(), 'w') as configfile: 94 | self.config.write(configfile) 95 | 96 | def double_language_changed(self,*args): 97 | double_language = self.double_language.get() 98 | if double_language: 99 | self.font_name.set('STKaiti') 100 | else: 101 | self.font_name.set('Arial') 102 | 103 | def start_modify(self): 104 | folder_path = self.folder_path.get() 105 | double_language = self.double_language.get() 106 | font_name = self.font_name.get() 107 | font_size = self.font_size.get() 108 | out_line = self.out_line.get() 109 | margin_v = self.margin_v.get() 110 | self.result_text.delete(1.0,tk.END) 111 | 112 | if not folder_path: 113 | tk.messagebox.showerror("错误", "请选择文件夹") 114 | return 115 | 116 | if os.path.isdir(folder_path): 117 | thread = threading.Thread(target=self.modify_subtitles_in_folder, args=(folder_path, double_language, font_name, font_size, out_line, margin_v)) 118 | thread.start() 119 | # self.modify_subtitles_in_folder(folder_path, double_language, font_name, font_size, out_line, margin_v) 120 | else: 121 | tk.messagebox.showerror("错误", "无效的文件夹路径") 122 | 123 | def modify_subtitle(self, ass_path, double_language, font_name, font_size, out_line, margin_v): 124 | try: 125 | with open(ass_path, 'r', encoding='utf-8') as f: 126 | sub_text = f.read() 127 | font_size_eng = str(int(int(font_size) * 0.618) + 1) 128 | chi_eng_style = f"Style: Default,{font_name},{font_size},&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,{out_line},0.5,2,5,5,5,134\nStyle: Eng,Cronos Pro Subhead,{font_size_eng},&H3CF1F3,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,2,2,0,0,{margin_v},1" 129 | # chi_style = f"Style: Default,{font_name},{font_size},&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,{out_line},0.5,2,5,5,{margin_v},134" 130 | chi_style = f"Style: Default,{font_name},{font_size},&H00FFFFFF,&H000000FF,&H00604C24,&H00977736,-1,0,0,0,100,100,0,0,1,{out_line},0.5,2,5,5,{margin_v},134" 131 | sub_text = re.sub("Style:.*?\n", "", sub_text) 132 | sub_text = re.sub("{[^}]+}", "", sub_text) 133 | 134 | if double_language: 135 | default_style = chi_eng_style 136 | sub_text = sub_text.replace("\\N", "\\N{\\rEng}") 137 | else: 138 | default_style = chi_style 139 | 140 | sub_text = re.sub("Encoding", f"Encoding\n{default_style}", sub_text) 141 | 142 | with open(ass_path, 'w', encoding='utf-8') as f: 143 | f.write(sub_text) 144 | return True 145 | except Exception as e: 146 | return False 147 | 148 | def modify_subtitles_in_folder(self, folder_path, double_language, font_name, font_size, out_line, margin_v): 149 | for root, _, files in os.walk(folder_path): 150 | for file in files: 151 | if file.lower().endswith(".ass"): 152 | file_path = os.path.join(root, file) 153 | success = self.modify_subtitle(file_path, double_language, font_name, font_size, out_line, margin_v) 154 | if success: 155 | self.result_text.insert(tk.END, f"{file_path}: 字幕修改成功\n") 156 | else: 157 | self.result_text.insert(tk.END, f"{file_path}: 字幕修改失败\n") 158 | 159 | if __name__ == "__main__": 160 | root = tk.Tk() 161 | app = SubtitleModifier(root) 162 | sw = root.winfo_screenwidth() 163 | sh = root.winfo_screenheight() 164 | ww = 600 165 | wh = 450 166 | x = (sw - ww) / 2 167 | y = (sh - wh) / 2 - 60 168 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 169 | root.mainloop() 170 | -------------------------------------------------------------------------------- /字幕处理/ass_customer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | 5 | if __name__ == '__main__': 6 | ass_path = sys.argv[1] 7 | double_language = True 8 | with open(ass_path,'r',encoding='utf-8') as f: 9 | sub_text = f.read() 10 | font_name = 'STKaiti' 11 | font_size = '20' 12 | out_line = '2' 13 | margin_v = '15' 14 | chi_eng_style = f"Style: Default,{font_name},{font_size},&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,5,5,15,134\nStyle: Eng,Cronos Pro Subhead,14,&H3CF1F3,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,{out_line},2,0,0,{margin_v},1" 15 | chi_style = f"Style: Default,{font_name},{font_size},&H00FFFFFF,&HF0000000,&H006C3300,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,5,5,{margin_v},134" 16 | sub_text = re.sub("Style:.*?\n","",sub_text) 17 | sub_text = re.sub("{[^}]+}", "",sub_text) 18 | if double_language: 19 | defautl_style = chi_eng_style 20 | sub_text = sub_text.replace("\\N", "\\N{\\rEng}") 21 | else: 22 | defautl_style = chi_style 23 | sub_text = re.sub("Encoding",f"Encoding\n{defautl_style}",sub_text) 24 | with open(ass_path,'w',encoding='utf-8') as f: 25 | f.write(sub_text) 26 | print("字幕修改成功") -------------------------------------------------------------------------------- /字幕处理/ass_edit-多线程.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/Downloads 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/ass_edit-多线程.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import threading 6 | import configparser 7 | import codecs 8 | import re 9 | 10 | #弹幕:Dialogue:.*, 925,.*\n|Dialogue:.*, 950,.*\n|Dialogue:.*, 975,.*\n|Dialogue:.*, 1050,.*\n 11 | 12 | def detect_and_convert_encoding(input_file): 13 | encodings = ["utf-32", "utf-16", "utf-8", "cp1252", "gb2312", "gbk", "big5"] 14 | 15 | for enc in encodings: 16 | try: 17 | with codecs.open(input_file, mode="r", encoding=enc,errors="ignore") as fd: 18 | content = fd.read() 19 | 20 | # 如果文件内容不为空,则表示找到了正确的编码 21 | if content: 22 | with codecs.open(input_file, 'w', 'utf-8') as fd: 23 | fd.write(content) 24 | return True 25 | 26 | except Exception as e: 27 | continue 28 | 29 | class AssFileEditor: 30 | def __init__(self, root): 31 | self.root = root 32 | self.root.title("Ass-Srt文本替换工具") 33 | # 创建配置文件对象 34 | if not os.path.exists(self.get_ini_path()): 35 | with open(self.get_ini_path(),'w') as f: 36 | f.write('') 37 | self.config = configparser.ConfigParser(default_section='Settings') 38 | self.config.read(self.get_ini_path()) # 读取配置文件 39 | 40 | self.folder_path = tk.StringVar() 41 | self.search_str = tk.StringVar() 42 | self.replace_str = tk.StringVar() 43 | self.regex = tk.BooleanVar() 44 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 45 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 46 | 47 | self.create_widgets() 48 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 49 | 50 | def create_widgets(self): 51 | frame_folder = ttk.LabelFrame(self.root, text="选择文件夹") 52 | frame_folder.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 53 | 54 | tk.Label(frame_folder, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 55 | tk.Entry(frame_folder, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 56 | tk.Button(frame_folder, text="浏览", command=self.browse_folder).grid(row=0, column=2, padx=5, pady=5) 57 | # 创建 LabelFrame 58 | frame_replace = ttk.LabelFrame(root, text="替换设置") 59 | frame_replace.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 60 | 61 | # 创建 Checkbutton,并放置在 LabelFrame 的最左边 62 | checkbutton = tk.Checkbutton(frame_replace, text="正则") 63 | checkbutton.grid(row=0, column=0, padx=5, pady=5) 64 | self.regex = tk.BooleanVar() 65 | checkbutton['variable'] = self.regex 66 | self.regex.set(False) 67 | 68 | # 第一行 69 | tk.Label(frame_replace, text="查找:").grid(row=1, column=0, padx=5, pady=5) 70 | tk.Entry(frame_replace, textvariable=self.search_str, width=40).grid(row=1, column=1, padx=5, pady=5) 71 | 72 | # 第二行 73 | tk.Label(frame_replace, text="替换:").grid(row=2, column=0, padx=5, pady=5) 74 | tk.Entry(frame_replace, textvariable=self.replace_str, width=40).grid(row=2, column=1, padx=5, pady=5) 75 | 76 | tk.Button(self.root, text="开始替换", command=self.start_replace_thread).pack(pady=10) 77 | 78 | frame_output = ttk.LabelFrame(self.root, text="处理日志") 79 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 80 | 81 | self.output_text = tk.Text(frame_output, wrap=tk.WORD, height=10) 82 | self.output_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 83 | 84 | def get_ini_path(self,*args): 85 | # 获取当前.py文件的绝对路径 86 | current_script = os.path.abspath(__file__) 87 | # 获取文件名(不包括后缀) 88 | # script_name = os.path.splitext(os.path.basename(current_script))[0] + '.ini' 89 | ini_path = current_script.replace('.py','.ini') 90 | return(ini_path) 91 | 92 | def folder_path_changed(self, *args): 93 | # 这个函数会在文件夹路径变化时触发 94 | folder_path = self.folder_path.get() 95 | # 保存文件夹路径到配置文件 96 | folder_root = os.path.dirname(folder_path) 97 | self.config.set('Settings', 'last_folder', folder_root) 98 | with open(self.get_ini_path(), 'w') as configfile: 99 | self.config.write(configfile) 100 | 101 | def browse_folder(self): 102 | folder_selected = filedialog.askdirectory(initialdir=self.folder_path.get()) 103 | if folder_selected: 104 | self.folder_path.set(folder_selected) 105 | 106 | def replace_files(self): 107 | folder_path = self.folder_path.get() 108 | search_str = self.search_str.get() 109 | replace_str = self.replace_str.get() 110 | 111 | if not folder_path: 112 | tk.messagebox.showerror("错误", "请选择文件夹") 113 | return 114 | 115 | if not search_str: 116 | tk.messagebox.showerror("错误", "请输入要替换的字符串") 117 | return 118 | 119 | self.output_text.delete(1.0, tk.END) 120 | 121 | for root, _, files in os.walk(folder_path): 122 | for file in files: 123 | if file.endswith('.ass') or file.endswith('.srt'): 124 | file_path = os.path.join(root, file) 125 | try: 126 | with open(file_path, 'r', encoding='utf-8') as f: 127 | content = f.read() 128 | except UnicodeDecodeError: 129 | detect_and_convert_encoding(file_path) 130 | with open(file_path, 'r', encoding='utf-8') as f: 131 | content = f.read() 132 | if self.regex: 133 | content = re.sub(search_str,replace_str,content) 134 | else: 135 | content = content.replace(search_str.strip(), replace_str.strip()) 136 | 137 | with open(file_path, 'w', encoding='utf-8') as f: 138 | f.write(content) 139 | 140 | self.insert_message(f"{file_path}\n替换成功.\n") 141 | self.output_text.see(tk.END) 142 | self.insert_message(f"全部替换完成.\n") 143 | 144 | def insert_message(self,message): 145 | self.output_text.insert(tk.END,message) 146 | self.output_text.see(tk.END) 147 | 148 | def start_replace_thread(self): 149 | # 创建一个线程来执行文件替换任务 150 | replace_thread = threading.Thread(target=self.replace_files) 151 | replace_thread.start() 152 | 153 | if __name__ == "__main__": 154 | root = tk.Tk() 155 | app = AssFileEditor(root) 156 | sw = root.winfo_screenwidth() 157 | sh = root.winfo_screenheight() 158 | ww = 600 159 | wh = 450 160 | x = (sw - ww) / 2 161 | y = (sh - wh) / 2 - 60 162 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 163 | root.mainloop() 164 | -------------------------------------------------------------------------------- /字幕处理/ass_edit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | 6 | class AssFileEditor: 7 | def __init__(self, root): 8 | self.root = root 9 | self.root.title("ASS文件替换工具") 10 | 11 | self.folder_path = tk.StringVar() 12 | self.search_str = tk.StringVar() 13 | self.replace_str = tk.StringVar() 14 | 15 | self.create_widgets() 16 | 17 | def create_widgets(self): 18 | frame_folder = ttk.LabelFrame(self.root, text="选择文件夹") 19 | frame_folder.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 20 | 21 | tk.Label(frame_folder, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 22 | tk.Entry(frame_folder, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 23 | tk.Button(frame_folder, text="浏览", command=self.browse_folder).grid(row=0, column=2, padx=5, pady=5) 24 | 25 | frame_replace = ttk.LabelFrame(self.root, text="替换设置") 26 | frame_replace.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 27 | 28 | tk.Label(frame_replace, text="要替换的字符串:").grid(row=0, column=0, padx=5, pady=5) 29 | tk.Entry(frame_replace, textvariable=self.search_str, width=40).grid(row=0, column=1, padx=5, pady=5) 30 | 31 | tk.Label(frame_replace, text="替换后的字符串:").grid(row=1, column=0, padx=5, pady=5) 32 | tk.Entry(frame_replace, textvariable=self.replace_str, width=40).grid(row=1, column=1, padx=5, pady=5) 33 | 34 | tk.Button(self.root, text="开始替换", command=self.replace_files).pack(pady=10) 35 | 36 | frame_output = ttk.LabelFrame(self.root, text="处理日志") 37 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 38 | 39 | self.output_text = tk.Text(frame_output, wrap=tk.WORD, height=10) 40 | self.output_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 41 | 42 | def browse_folder(self): 43 | folder_selected = filedialog.askdirectory() 44 | if folder_selected: 45 | self.folder_path.set(folder_selected) 46 | 47 | def replace_files(self): 48 | folder_path = self.folder_path.get() 49 | search_str = self.search_str.get() 50 | replace_str = self.replace_str.get() 51 | 52 | if not folder_path: 53 | tk.messagebox.showerror("错误", "请选择文件夹") 54 | return 55 | 56 | if not search_str: 57 | tk.messagebox.showerror("错误", "请输入要替换的字符串") 58 | return 59 | 60 | self.output_text.delete(1.0, tk.END) 61 | 62 | for root, _, files in os.walk(folder_path): 63 | for file in files: 64 | if file.endswith('.ass'): 65 | file_path = os.path.join(root, file) 66 | with open(file_path, 'r', encoding='utf-8') as f: 67 | content = f.read() 68 | 69 | os.remove(file_path) #挂载的网盘需要先将文件删除后再重新写入才行 70 | content = content.replace(search_str, replace_str) 71 | 72 | with open(file_path, 'w', encoding='utf-8') as f: 73 | f.write(content) 74 | 75 | self.output_text.insert(tk.END, f"{file_path}\n") 76 | 77 | # tk.messagebox.showinfo("完成", "所有文件替换完成") 78 | 79 | if __name__ == "__main__": 80 | root = tk.Tk() 81 | app = AssFileEditor(root) 82 | sw = root.winfo_screenwidth() # 得到屏幕宽度 83 | sh = root.winfo_screenheight() # 得到屏幕高度 84 | ww = 600 85 | wh = 450 86 | x = (sw - ww) / 2 87 | y = (sh - wh) / 2 - 60 88 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) # 窗口居中 89 | root.mainloop() 90 | -------------------------------------------------------------------------------- /字幕处理/srt2ass_folder-多线程.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/Downloads 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/sub_get-多线程.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/Downloads 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/sub_get-多线程.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import subprocess 6 | import threading 7 | import configparser 8 | import shenmail 9 | import re 10 | 11 | def get_sub_info(mkv_path): 12 | ffmpeg_command = f'ffprobe -v error -select_streams s -show_entries stream=index:stream_tags=language:format_tags=title -of default=noprint_wrappers=1 "{mkv_path}"' 13 | output_txt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),'sub_info.txt') 14 | with open(output_txt_path, 'w', encoding='utf-8') as output_file: 15 | try: 16 | subprocess.run(ffmpeg_command, stdout=output_file, stderr=subprocess.STDOUT, text=True, check=True) 17 | except subprocess.CalledProcessError as e: 18 | # 如果出现非零返回码,也将错误信息写入文件 19 | output_file.write(str(e.output)) 20 | 21 | # 使用正则表达式提取字幕信息 22 | with open(output_txt_path, 'r', encoding='utf-8') as f: 23 | content = f.read().replace("subrip",'srt') 24 | subtitle_info = [] 25 | lines = content.split('\n') 26 | for line in lines: 27 | if re.search(r'Stream #0:\d+\(.*?\): Subtitle: .*', line): 28 | stream_match = re.search(r'Stream #0:(\d+)\((.*?)\): Subtitle: (.*)', line) 29 | if stream_match: 30 | stream_number = stream_match.group(1).strip() 31 | subtitle_lang = stream_match.group(2).strip() 32 | subtitle_type = re.sub('\(.*?\)','',stream_match.group(3)).strip() 33 | subtitle_info.append([stream_number, subtitle_type, "",subtitle_lang]) 34 | elif re.search(r'title\s+:\s+(.+)', line): 35 | title_match = re.search(r'title\s+:\s+(.+)', line) 36 | if title_match and subtitle_info: 37 | subtitle_info[-1][2] = title_match.group(1).strip() 38 | print('字幕信息:\n') 39 | for index,sub_type,title,subtitle_lang in [item for item in subtitle_info if item[2]]: 40 | sub_info = f"{int(index.strip())-1}:{title.strip()}.{sub_type}\n" 41 | print(sub_info) 42 | chi_sub = [item for item in subtitle_info if "chi" in item[3]] 43 | chi_sub = [item for item in chi_sub if "简" in item[2] or "Simplified" in item[2] or "chs" in item[2]] 44 | chi_sub_res = [('1','srt','chi')] 45 | if chi_sub: 46 | print(f'找到中文字幕:\n') 47 | chi_sub_res = chi_sub[:] 48 | sub_num = str(int(chi_sub_res[0][0])-1) 49 | sub_ext = chi_sub_res[0][1] 50 | return str(int(sub_num) - 1),sub_ext 51 | 52 | class SubtitleExtractor: 53 | def __init__(self, root): 54 | self.root = root 55 | self.root.title("字幕提取工具") 56 | 57 | # 创建配置文件对象 58 | if not os.path.exists(self.get_ini_path()): 59 | with open(self.get_ini_path(),'w') as f: 60 | f.write('') 61 | self.config = configparser.ConfigParser(default_section='Settings') 62 | self.config.read(self.get_ini_path()) # 读取配置文件 63 | 64 | self.font_types = ["srt","ass"] 65 | 66 | self.folder_path = tk.StringVar() 67 | self.sub_num = tk.StringVar() 68 | self.sub_ext = tk.StringVar() 69 | self.progress_text = tk.StringVar() 70 | self.auto_process = tk.BooleanVar() 71 | 72 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 73 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 74 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 75 | 76 | self.create_widgets() 77 | 78 | def create_widgets(self): 79 | frame_folder = ttk.LabelFrame(self.root, text="选择文件夹") 80 | frame_folder.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 81 | 82 | tk.Label(frame_folder, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 83 | tk.Entry(frame_folder, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 84 | tk.Button(frame_folder, text="浏览", command=self.browse_folder).grid(row=0, column=2, padx=5, pady=5) 85 | 86 | frame_options = ttk.LabelFrame(self.root, text="提取选项") 87 | frame_options.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 88 | 89 | tk.Label(frame_options, text="字幕流编号:").grid(row=0, column=0, padx=5, pady=5) 90 | self.sub_num_combo = ttk.Combobox(frame_options, textvariable=self.sub_num, state="readonly") 91 | self.sub_num_combo['values'] = list(range(1, 40)) # 设置下拉菜单范围为1到39 92 | self.sub_num_combo.grid(row=0, column=1, padx=5, pady=5) 93 | self.sub_num_combo.set('1') # 设置默认选择项 94 | 95 | 96 | tk.Label(frame_options, text="字幕格式:").grid(row=0, column=2, padx=5, pady=5) 97 | # tk.Entry(frame_options, textvariable=self.sub_ext, width=10).grid(row=0, column=3, padx=5, pady=5) 98 | tk.OptionMenu(frame_options, self.sub_ext, *self.font_types).grid(row=0, column=3, padx=5, pady=5) 99 | self.sub_ext.set("srt") 100 | tk.Checkbutton(frame_options, text="自动识别", variable=self.auto_process).grid(row=0, column=4, padx=5, pady=5) 101 | self.auto_process.set(True) 102 | 103 | button_frame = tk.Frame(self.root) 104 | button_frame.pack(padx=10, pady=5) 105 | 106 | tk.Button(button_frame, text="获取信息", command=self.start_get_sub_info).pack(side=tk.LEFT, padx=10,pady=10) 107 | tk.Button(button_frame, text="开始提取", command=self.start_extraction).pack(side=tk.LEFT, padx=10,pady=10) 108 | 109 | 110 | frame_output = ttk.LabelFrame(self.root, text="处理日志") 111 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 112 | 113 | self.output_text = tk.Text(frame_output, wrap=tk.WORD, height=10) 114 | self.output_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 115 | 116 | tk.Label(frame_output, textvariable=self.progress_text).pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 117 | 118 | def browse_folder(self): 119 | folder_selected = filedialog.askdirectory(initialdir=self.folder_path.get()) # 设置initialdir为当前文件夹路径 120 | if folder_selected: 121 | self.folder_path.set(folder_selected) 122 | 123 | def get_ini_path(self,*args): 124 | # 获取当前.py文件的绝对路径 125 | current_script = os.path.abspath(__file__) 126 | ini_path = current_script.replace('.py','.ini') 127 | return(ini_path) 128 | 129 | def insert_message(self,message): 130 | self.output_text.insert(tk.END,message) 131 | self.output_text.see(tk.END) 132 | 133 | def start_get_sub_info(self): 134 | folder_path = self.folder_path.get() 135 | self.progress_text.set("正在获取字幕信息...") 136 | self.output_text.delete(1.0, tk.END) 137 | mkv_path = '' 138 | for root, _, files in os.walk(folder_path): 139 | for file in files: 140 | if file.endswith('.mkv') or file.endswith('.mp4'): 141 | mkv_path = os.path.join(root, file) 142 | break 143 | if mkv_path: 144 | thread = threading.Thread(target=self.get_sub_info, args=(mkv_path,)) 145 | thread.start() 146 | 147 | def get_sub_info(self,mkv_path): 148 | ffmpeg_command = ['ffmpeg', '-i', mkv_path] 149 | output_txt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),'sub_info.txt') 150 | with open(output_txt_path, 'w', encoding='utf-8') as output_file: 151 | try: 152 | subprocess.run(ffmpeg_command, stdout=output_file, stderr=subprocess.STDOUT, text=True, check=True) 153 | except subprocess.CalledProcessError as e: 154 | # 如果出现非零返回码,也将错误信息写入文件 155 | output_file.write(str(e.output)) 156 | 157 | # 使用正则表达式提取字幕信息 158 | try: 159 | with open(output_txt_path, 'r', encoding='utf-8') as f: 160 | content = f.read().replace("subrip",'srt') 161 | subtitle_info = [] 162 | lines = content.split('\n') 163 | for line in lines: 164 | if re.search(r'Stream #0:\d+\(.*?\): Subtitle: .*', line): 165 | stream_match = re.search(r'Stream #0:(\d+)\((.*?)\): Subtitle: (.*)', line) 166 | if stream_match: 167 | stream_number = stream_match.group(1).strip() 168 | subtitle_lang = stream_match.group(2).strip() 169 | subtitle_type = re.sub('\(.*?\)','',stream_match.group(3)).strip() 170 | subtitle_info.append([stream_number, subtitle_type, "",subtitle_lang]) 171 | elif re.search(r'title\s+:\s+(.+)', line): 172 | title_match = re.search(r'title\s+:\s+(.+)', line) 173 | if title_match and subtitle_info: 174 | subtitle_info[-1][2] = title_match.group(1).strip() 175 | self.insert_message('字幕信息:\n') 176 | for index,sub_type,title,subtitle_lang in [item for item in subtitle_info if item[2]]: 177 | sub_info = f"{int(index.strip())-1}:{title.strip()}.{sub_type}\n" 178 | self.insert_message(sub_info) 179 | chi_sub = [item for item in subtitle_info if "chi" in item[3] or "Chi" in item[3] or 'zh' in item[3]] 180 | chi_sub = [item for item in chi_sub if "简" in item[2] or "Simplified" in item[2] or "chs" in item[2]] 181 | chi_sub_res = [('1','srt','chi')] 182 | if chi_sub: 183 | self.insert_message(f'找到中文字幕:\n') 184 | chi_sub_res = chi_sub[:] 185 | sub_num = str(int(chi_sub_res[0][0])-1) 186 | sub_ext = chi_sub_res[0][1] 187 | sub_name = chi_sub_res[0][2] 188 | self.insert_message(f'序号:{sub_num},名称:{sub_name},类型:{sub_ext}\n') 189 | self.sub_num.set(sub_num) 190 | self.sub_ext.set(sub_ext) 191 | self.progress_text.set(f"字幕信息获取成功") 192 | return int(sub_num)-1,sub_ext 193 | except Exception as e: 194 | self.insert_message(f"发生错误:{str(e)}\n") 195 | return 196 | 197 | def folder_path_changed(self, *args): 198 | # 这个函数会在文件夹路径变化时触发 199 | folder_path = self.folder_path.get() 200 | # 保存文件夹路径到配置文件 201 | folder_root = os.path.dirname(folder_path) 202 | self.config.set('Settings', 'last_folder', folder_root) 203 | with open(self.get_ini_path(), 'w') as configfile: 204 | self.config.write(configfile) 205 | 206 | def start_extraction(self): 207 | folder_path = self.folder_path.get() 208 | sub_num = self.sub_num.get() 209 | sub_num = str(int(sub_num) - 1) 210 | sub_ext = self.sub_ext.get() 211 | auto_process = self.auto_process.get() 212 | if not folder_path: 213 | tk.messagebox.showerror("错误", "请选择文件夹") 214 | return 215 | 216 | if not sub_num: 217 | tk.messagebox.showerror("错误", "请输入字幕流编号") 218 | return 219 | 220 | if not sub_ext: 221 | tk.messagebox.showerror("错误", "请输入字幕格式") 222 | return 223 | 224 | self.progress_text.set("正在提取字幕...") 225 | self.output_text.delete(1.0, tk.END) 226 | thread = threading.Thread(target=self.extract_subtitles, args=(folder_path, sub_num, sub_ext,auto_process)) 227 | thread.start() 228 | 229 | def extract_subtitles(self, folder_path, sub_num, sub_ext,auto_process): 230 | count = 0 231 | for root, _, files in os.walk(folder_path): 232 | for file in files: 233 | if file.endswith('.mkv') or file.endswith('.mp4'): 234 | mkv_path = os.path.join(root, file) 235 | if auto_process: 236 | sub_num,sub_ext = self.get_sub_info(mkv_path) 237 | self.sub_num.set(sub_num) 238 | self.sub_ext.set(sub_ext) 239 | output_subfile = f'{os.path.splitext(mkv_path)[0]}.chs.{sub_ext}' 240 | if os.path.exists(output_subfile): 241 | os.remove(output_subfile) 242 | cmd = f'ffmpeg -i "{mkv_path}" -map 0:s:{sub_num} "{output_subfile}"' 243 | os.system(cmd) 244 | count += 1 245 | self.progress_text.set(f"已提取 {count} 个字幕") 246 | self.insert_message(f"成功提取字幕: {file}\n") 247 | 248 | self.progress_text.set(f"提取完成,共提取 {count} 个字幕") 249 | shenmail.send_bark(f'字幕提取成功\n{folder_path}\n成功提取{count}个字幕') 250 | 251 | 252 | if __name__ == "__main__": 253 | root = tk.Tk() 254 | app = SubtitleExtractor(root) 255 | sw = root.winfo_screenwidth() 256 | sh = root.winfo_screenheight() 257 | ww = 600 258 | wh = 450 259 | x = (sw - ww) / 2 260 | y = (sh - wh) / 2 - 60 261 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 262 | root.mainloop() -------------------------------------------------------------------------------- /字幕处理/sub_get.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import threading 4 | import shenmail 5 | import sys 6 | 7 | class SubtitleExtractor: 8 | def __init__(self, folder_path, sub_num, sub_ext): 9 | self.folder_path = folder_path 10 | self.sub_num = int(sub_num) - 1 11 | self.sub_ext = sub_ext 12 | 13 | def extract_subtitles(self): 14 | count = 0 15 | for root, _, files in os.walk(self.folder_path): 16 | for file in files: 17 | if file.endswith('.mkv') or file.endswith('.mp4'): 18 | mkv_path = os.path.join(root, file) 19 | 20 | output_subfile = f'{os.path.splitext(mkv_path)[0]}.chs.{self.sub_ext}' 21 | if os.path.exists(output_subfile): 22 | os.remove(output_subfile) 23 | cmd = f'ffmpeg -i "{mkv_path}" -map 0:s:{self.sub_num} "{output_subfile}"' 24 | os.system(cmd) 25 | count += 1 26 | print(f"成功提取字幕: {file}") 27 | 28 | print(f"提取完成,共提取 {count} 个字幕") 29 | shenmail.send_bark(f'字幕提取成功\n{self.folder_path}\n成功提取{count}个字幕') 30 | 31 | def main(): 32 | if len(sys.argv) != 4: 33 | print("请提供文件夹路径、字幕流编号和字幕格式作为参数。") 34 | print("示例: python script.py '/path/to/folder' '1' 'srt'") 35 | sys.exit(1) 36 | 37 | folder_path = sys.argv[1] 38 | sub_num = sys.argv[2] 39 | sub_ext = sys.argv[3] 40 | 41 | extractor = SubtitleExtractor(folder_path, sub_num, sub_ext) 42 | extractor.extract_subtitles() 43 | 44 | if __name__ == "__main__": 45 | main() -------------------------------------------------------------------------------- /字幕处理/去除顶部字幕.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | folder_path = '/Users/shenxian/Downloads/银魂' 5 | for root, _, files in os.walk(folder_path): 6 | for file in files: 7 | if file.endswith('ass'): 8 | file_path = os.path.join(root,file) 9 | with open(file_path,'r',encoding='utf-8') as f: 10 | content = f.read() 11 | content = re.sub('Dialogue.*?, 50\).*','',content) 12 | content = content.replace('\n\n\nDialogue','\nDialogue') 13 | content = content.replace('\n\nDialogue','\nDialogue') 14 | content = content.replace('\n\nDialogue','\nDialogue') 15 | content = content.replace('50,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0','75,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1') 16 | with open(file_path,'w',encoding='utf-8') as f: 17 | f.write(content) 18 | if 'zh' not in file_path or 'chs' not in file_path: 19 | new_file_path = file_path.replace('ass','zh.ass').replace('chs.zh','zh') 20 | os.rename(file_path,new_file_path) 21 | print(file_path) 22 | -------------------------------------------------------------------------------- /字幕处理/字幕平移时间轴.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import codecs 4 | import pyperclip 5 | 6 | 7 | def detect_and_convert_encoding(input_file): 8 | encodings = ["utf-32", "utf-16", "utf-8", "cp1252", "gb2312", "gbk", "big5"] 9 | 10 | for enc in encodings: 11 | try: 12 | with codecs.open(input_file, mode="r", encoding=enc, errors="ignore") as fd: 13 | content = fd.read() 14 | 15 | # 如果文件内容不为空,则表示找到了正确的编码 16 | if content: 17 | with codecs.open(input_file, "w", "utf-8") as fd: 18 | fd.write(content) 19 | return True 20 | 21 | except Exception as e: 22 | continue 23 | # 如果所有编码都无法正常读取,则返回 False 24 | return False 25 | 26 | 27 | def shift_subtitle_timeline_srt(input_subtitle_path, shift_second): 28 | # 输入字幕路径 29 | if input_subtitle_path == 0: 30 | input_subtitle_path = input("请输入字幕文件路径:").replace('"', "") 31 | input_subtitle_name = input_subtitle_path.split("\\")[-1] 32 | input_subtitle_format = input_subtitle_name.split(".")[-1] 33 | # 确认字幕移动时间数值 34 | if shift_second == 0: 35 | shift_second = float( 36 | input("请输字幕时间线移动数值,正整数为向后移动,负整数为向前移:") 37 | ) 38 | # 获取字幕内容 39 | try: 40 | with open(input_subtitle_path, "r", encoding="UTF-8") as f: 41 | subtitle_data = f.readlines() 42 | except UnicodeDecodeError: 43 | detect_and_convert_encoding(input_subtitle_path) 44 | with open(input_subtitle_path, "r", encoding="UTF-8") as f: 45 | subtitle_data = f.readlines() 46 | # 提取时间点并修改 47 | for line in subtitle_data: 48 | if "-->" in line: 49 | where = subtitle_data.index(line) # 标记索引 50 | start, end = line.split(" --> ") # 开始结束时间 51 | start_h, start_m, start_s, start_ms = re.split(r"[:,.]", start) # 开始时间 52 | end_h, end_m, end_s, end_ms = re.split(r"[:,.]", end) # 结束时间 53 | start_total_ms = ( 54 | int(start_h) * 3600000 55 | + int(start_m) * 60000 56 | + int(start_s) * 1000 57 | + int(start_ms) 58 | ) 59 | end_total_ms = ( 60 | int(end_h) * 3600000 61 | + int(end_m) * 60000 62 | + int(end_s) * 1000 63 | + int(end_ms) 64 | ) 65 | if start_total_ms + shift_second >= 0: 66 | start_total_ms = ( 67 | start_total_ms + shift_second 68 | ) # 添加了移动时间的开始时间 69 | end_total_ms = end_total_ms + shift_second # 添加了移动时间的结束时间 70 | start_h, remain = divmod( 71 | start_total_ms, 3600000 72 | ) # 将开始时间转回时、分、秒、毫秒 73 | start_m, remain = divmod(remain, 60000) 74 | start_s, remain = divmod(remain, 1000) 75 | start_ms = remain 76 | end_h, remain = divmod( 77 | end_total_ms, 3600000 78 | ) # 将开始时间转回时、分、秒、毫秒 79 | end_m, remain = divmod(remain, 60000) 80 | end_s, remain = divmod(remain, 1000) 81 | end_ms = remain 82 | if input_subtitle_format == "srt": 83 | start = f"{start_h:02d}:{start_m:02d}:{start_s:02d},{start_ms:03d}" 84 | end = f"{end_h:02d}:{end_m:02d}:{end_s:02d},{end_ms:03d}" # 将结束时间换回00:00:00,000格式 85 | elif input_subtitle_format == "vtt": 86 | start = f"{start_h:02d}:{start_m:02d}:{start_s:02d}.{start_ms:03d}" 87 | end = f"{end_h:02d}:{end_m:02d}:{end_s:02d}.{end_ms:03d}" # 将结束时间换回00:00:00,000格式 88 | line = start + " --> " + end + "\n" 89 | subtitle_data[where] = line 90 | with open(input_subtitle_path, "w", encoding="UTF-8") as f: 91 | for line in subtitle_data: 92 | f.write(line) 93 | print(f"{input_subtitle_path}修改成功") 94 | 95 | 96 | def shift_subtitle_timeline_ass(input_subtitle_path, shift_second): 97 | # 输入字幕路径 98 | if input_subtitle_path == 0: 99 | input_subtitle_path = input("请输入字幕文件路径:").replace('"', "") 100 | input_subtitle_name = input_subtitle_path.split("\\")[-1] 101 | input_subtitle_format = input_subtitle_name.split(".")[-1] 102 | # 确认字幕移动时间数值 103 | if shift_second == 0: 104 | shift_second = float( 105 | input("请输字幕时间线移动数值,正整数为向后移动,负整数为向前移:") 106 | ) 107 | # 获取字幕内容 108 | try: 109 | with open(input_subtitle_path, "r", encoding="UTF-8") as f: 110 | subtitle_data = f.readlines() 111 | except UnicodeDecodeError: 112 | detect_and_convert_encoding(input_subtitle_path) 113 | with open(input_subtitle_path, "r", encoding="UTF-8") as f: 114 | subtitle_data = f.readlines() 115 | # 提取时间点并修改 116 | for line in subtitle_data: 117 | if "Dialogue" in line: 118 | where = subtitle_data.index(line) # 标记索引 119 | # line = line.replace('\n', '') # 去掉换行符 120 | start, end = re.findall("(\d+:\d+:\d+\.\d+)", line) # 开始结束时间 121 | old_time = f"{start},{end}" 122 | start_h, start_m, start_s, start_ms = re.split(r"[:.]", start) # 开始时间 123 | end_h, end_m, end_s, end_ms = re.split(r"[:.]", end) # 结束时间 124 | start_ms = float(f"0.{start_ms}") * 1000 125 | end_ms = float(f"0.{end_ms}") * 1000 126 | start_total_ms = ( 127 | int(start_h) * 3600000 128 | + int(start_m) * 60000 129 | + int(start_s) * 1000 130 | + int(start_ms) 131 | ) 132 | end_total_ms = ( 133 | int(end_h) * 3600000 134 | + int(end_m) * 60000 135 | + int(end_s) * 1000 136 | + int(end_ms) 137 | ) 138 | if start_total_ms + shift_second >= 0: 139 | start_total_ms = ( 140 | start_total_ms + shift_second 141 | ) # 添加了移动时间的开始时间 142 | end_total_ms = end_total_ms + shift_second # 添加了移动时间的结束时间 143 | start_h, remain = divmod( 144 | start_total_ms, 3600000 145 | ) # 将开始时间转回时、分、秒、毫秒 146 | start_m, remain = divmod(remain, 60000) 147 | start_s, remain = divmod(remain, 1000) 148 | start_ms = int(remain / 10) 149 | end_h, remain = divmod( 150 | end_total_ms, 3600000 151 | ) # 将开始时间转回时、分、秒、毫秒 152 | end_m, remain = divmod(remain, 60000) 153 | end_s, remain = divmod(remain, 1000) 154 | end_ms = int(remain / 10) 155 | if input_subtitle_format == "srt": 156 | start = f"{start_h:02d}:{start_m:02d}:{start_s:02d},{start_ms:02d}" 157 | end = f"{end_h:02d}:{end_m:02d}:{end_s:02d},{end_ms:03d}" # 将结束时间换回00:00:00,000格式 158 | elif input_subtitle_format == "ass": 159 | start = f"{start_h:02d}:{start_m:02d}:{start_s:02d}.{start_ms:02d}" 160 | end = f"{end_h:02d}:{end_m:02d}:{end_s:02d}.{end_ms:02d}" # 将结束时间换回00:00:00,000格式 161 | new_time = f"{start},{end}" 162 | line = line.replace(old_time, new_time) 163 | subtitle_data[where] = line 164 | with open(input_subtitle_path, "w", encoding="UTF-8") as f: 165 | for line in subtitle_data: 166 | f.write(line) 167 | print(f"{input_subtitle_path}修改成功") 168 | 169 | 170 | if __name__ == "__main__": 171 | folder_path = pyperclip.paste() 172 | time_delay = 50 173 | if os.path.isdir(folder_path): 174 | for root, _, files in os.walk(folder_path): 175 | for file in files: 176 | if file.endswith(".ass"): 177 | file_path = os.path.join(root, file) 178 | shift_subtitle_timeline_ass(file_path, time_delay) 179 | elif file.endswith(".srt"): 180 | file_path = os.path.join(root, file) 181 | shift_subtitle_timeline_srt(file_path, time_delay) 182 | else: 183 | file_path = folder_path 184 | if file_path.endswith(".ass"): 185 | shift_subtitle_timeline_ass(file_path, time_delay) 186 | elif file_path.endswith(".srt"): 187 | shift_subtitle_timeline_srt(file_path, time_delay) 188 | -------------------------------------------------------------------------------- /字幕处理/字幕时间同步-input.py: -------------------------------------------------------------------------------- 1 | import os 2 | from posixpath import basename 3 | import subprocess 4 | import sys 5 | 6 | alass_path = r'\volume1\Shencode\subsync\alass' 7 | def process_videos(search_directory): 8 | subtitle_ext_list = ['.srt', '.ass', '.zh.srt', '.zh.ass', 'chs.srt', '.chs.ass'] 9 | total_processed = 0 # 用于跟踪处理的字幕数量 10 | 11 | for root, dirs, files in os.walk(search_directory): 12 | for video_file in files: 13 | if video_file.lower().endswith(('.avi', '.mkv', '.wmv', '.mp4', '.mpeg', '.m4v')): 14 | video_filepath = os.path.join(root, video_file) 15 | video_filename = os.path.splitext(video_file)[0] 16 | video_name = os.path.splitext(video_filepath)[0] 17 | subtitle_path_list = [f'{video_name}{i}' for i in subtitle_ext_list] 18 | for subtitle_path in subtitle_path_list: 19 | if os.path.exists(subtitle_path): 20 | print(f"开始同步字幕时间轴: {subtitle_path}") 21 | file_name,ext = os.path.splitext(subtitle_path) 22 | cmd = [alass_path, video_filepath,subtitle_path,f'{file_name}.new{ext}'] 23 | subprocess.call(cmd) 24 | total_processed += 1 25 | print(f"成功同步字幕时间轴: {subtitle_path}") 26 | sys.exit() 27 | 28 | print(f"一共处理了 {total_processed} 个字幕。") 29 | 30 | if __name__ == "__main__": 31 | search_directory = input('请输入要同步的目录') 32 | search_directory = rf'{search_directory}' 33 | process_videos(search_directory) 34 | -------------------------------------------------------------------------------- /字幕处理/字幕时间同步-命令行.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import argparse 4 | 5 | alass_path = r'\volume1\Shencode\subsync\alass' 6 | def process_videos(search_directory): 7 | subtitle_ext_list = ['.srt', '.ass', '.zh.srt', '.zh.ass', 'chs.srt', '.chs.ass'] 8 | total_processed = 0 # 用于跟踪处理的字幕数量 9 | 10 | for root, dirs, files in os.walk(search_directory): 11 | for video_file in files: 12 | if video_file.lower().endswith(('.avi', '.mkv', '.wmv', '.mp4', '.mpeg', '.m4v')): 13 | video_filepath = os.path.join(root, video_file) 14 | video_filename = os.path.splitext(video_file)[0] 15 | video_name = os.path.splitext(video_filepath)[0] 16 | subtitle_path_list = [f'{video_name}{i}' for i in subtitle_ext_list] 17 | for subtitle_path in subtitle_path_list: 18 | if os.path.exists(subtitle_path): 19 | print(f"开始同步字幕时间轴: {subtitle_path}") 20 | cmd = [alass_path, video_filepath, subtitle_path, subtitle_path] 21 | subprocess.call(cmd) 22 | total_processed += 1 23 | print(f"成功同步字幕时间轴: {subtitle_path}") 24 | 25 | print(f"一共处理了 {total_processed} 个字幕。") 26 | 27 | if __name__ == "__main__": 28 | parser = argparse.ArgumentParser(description="字幕时间轴同步工具") 29 | parser.add_argument("search_directory", help="要同步的视频目录") 30 | 31 | args = parser.parse_args() 32 | search_directory = args.search_directory 33 | 34 | process_videos(search_directory) 35 | -------------------------------------------------------------------------------- /字幕处理/字幕时间轴同步-自定偏移.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/Desktop 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/字幕时间轴同步.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Volumes/dav/115/看剧/links/电影/欧美电影 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/字幕时间轴同步(原版).ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Volumes/dav/115/看剧/links/电影/欧美电影 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/字幕繁体转简体.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | import opencc 5 | import threading 6 | 7 | class SrtConversionApp: 8 | def __init__(self, root): 9 | self.root = root 10 | self.root.title("SRT文件转换工具") 11 | 12 | # 设置窗口大小和居中显示 13 | window_width = 600 14 | window_height = 300 15 | screen_width = root.winfo_screenwidth() 16 | screen_height = root.winfo_screenheight() 17 | x = (screen_width - window_width) // 2 18 | y = (screen_height - window_height) // 2 19 | root.geometry(f"{window_width}x{window_height}+{x}+{y}") 20 | 21 | self.folder_path = tk.StringVar() 22 | self.progress_text = tk.StringVar() 23 | 24 | self.create_widgets() 25 | 26 | def create_widgets(self): 27 | frame_top = tk.Frame(self.root) 28 | frame_top.pack(side=tk.TOP, padx=10, pady=10, fill=tk.X) 29 | 30 | folder_label = tk.Label(frame_top, text="选择文件夹路径:") 31 | folder_label.pack(side=tk.LEFT, padx=(0, 5), anchor="w") 32 | 33 | folder_entry = tk.Entry(frame_top, textvariable=self.folder_path, width=40) 34 | folder_entry.pack(side=tk.LEFT, padx=(0, 5)) 35 | 36 | browse_button = tk.Button(frame_top, text="浏览", command=self.browse_folder) 37 | browse_button.pack(side=tk.LEFT) 38 | 39 | convert_button = tk.Button(self.root, text="开始转换", command=self.start_conversion) 40 | convert_button.pack(padx=10, pady=5) 41 | 42 | frame_bottom = tk.Frame(self.root) 43 | frame_bottom.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) 44 | 45 | progress_label = tk.Label(frame_bottom, text="处理进度:") 46 | progress_label.pack(padx=10, pady=(0, 5), anchor="w") 47 | 48 | self.progress_text.set("") 49 | progress_textbox = tk.Text(frame_bottom, height=10, width=50) 50 | progress_textbox.pack(padx=10, pady=(0, 5), fill=tk.BOTH, expand=True) 51 | progress_textbox.config(state=tk.DISABLED) 52 | progress_textbox.tag_configure("center", justify="center") 53 | 54 | self.progress_textbox = progress_textbox 55 | 56 | def browse_folder(self): 57 | folder_selected = filedialog.askdirectory() 58 | if folder_selected: 59 | self.folder_path.set(folder_selected) 60 | 61 | def convert_srt_files_to_simplified_chinese(self): 62 | folder_path = self.folder_path.get() 63 | if not os.path.isdir(folder_path): 64 | self.progress_text.set("无效的文件夹路径。") 65 | return 66 | 67 | # 创建 OpenCC 实例,选择繁体字转换为简体字 68 | converter = opencc.OpenCC('t2s') 69 | 70 | total_files = 0 71 | converted_files = 0 72 | 73 | # 遍历文件夹中的所有文件 74 | for root, _, files in os.walk(folder_path): 75 | for file in files: 76 | if file.endswith('.srt') or file.endswith('.ass'): 77 | total_files += 1 78 | file_path = os.path.join(root, file) 79 | 80 | # 打开 SRT 文件并读取内容 81 | try: 82 | with open(file_path, 'r', encoding='utf-8') as f: 83 | srt_content = f.read() 84 | except Exception as e: 85 | self.progress_textbox.insert(tk.END, f"{file_path} 错误\n", "center") 86 | 87 | # 使用 OpenCC 进行繁体到简体中文的转换 88 | simplified_chinese = converter.convert(srt_content) 89 | 90 | # 写回到文件中 91 | with open(file_path, 'w', encoding='utf-8') as f: 92 | f.write(simplified_chinese) 93 | 94 | converted_files += 1 95 | self.progress_textbox.config(state=tk.NORMAL) 96 | self.progress_textbox.insert(tk.END, f"{file_path} 已处理\n", "center") 97 | self.progress_textbox.config(state=tk.DISABLED) 98 | self.root.update_idletasks() 99 | 100 | self.progress_text.set(f"处理完成,共处理 {converted_files}/{total_files} 个文件") 101 | 102 | def start_conversion(self): 103 | self.progress_textbox.delete(1.0,tk.END) 104 | # 创建一个线程来执行转换 105 | conversion_thread = threading.Thread(target=self.convert_srt_files_to_simplified_chinese) 106 | conversion_thread.start() 107 | 108 | if __name__ == '__main__': 109 | root = tk.Tk() 110 | app = SrtConversionApp(root) 111 | root.mainloop() 112 | -------------------------------------------------------------------------------- /字幕处理/网盘字幕提取上传.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Users/shenxian/CloudNAS/CloudDrive2/阿里云盘Open/看剧/追剧 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/网盘字幕提取上传.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tkinter as tk 4 | import tkinter as ttk 5 | from tkinter import filedialog 6 | import threading 7 | import configparser 8 | 9 | def get_desktop_path(): 10 | if os.name == "posix": # 检查操作系统是否为类Unix系统(包括macOS) 11 | return os.path.expanduser("~/Desktop") # macOS的桌面路径 12 | elif os.name == "nt": # 检查操作系统是否为Windows 13 | return os.path.expanduser("~\\Desktop") # Windows的桌面路径 14 | else: 15 | return None # 其他操作系统返回None或自定义路径 16 | 17 | class FileExtractorUploader: 18 | def __init__(self, root): 19 | self.root = root 20 | self.root.title("网盘字幕提取上传工具") 21 | # 创建配置文件对象 22 | if not os.path.exists(self.get_ini_path()): 23 | with open(self.get_ini_path(),'w') as f: 24 | f.write('') 25 | self.config = configparser.ConfigParser(default_section='Settings') 26 | self.config.read(self.get_ini_path()) # 读取配置文件 27 | self.folder_path = tk.StringVar() 28 | 29 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 30 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 31 | 32 | # 指定 temp_folder 的路径 33 | self.temp_folder = os.path.join('/Users/shenxian/Downloads','看剧') # 在这里指定 temp_folder 的路径 34 | 35 | # 创建GUI界面 36 | self.create_widgets() 37 | 38 | # 记录上次选择的文件夹路径 39 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 40 | 41 | def create_widgets(self): 42 | # 选择文件夹按钮和文本框显示选取的文件夹路径 43 | frame_folder = ttk.LabelFrame(self.root,borderwidth=0) 44 | frame_folder.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 45 | 46 | tk.Label(frame_folder, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 47 | tk.Entry(frame_folder, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 48 | tk.Button(frame_folder, text="浏览", command=self.select_folder).grid(row=0, column=2, padx=5, pady=5) 49 | 50 | 51 | # 创建包含两个按钮的框架并放在主窗口中 52 | button_frame = tk.Frame(self.root) 53 | button_frame.pack(padx=10, pady=5) 54 | 55 | tk.Button(button_frame, text="提取", command=self.extract_files).pack(side=tk.LEFT, padx=10) 56 | tk.Button(button_frame, text="上传", command=self.upload_files).pack(side=tk.LEFT, padx=10) 57 | 58 | # 文本框用于显示任务完成情况 59 | self.textbox = tk.Text(self.root, height=10, width=50) 60 | self.textbox.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) 61 | 62 | def select_folder(self): 63 | folder_selected = filedialog.askdirectory(initialdir=self.folder_path.get()) # 设置initialdir为当前文件夹路径 64 | if folder_selected: 65 | self.folder_path.set(folder_selected) 66 | 67 | def empty_folder(self,folder_path): 68 | for filename in os.listdir(folder_path): 69 | file_path = os.path.join(folder_path, filename) 70 | if os.path.isfile(file_path): 71 | os.unlink(file_path) 72 | elif os.path.isdir(file_path): 73 | shutil.rmtree(file_path) 74 | 75 | def folder_path_changed(self, *args): 76 | # 这个函数会在文件夹路径变化时触发 77 | folder_path = self.folder_path.get() 78 | # 保存文件夹路径到配置文件 79 | folder_root = os.path.dirname(folder_path) 80 | self.config.set('Settings', 'last_folder', folder_root) 81 | with open(self.get_ini_path(), 'w') as configfile: 82 | self.config.write(configfile) 83 | 84 | def get_ini_path(self,*args): 85 | # 获取当前.py文件的绝对路径 86 | current_script = os.path.abspath(__file__) 87 | ini_path = current_script.replace('.py','.ini') 88 | return(ini_path) 89 | 90 | def extract_files(self): 91 | if os.path.exists(self.temp_folder): 92 | self.empty_folder(self.temp_folder) 93 | else: 94 | os.mkdir(self.temp_folder) 95 | self.textbox.delete(1.0, tk.END) 96 | if not self.folder_path.get(): 97 | self.display_message("请选择文件夹") 98 | return 99 | 100 | # 创建一个线程来执行提取任务 101 | extraction_thread = threading.Thread(target=self.perform_extraction) 102 | extraction_thread.start() 103 | 104 | def perform_extraction(self): 105 | for root, _, files in os.walk(self.folder_path.get()): 106 | for folder_name in os.listdir(root): 107 | folder_path = os.path.join(root, folder_name) 108 | if os.path.isdir(folder_path): 109 | self.start_extract_files(os.path.basename(self.folder_path.get()),folder_path) 110 | self.display_message("全部提取完成.") 111 | 112 | def start_extract_files(self,root_path, folder_path): 113 | ass_files = [f for f in os.listdir(folder_path) if f.endswith(".ass") or f.endswith(".srt")] 114 | 115 | if not ass_files: 116 | return 117 | 118 | # 获取源文件夹的名称 119 | folder_name = os.path.basename(folder_path) 120 | 121 | # 在 self.temp_folder 下创建与源文件夹同名的文件夹 122 | target_folder = os.path.join(self.temp_folder,root_path,folder_name) 123 | os.makedirs(target_folder, exist_ok=True) 124 | 125 | # 复制文件到目标文件夹 126 | for file in ass_files: 127 | shutil.copy(os.path.join(folder_path, file), os.path.join(target_folder, file)) 128 | 129 | # 写入源文件夹路径到 source_folder.txt 130 | with open(os.path.join(target_folder, "source_folder.txt"), "w",encoding='utf-8') as f: 131 | f.write(folder_path) 132 | 133 | self.display_message(f"成功提取:{folder_path}") 134 | 135 | 136 | def upload_files(self): 137 | self.textbox.delete(1.0, tk.END) 138 | if not os.path.exists(self.temp_folder): 139 | self.display_message("temp_folder 不存在") 140 | return 141 | 142 | # 创建一个线程来执行上传任务 143 | upload_thread = threading.Thread(target=self.perform_upload) 144 | upload_thread.start() 145 | 146 | def perform_upload(self): 147 | for root, _, files in os.walk(self.temp_folder): 148 | for file in files: 149 | if file.endswith(".ass") or file.endswith(".srt"): 150 | source_txt = os.path.join(root, "source_folder.txt") 151 | if os.path.exists(source_txt): 152 | with open(source_txt, "r",encoding='utf-8') as f: 153 | source_folder = f.read() 154 | file_path = os.path.join(root, file) 155 | upload_filepath = os.path.join(source_folder, file) 156 | if os.path.exists(upload_filepath): 157 | os.remove(upload_filepath) 158 | shutil.copy(file_path, upload_filepath) 159 | self.display_message(f"{upload_filepath}上传完成") 160 | 161 | self.display_message("全部上传完成.") 162 | 163 | def display_message(self, message): 164 | self.textbox.insert(tk.END, message + "\n") 165 | self.textbox.see(tk.END) 166 | 167 | if __name__ == '__main__': 168 | root = tk.Tk() 169 | app = FileExtractorUploader(root) 170 | 171 | # 设置窗口尺寸和居中 172 | window_width = 600 173 | window_height = 325 174 | screen_width = root.winfo_screenwidth() 175 | screen_height = root.winfo_screenheight() 176 | x = (screen_width - window_width) // 2 177 | y = (screen_height - window_height) // 2 178 | root.geometry(f"{window_width}x{window_height}+{x}+{y}") 179 | root.mainloop() -------------------------------------------------------------------------------- /字幕处理/获取sup字幕时间轴.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def format_srt_time(time_str): 4 | hours, minutes, seconds, milliseconds = re.match(r'(\d+:)?(\d{1,2}:)?(\d{1,2}),(\d+)', time_str).groups() 5 | if not hours: 6 | hours = '00:' 7 | if not minutes: 8 | minutes = '00:' 9 | return f"{hours.zfill(3)}{minutes.zfill(3)}{seconds.zfill(2)},{milliseconds}" 10 | 11 | 12 | with open('/Users/shenxian/Downloads/新建文件夹 (2)/index.html','r',encoding='utf-8') as f: 13 | content= f.read() 14 | res = re.findall('#\d+:(\d+:?.*\d+)') 20 | start = format_srt_time(start) 21 | end = format_srt_time(end) 22 | time_line = f'{j}\n{start} --> {end}\n\n' 23 | result.append(time_line) 24 | with open('/Users/shenxian/Desktop/sub.srt','w',encoding='utf-8') as f: 25 | f.write(''.join(result)) 26 | -------------------------------------------------------------------------------- /字幕处理/获取字幕信息.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import subprocess 6 | import threading 7 | import configparser 8 | import re 9 | 10 | def get_sub_info(mkv_path): 11 | ffmpeg_command = ['ffmpeg', '-i', mkv_path] 12 | output_txt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),'sub_info.txt') 13 | with open(output_txt_path, 'w', encoding='utf-8') as output_file: 14 | try: 15 | subprocess.run(ffmpeg_command, stdout=output_file, stderr=subprocess.STDOUT, text=True, check=True) 16 | except subprocess.CalledProcessError as e: 17 | # 如果出现非零返回码,也将错误信息写入文件 18 | output_file.write(str(e.output)) 19 | 20 | # 使用正则表达式提取字幕信息 21 | with open(output_txt_path, 'r', encoding='utf-8') as f: 22 | content = f.read().replace("subrip",'srt') 23 | subtitle_info = [] 24 | lines = content.split('\n') 25 | for line in lines: 26 | if re.search(r'Stream #0:\d+\(.*?\): Subtitle: .*', line): 27 | stream_match = re.search(r'Stream #0:(\d+)\((.*?)\): Subtitle: (.*)', line) 28 | if stream_match: 29 | stream_number = stream_match.group(1).strip() 30 | subtitle_lang = stream_match.group(2).strip() 31 | subtitle_type = re.sub('\(.*?\)','',stream_match.group(3)).strip() 32 | subtitle_info.append([stream_number, subtitle_type, "",subtitle_lang]) 33 | elif re.search(r'title\s+:\s+(.+)', line): 34 | title_match = re.search(r'title\s+:\s+(.+)', line) 35 | if title_match and subtitle_info: 36 | subtitle_info[-1][2] = title_match.group(1).strip() 37 | print('字幕信息:\n') 38 | for index,sub_type,title,subtitle_lang in [item for item in subtitle_info if item[2]]: 39 | sub_info = f"{int(index.strip())-1}:{title.strip()}.{sub_type}\n" 40 | print(sub_info) 41 | chi_sub = [item for item in subtitle_info if "chi" in item[3]] 42 | chi_sub = [item for item in chi_sub if "简" in item[2] or "Simplified" in item[2] or "chs" in item[2]] 43 | chi_sub_res = [('1','srt','chi')] 44 | if chi_sub: 45 | print(f'找到中文字幕:\n') 46 | chi_sub_res = chi_sub[:] 47 | sub_num = str(int(chi_sub_res[0][0])-1) 48 | sub_ext = chi_sub_res[0][1] 49 | return str(int(sub_num) - 1),sub_ext 50 | 51 | class SubtitleExtractor: 52 | def __init__(self, root): 53 | self.root = root 54 | self.root.title("字幕提取工具") 55 | 56 | # 创建配置文件对象 57 | if not os.path.exists(self.get_ini_path()): 58 | with open(self.get_ini_path(),'w') as f: 59 | f.write('') 60 | self.config = configparser.ConfigParser(default_section='Settings') 61 | self.config.read(self.get_ini_path()) # 读取配置文件 62 | 63 | self.font_types = ["srt","ass"] 64 | 65 | self.folder_path = tk.StringVar() 66 | self.sub_num = tk.StringVar() 67 | self.sub_ext = tk.StringVar() 68 | self.progress_text = tk.StringVar() 69 | self.auto_process = tk.BooleanVar() 70 | 71 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 72 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 73 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 74 | 75 | self.create_widgets() 76 | 77 | def create_widgets(self): 78 | frame_folder = ttk.LabelFrame(self.root, text="选择文件夹") 79 | frame_folder.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 80 | 81 | tk.Label(frame_folder, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 82 | tk.Entry(frame_folder, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 83 | tk.Button(frame_folder, text="浏览", command=self.browse_folder).grid(row=0, column=2, padx=5, pady=5) 84 | 85 | button_frame = tk.Frame(self.root) 86 | button_frame.pack(padx=10, pady=5) 87 | 88 | tk.Button(button_frame, text="获取信息", command=self.start_get_sub_info).pack(side=tk.LEFT, padx=10,pady=10) 89 | tk.Button(button_frame, text="开始提取", command=self.start_extraction).pack(side=tk.LEFT, padx=10,pady=10) 90 | 91 | 92 | frame_output = ttk.LabelFrame(self.root, text="处理日志") 93 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 94 | 95 | self.output_text = tk.Text(frame_output, wrap=tk.WORD, height=10) 96 | self.output_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 97 | 98 | tk.Label(frame_output, textvariable=self.progress_text).pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 99 | 100 | def browse_folder(self): 101 | folder_selected = filedialog.askdirectory(initialdir=self.folder_path.get()) # 设置initialdir为当前文件夹路径 102 | if folder_selected: 103 | self.folder_path.set(folder_selected) 104 | 105 | def get_ini_path(self,*args): 106 | # 获取当前.py文件的绝对路径 107 | current_script = os.path.abspath(__file__) 108 | ini_path = current_script.replace('.py','.ini') 109 | return(ini_path) 110 | 111 | def start_get_sub_info(self): 112 | folder_path = self.folder_path.get() 113 | self.progress_text.set("正在获取字幕信息...") 114 | self.output_text.delete(1.0, tk.END) 115 | mkv_path = '' 116 | for root, _, files in os.walk(folder_path): 117 | for file in files: 118 | if file.endswith('.mkv') or file.endswith('.mp4'): 119 | mkv_path = os.path.join(root, file) 120 | break 121 | if mkv_path: 122 | thread = threading.Thread(target=self.get_sub_info, args=(mkv_path,)) 123 | thread.start() 124 | 125 | def get_sub_info(self,mkv_path): 126 | # 定义命令和参数列表 127 | command = [ 128 | 'ffprobe', 129 | '-v', 'error', 130 | '-show_entries', 'stream=index:stream_tags=title', 131 | '-select_streams', 's', 132 | mkv_path 133 | ] 134 | 135 | # 执行命令 136 | result = subprocess.run(command, stdout=subprocess.PIPE, text=True) 137 | # 获取输出 138 | output = result.stdout 139 | self.output_text.insert(tk.END,f'{output}\n') 140 | self.progress_text.set(f"字幕信息获取成功") 141 | 142 | 143 | def folder_path_changed(self, *args): 144 | # 这个函数会在文件夹路径变化时触发 145 | folder_path = self.folder_path.get() 146 | # 保存文件夹路径到配置文件 147 | folder_root = os.path.dirname(folder_path) 148 | self.config.set('Settings', 'last_folder', folder_root) 149 | with open(self.get_ini_path(), 'w') as configfile: 150 | self.config.write(configfile) 151 | 152 | def start_extraction(self): 153 | folder_path = self.folder_path.get() 154 | sub_num = self.sub_num.get() 155 | sub_num = str(int(sub_num) - 1) 156 | sub_ext = self.sub_ext.get() 157 | auto_process = self.auto_process.get() 158 | if not folder_path: 159 | tk.messagebox.showerror("错误", "请选择文件夹") 160 | return 161 | 162 | if not sub_num: 163 | tk.messagebox.showerror("错误", "请输入字幕流编号") 164 | return 165 | 166 | if not sub_ext: 167 | tk.messagebox.showerror("错误", "请输入字幕格式") 168 | return 169 | 170 | self.progress_text.set("正在提取字幕...") 171 | self.output_text.delete(1.0, tk.END) 172 | thread = threading.Thread(target=self.extract_subtitles, args=(folder_path, sub_num, sub_ext,auto_process)) 173 | thread.start() 174 | 175 | def extract_subtitles(self, folder_path, sub_num, sub_ext,auto_process): 176 | count = 0 177 | for root, _, files in os.walk(folder_path): 178 | for file in files: 179 | if file.endswith('.mkv') or file.endswith('.mp4'): 180 | mkv_path = os.path.join(root, file) 181 | if auto_process: 182 | sub_num,sub_ext = self.get_sub_info(mkv_path) 183 | self.sub_num.set(sub_num) 184 | self.sub_ext.set(sub_ext) 185 | output_subfile = f'{os.path.splitext(mkv_path)[0]}.chs.{sub_ext}' 186 | if os.path.exists(output_subfile): 187 | os.remove(output_subfile) 188 | cmd = f'ffmpeg -i "{mkv_path}" -map 0:s:{sub_num} "{output_subfile}"' 189 | os.system(cmd) 190 | count += 1 191 | self.progress_text.set(f"已提取 {count} 个字幕") 192 | self.output_text.insert(tk.END, f"成功提取字幕: {file}\n") 193 | 194 | self.progress_text.set(f"提取完成,共提取 {count} 个字幕") 195 | 196 | if __name__ == "__main__": 197 | root = tk.Tk() 198 | app = SubtitleExtractor(root) 199 | sw = root.winfo_screenwidth() 200 | sh = root.winfo_screenheight() 201 | ww = 600 202 | wh = 450 203 | x = (sw - ww) / 2 204 | y = (sh - wh) / 2 - 60 205 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 206 | root.mainloop() -------------------------------------------------------------------------------- /字幕处理/获取视频字幕信息.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import os 4 | import traceback 5 | 6 | working_directory = os.path.dirname(os.path.abspath(__file__)) 7 | os.chdir(working_directory) 8 | 9 | 10 | def write_completed_file_name(file_path): 11 | with open("completed_file_name.txt", "a", encoding="utf-8") as f: 12 | f.write(f"{file_path}\n") 13 | 14 | 15 | def write_video_without_chs(file_path): 16 | with open("video_without_chs.txt", "a", encoding="utf-8") as f: 17 | f.write(f"{file_path}\n") 18 | 19 | 20 | def get_chinese_sub_dict(video_path=None): 21 | chi_flag = False 22 | try: 23 | subtitles = get_subtitle_info(video_path) 24 | for sub_dict in subtitles: 25 | title = sub_dict["title"] 26 | if "简" in title or "sim" in title.lower() or "chs" in title.lower(): 27 | print(f"{video_path}::: {title}") 28 | chi_flag = True 29 | break 30 | if not chi_flag: 31 | write_video_without_chs(video_path) 32 | print(f"该视频不包含中文简体字幕::: {video_path}") 33 | write_completed_file_name(video_path) 34 | except: 35 | print(traceback.format_exc()) 36 | 37 | 38 | def get_subtitle_info(video_path): 39 | # 运行ffprobe命令并捕获输出 40 | command = [ 41 | "ffprobe", 42 | "-v", 43 | "quiet", 44 | "-print_format", 45 | "json", 46 | "-show_streams", 47 | "-select_streams", 48 | "s", 49 | video_path, 50 | ] 51 | output = subprocess.check_output(command) 52 | 53 | # 解析输出的JSON数据 54 | data = json.loads(output) 55 | 56 | # 提取字幕信息 57 | subtitles = [] 58 | for stream in data["streams"]: 59 | if stream["codec_type"] == "subtitle": 60 | language = stream.get("tags", {}).get("language", "") 61 | title = stream.get("tags", {}).get("title", "") 62 | index = str(int(stream["index"]) - 2) 63 | format_name = stream["codec_name"] 64 | subtitles.append( 65 | { 66 | "language": language, 67 | "title": title, 68 | "index": index, 69 | "format": format_name, 70 | } 71 | ) 72 | return subtitles 73 | 74 | 75 | if __name__ == "__main__": 76 | with open("completed_file_name.txt", "r", encoding="utf-8") as f: 77 | video_list = f.read().strip().split("\n") 78 | 79 | video_num = 0 80 | folder_list = [ 81 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/Nas/电影合集/华语电影", 82 | ] 83 | video_extensions = ( 84 | ".mkv", 85 | ".iso", 86 | ".ts", 87 | ".mp4", 88 | ".avi", 89 | ".rmvb", 90 | ".wmv", 91 | ".m2ts", 92 | ".mpg", 93 | ".flv", 94 | ".rm", 95 | ".mov", 96 | ) 97 | for folder_path in folder_list: 98 | for root, dirs, files in os.walk(folder_path): 99 | for file in files: 100 | if file.endswith(video_extensions): 101 | file_path = os.path.join(root, file) 102 | video_num += 1 103 | if file_path not in video_list: 104 | get_chinese_sub_dict(file_path) 105 | print(f"已处理{video_num}个视频文件.") 106 | -------------------------------------------------------------------------------- /字幕处理/重命名字幕.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | last_folder = /Volumes/dav/115/看剧/links/电影/系列电影 3 | 4 | -------------------------------------------------------------------------------- /字幕处理/重命名字幕.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import filedialog 4 | from tkinter import ttk 5 | import subprocess 6 | import threading 7 | import configparser 8 | import codecs 9 | from tkinter.constants import N 10 | 11 | def detect_and_convert_encoding(input_file): 12 | encodings = ["utf-32", "utf-16", "utf-8", "cp1252", "gb2312", "gbk", "big5"] 13 | 14 | for enc in encodings: 15 | try: 16 | with codecs.open(input_file, mode="r", encoding=enc) as fd: 17 | content = fd.read() 18 | 19 | # 如果文件内容不为空,则表示找到了正确的编码 20 | if content: 21 | with codecs.open(input_file, 'w', 'utf-8') as fd: 22 | fd.write(content) 23 | return True 24 | 25 | except Exception as e: 26 | continue 27 | 28 | class SubRename: 29 | def __init__(self, root): 30 | self.root = root 31 | self.root.title("字幕重命名") 32 | 33 | # 创建配置文件对象 34 | if not os.path.exists(self.get_ini_path()): 35 | with open(self.get_ini_path(),'w') as f: 36 | f.write('') 37 | self.config = configparser.ConfigParser(default_section='Settings') 38 | self.config.read(self.get_ini_path()) # 读取配置文件 39 | 40 | self.font_types = ["srt","ass"] 41 | 42 | self.folder_path = tk.StringVar() 43 | self.progress_text = tk.StringVar() 44 | 45 | 46 | # 设置文件夹路径为上次保存的路径,如果没有则默认为空 47 | self.folder_path.set(self.config.get('Settings', 'last_folder', fallback='')) 48 | 49 | self.create_widgets() 50 | self.folder_path.trace_add("write", self.folder_path_changed) # 绑定变量变化的回调函数 51 | 52 | def create_widgets(self): 53 | frame_option = ttk.LabelFrame(self.root, text="选择文件夹") 54 | frame_option.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 55 | 56 | tk.Label(frame_option, text="文件夹路径:").grid(row=0, column=0, padx=5, pady=5) 57 | tk.Entry(frame_option, textvariable=self.folder_path, width=40).grid(row=0, column=1, padx=5, pady=5) 58 | tk.Button(frame_option, text="浏览", command=self.browse_folder).grid(row=0, column=2, padx=5, pady=5) 59 | tk.Button(frame_option, text="开始转换", command=self.start_rename).grid(row=1, column=1, padx=5, pady=5) 60 | 61 | frame_output = ttk.LabelFrame(self.root, text="处理日志") 62 | frame_output.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) 63 | 64 | self.output_text = tk.Text(frame_output, wrap=tk.WORD, height=10) 65 | self.output_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 66 | 67 | tk.Label(frame_output, textvariable=self.progress_text).pack(padx=5, pady=5, fill=tk.BOTH, expand=True) 68 | 69 | def browse_folder(self): 70 | folder_selected = filedialog.askdirectory(initialdir=self.folder_path.get()) # 设置initialdir为当前文件夹路径 71 | if folder_selected: 72 | self.folder_path.set(folder_selected) 73 | 74 | def get_ini_path(self,*args): 75 | # 获取当前.py文件的绝对路径 76 | current_script = os.path.abspath(__file__) 77 | ini_path = current_script.replace('.py','.ini') 78 | return(ini_path) 79 | 80 | def folder_path_changed(self, *args): 81 | # 这个函数会在文件夹路径变化时触发 82 | folder_path = self.folder_path.get() 83 | # 保存文件夹路径到配置文件 84 | folder_root = os.path.dirname(folder_path) 85 | self.config.set('Settings', 'last_folder', folder_root) 86 | with open(self.get_ini_path(), 'w') as configfile: 87 | self.config.write(configfile) 88 | 89 | def start_rename(self): 90 | folder_path = self.folder_path.get() 91 | if not folder_path: 92 | tk.messagebox.showerror("错误", "请选择文件夹") 93 | return 94 | 95 | self.progress_text.set("正在重命名字幕...") 96 | self.output_text.delete(1.0, tk.END) 97 | thread = threading.Thread(target=self.rename_sub, args=(folder_path,)) 98 | thread.start() 99 | 100 | def rename_sub(self, folder_path): 101 | count = 0 102 | for root, _, files in os.walk(folder_path): 103 | for file in files: 104 | if file.endswith('.ass') or file.endswith('.srt'): 105 | file_path = os.path.join(root, file) 106 | if '.zh' not in file_path: 107 | new_file_path = file_path.replace('.ass','.zh.ass').replace('.srt','.zh.srt').replace('.chs','') 108 | if os.path.exists(new_file_path): 109 | os.remove(new_file_path) 110 | os.rename(file_path,new_file_path) 111 | count += 1 112 | self.progress_text.set(f"重命名 {count} 个字幕") 113 | self.output_text.insert(tk.END, f"成功重命名字幕: {file}\n") 114 | else: 115 | continue 116 | self.progress_text.set(f"重命名,共重命名 {count} 个字幕") 117 | 118 | 119 | if __name__ == "__main__": 120 | root = tk.Tk() 121 | app = SubRename(root) 122 | sw = root.winfo_screenwidth() 123 | sh = root.winfo_screenheight() 124 | ww = 600 125 | wh = 450 126 | x = (sw - ww) / 2 127 | y = (sh - wh) / 2 - 60 128 | root.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) 129 | root.mainloop() -------------------------------------------------------------------------------- /寻找缺失集数.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import pyperclip 4 | 5 | # 指定文件夹路径 6 | folder_path = pyperclip.paste() # 替换为你的文件夹路径 7 | 8 | # 获取文件夹下所有文件夹的名称 9 | subfiles = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] 10 | text = ','.join(subfiles) 11 | # 使用正则表达式查找文件名中的数字,并去除可能存在的前导零 12 | pattern = r'第0*(\d+)集' # 匹配数字并去除前导零 13 | matches = re.findall(pattern, text) 14 | numbers = sorted(map(int, matches)) 15 | # 查找缺失的数字 16 | missing_numbers = [] 17 | for i in range(min(numbers), max(numbers)): 18 | if i not in numbers: 19 | missing_numbers.append(i) 20 | 21 | # 输出结果 22 | if missing_numbers: 23 | print("缺失的数字:", missing_numbers) 24 | else: 25 | print("没有缺失的数字") 26 | 27 | -------------------------------------------------------------------------------- /批量扫描emby视频信息.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import os 4 | import concurrent.futures 5 | 6 | working_directory = os.path.dirname(os.path.abspath(__file__)) 7 | os.chdir(working_directory) 8 | 9 | # 创建日志记录器 10 | logger = logging.getLogger(__name__) 11 | logger.setLevel(logging.DEBUG) 12 | 13 | # 创建控制台处理器 14 | console_handler = logging.StreamHandler() 15 | console_handler.setLevel(logging.DEBUG) 16 | console_format = logging.Formatter( 17 | "%(asctime)s [%(levelname)s] %(message)s\n", "%Y-%m-%d %H:%M:%S" 18 | ) 19 | console_handler.setFormatter(console_format) 20 | 21 | # 创建文件处理器 22 | file_handler = logging.FileHandler("emby_extract.log") 23 | file_handler.setLevel(logging.DEBUG) 24 | file_format = logging.Formatter( 25 | "%(asctime)s [%(levelname)s] %(message)s\n", "%Y-%m-%d %H:%M:%S" 26 | ) 27 | file_handler.setFormatter(file_format) 28 | 29 | # 将处理器添加到记录器 30 | logger.addHandler(console_handler) 31 | logger.addHandler(file_handler) 32 | 33 | import time 34 | 35 | 36 | def measure_time(func): 37 | def wrapper(*args, **kwargs): 38 | start_time = time.time() 39 | result = func(*args, **kwargs) 40 | end_time = time.time() 41 | total_time = end_time - start_time 42 | logger.info(f"总耗时: {total_time:.2f} 秒") 43 | return result 44 | 45 | return wrapper 46 | 47 | 48 | # 获取所有电影的信息 49 | def get_all_movies(): 50 | url = f"{EMBY_SERVER_URL}/emby/Items" 51 | params = { 52 | "api_key": API_KEY, 53 | "IncludeItemTypes": "Movie,Episode", 54 | "Recursive": True, 55 | "Fields": "ProviderIds,Path", 56 | } 57 | try: 58 | response = requests.get(url, params=params) 59 | response.raise_for_status() # 检查请求是否成功 60 | return response.json()["Items"] 61 | except requests.exceptions.HTTPError as http_err: 62 | logger.info(f"HTTP error occurred: {http_err}") # 输出HTTP错误 63 | except requests.exceptions.RequestException as err: 64 | logger.info(f"Other error occurred: {err}") # 输出其他错误 65 | except ValueError: 66 | logger.info("Error parsing JSON response") 67 | logger.info("Response content:", response.content) # 输出响应内容以便调试 68 | return [] 69 | 70 | 71 | def scan_item(item_id: str): 72 | url = f"{EMBY_SERVER_URL}/emby/Items/{item_id}/PlaybackInfo" 73 | params = { 74 | "api_key": API_KEY, 75 | } 76 | response = requests.get(url, params=params) 77 | 78 | if response.status_code == 200: 79 | return True 80 | else: 81 | logger.info(f"Request failed with status code: {response.status_code}") 82 | return False 83 | 84 | 85 | # 主函数 86 | @measure_time 87 | def main(): 88 | total_num = 0 89 | all_movies = get_all_movies() 90 | 91 | with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: 92 | futures = [executor.submit(scan_item, item["Id"]) for item in all_movies] 93 | for future in concurrent.futures.as_completed(futures): 94 | try: 95 | if future.result(): 96 | total_num += 1 97 | logger.info( 98 | f"已成功扫描视频信息:{all_movies[futures.index(future)]['Path']}\n共扫描{total_num}个视频" 99 | ) 100 | except: 101 | continue 102 | 103 | logger.info(f"总共扫描了 {total_num} 个视频") 104 | 105 | 106 | if __name__ == "__main__": 107 | # 配置Emby服务器信息 108 | EMBY_SERVER_URL = "http://127.0.0.1:8096" 109 | API_KEY = "5b2d4062d9124a6ca9e7eab015251f53" 110 | num_threads = 5 # 设置线程数 111 | main() 112 | -------------------------------------------------------------------------------- /批量更新emby封面.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import base64 4 | 5 | 6 | class EmbyCoverUpdater: 7 | def __init__(self, server, api_key, cover_folder_path): 8 | self.server = server 9 | self.api_key = api_key 10 | self.cover_folder_path = cover_folder_path 11 | 12 | def image_to_base64(self, image_path): 13 | with open(image_path, "rb") as image_file: 14 | base64_image = base64.b64encode(image_file.read()).decode() 15 | return base64_image 16 | 17 | def get_image_list(self): 18 | image_list = [] 19 | for root, dirs, files in os.walk(self.cover_folder_path): 20 | for file in files: 21 | if file.lower().endswith((".jpg", "jpeg", ".png", ".gif")): 22 | file_path = os.path.join(root, file) 23 | image_list.append(file_path) 24 | return image_list 25 | 26 | def get_media_libraries(self): 27 | url = ( 28 | f"{self.server}/emby/Library/SelectableMediaFolders?api_key={self.api_key}" 29 | ) 30 | response = requests.get(url) 31 | return response.json() 32 | 33 | def update_library_cover(self, library_id, library_name): 34 | image_list = self.get_image_list() 35 | image_path = None 36 | for file_path in image_list: 37 | filename = os.path.split(file_path)[-1] 38 | if library_name in filename: 39 | image_path = file_path 40 | break 41 | if image_path: 42 | try: 43 | url = f"{self.server}/emby/Items/{library_id}/Images/Primary?api_key={self.api_key}" 44 | base64_image = self.image_to_base64(image_path) 45 | headers = {"Content-Type": "image/jpg"} 46 | response = requests.post(url, data=base64_image, headers=headers) 47 | return response.status_code 48 | except: 49 | return 404 50 | else: 51 | return "未找到封面图片" 52 | 53 | def update_all_covers(self): 54 | media_libraries = self.get_media_libraries() 55 | for library in media_libraries: 56 | library_name = library["Name"] 57 | library_id = library["Id"] 58 | print(f"开始为媒体库 '{library_name}' 更新封面...") 59 | status_code = self.update_library_cover(library_id, library_name) 60 | if status_code == 204: 61 | print(f"媒体库 {library_name} 成功更新封面\n") 62 | else: 63 | print(f"媒体库 {library_name} 更新封面失败: {status_code}\n") 64 | 65 | 66 | if __name__ == "__main__": 67 | EMBY_SERVER = "http://192.168.9.28:8096" 68 | EMBY_API_KEY = "d0ef77bc3905408381b15c38d12a7ffc" 69 | COVER_FOLDER_PATH = "/Users/shenxian/沈闲云/沈闲/封面/Emby封面" 70 | 71 | updater = EmbyCoverUpdater(EMBY_SERVER, EMBY_API_KEY, COVER_FOLDER_PATH) 72 | updater.update_all_covers() 73 | -------------------------------------------------------------------------------- /根据tmdbid查重.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import traceback 5 | 6 | tmdb_list = [] 7 | 8 | # 这个是基准电影库 9 | base_dir = "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/4K Remux" 10 | for root, dirs, files in os.walk(base_dir): 11 | for dir in dirs: 12 | if "tmdb" in dir: 13 | tmdbid = re.findall(r"tmdb.*?(\d+)", dir) 14 | tmdb_list.append(tmdbid[0]) 15 | num = 0 16 | 17 | # # 这个是基准电影库 18 | # base_dir = "/Users/shenxian/CloudNAS/CloudDrive2/115/abc" 19 | # for root, dirs, files in os.walk(base_dir): 20 | # for dir in dirs: 21 | # if "tmdb" in dir: 22 | # tmdbid = re.findall(r"tmdb.*?(\d+)", dir) 23 | # tmdb_list.append(tmdbid[0]) 24 | 25 | # 这里面填你要查重的电影库 26 | target_dir_list = [ 27 | "/Users/shenxian/CloudNAS/CloudDrive2/115/abc", 28 | ] 29 | for root_dir in target_dir_list: 30 | for root, dirs, files in os.walk(root_dir): 31 | for dir in dirs: 32 | if "tmdb" in dir: 33 | tmdbid = re.findall(r"tmdb.*?(\d+)", dir) 34 | if tmdbid[0] in tmdb_list: 35 | num += 1 36 | dir_path = os.path.join(root, dir) 37 | try: 38 | shutil.rmtree(dir_path) 39 | print(f"dir deleted::: {dir}") 40 | except: 41 | print(f"error::: {dir_path}") 42 | print(traceback.format_exc()) 43 | print(num) 44 | -------------------------------------------------------------------------------- /清空二级文件夹.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import shutil 5 | import logging 6 | 7 | # 配置日志 8 | log_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"empty_child_folder.log") 9 | logging.basicConfig(filename=log_path, level=logging.INFO, format="%(asctime)s [%(levelname)s]: %(message)s") 10 | 11 | # 记录程序运行时间 12 | current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 13 | logging.info(f"程序运行开始时间: {current_time}") 14 | 15 | 16 | def empty_folder(folder_path): 17 | try: 18 | for i in os.listdir(folder_path): 19 | item_path = os.path.join(folder_path,i) 20 | if os.path.isfile(item_path): 21 | os.remove(item_path) 22 | elif os.path.isdir(item_path): 23 | for root, dirs, files in os.walk(item_path): 24 | for file in files: 25 | file_path = os.path.join(root, file) 26 | os.remove(file_path) 27 | for dir in dirs: 28 | dir_path = os.path.join(root,dir) 29 | shutil.rmtree(dir_path) 30 | logging.info(f'已删除文件夹: {dir_path}') 31 | logging.info(f'已清空文件夹: {folder_path}') 32 | except Exception as e: 33 | logging.error(e) 34 | 35 | if __name__ == '__main__': 36 | # 获取要处理的文件夹列表 37 | folder_list = ["/volume2/Media/links/电影","/volume2/Media/links/电视剧","/volume2/Media/links/动漫"] 38 | 39 | # 检查是否有命令行参数 40 | if len(sys.argv) > 1: 41 | user_input = sys.argv[1] # 使用命令行参数作为用户输入 42 | else: 43 | # 获取用户输入,以逗号分隔的数字或 "all" 44 | user_input = input("请输入要处理的文件夹序号(以逗号分隔)或输入 'all' 处理所有文件夹: ") 45 | 46 | if user_input == "all": 47 | # 处理所有文件夹 48 | for folder in folder_list: 49 | empty_folder(folder) 50 | else: 51 | # 根据用户输入处理特定文件夹 52 | selected_folders = [int(num) - 1 for num in user_input.split(",")] 53 | for index in selected_folders: 54 | if 0 <= index < len(folder_list): 55 | folder = folder_list[index] 56 | empty_folder(folder) 57 | 58 | print("处理完成") 59 | -------------------------------------------------------------------------------- /电影文件夹加tmdbid-movie-nfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | working_directory = os.path.dirname(os.path.abspath(__file__)) 5 | os.chdir(working_directory) 6 | 7 | 8 | def rename_folders(root_dir): 9 | # 遍历指定文件夹下的所有子文件夹 10 | for subdir, dirs, files in os.walk(root_dir, topdown=False): 11 | for folder in dirs: 12 | # 判断文件夹名称中是否包含tmdbid 13 | if "tmdbid" in folder and "(" in folder: 14 | continue # 跳过包含tmdbid的文件夹 15 | folder_path = os.path.join(subdir, folder) 16 | nfo_file = [f for f in os.listdir(folder_path) if f.endswith(".nfo")] 17 | if nfo_file: 18 | nfo_file = nfo_file[0] 19 | nfo_file_path = os.path.join(folder_path, nfo_file) 20 | try: 21 | with open(nfo_file_path, "r", encoding="utf-8") as f: 22 | nfo_content = f.read() 23 | # 使用正则表达式获取tmdbid 24 | except: 25 | with open("error.txt", "a") as f: 26 | f.write(f"{folder_path}\n") 27 | continue 28 | match = re.search( 29 | r'(\d+)', nfo_content 30 | ) 31 | if match: 32 | tmdbid = match.group(1) 33 | new_folder_name = f"{folder} {{tmdb-{tmdbid}}}" 34 | new_folder_name = ( 35 | new_folder_name.replace("(", " (") 36 | .replace(")", ") ") 37 | .replace(" ", " ") 38 | ) 39 | new_folder_path = os.path.join(subdir, new_folder_name) 40 | os.rename(folder_path, new_folder_path) 41 | print(f"Renamed folder '{folder}' to '{new_folder_name}'") 42 | else: 43 | with open( 44 | "/Users/shenxian/Desktop/error.txt", "a", encoding="utf-8" 45 | ) as f: 46 | f.write(folder_path + "\n") 47 | print(f"No uniqueid found in .nfo files in folder '{folder}'") 48 | else: 49 | print(f"No .nfo files found in folder '{folder}'") 50 | 51 | 52 | # 指定要遍历的文件夹路径 53 | root_directory = "/Users/shenxian/CloudNAS/CloudDrive2/115(Wen)/Shenxian/已完成电影" 54 | 55 | # 调用函数进行文件夹重命名 56 | rename_folders(root_directory) 57 | -------------------------------------------------------------------------------- /电影版本筛选.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import traceback 4 | 5 | 6 | def get_video_files(folder_path): 7 | video_extensions = [ 8 | ".mkv", 9 | ".iso", 10 | ".ts", 11 | ".mp4", 12 | ".avi", 13 | ".rmvb", 14 | ".wmv", 15 | ".m2ts", 16 | ".mpg", 17 | ".flv", 18 | ".rm", 19 | ".mov", 20 | ] 21 | video_files = [] 22 | for root, dirs, files in os.walk(folder_path): 23 | for file in files: 24 | if os.path.splitext(file)[1].lower() in video_extensions: 25 | if "720p" in file: 26 | os.remove(os.path.join(root, file)) 27 | continue 28 | video_files.append(os.path.join(root, file)) 29 | return video_files 30 | 31 | 32 | def remove_metadata(filepath): 33 | dir_path = os.path.dirname(filepath) 34 | basename = os.path.basename(filepath) 35 | filename, ext = os.path.splitext(basename) 36 | for ext in [ 37 | ".nfo", 38 | "-clearlogo.png", 39 | "-fanart.jpg", 40 | "-landscape.jpg", 41 | "-poster.jpg", 42 | ]: 43 | file = os.path.join(dir_path, filename + ext) 44 | if os.path.exists(file): 45 | try: 46 | os.remove(file) 47 | print(f"metadata file deleted: {file}") 48 | except: 49 | pass 50 | 51 | 52 | def process_video_files(video_files, resolution): 53 | if len(video_files) == 1: 54 | return 55 | try: 56 | files_with_2160p = [file for file in video_files if resolution in file] 57 | if len(files_with_2160p) > 0: 58 | if len(files_with_2160p) == 1: 59 | max_file = files_with_2160p[0] 60 | elif len(files_with_2160p) > 1: 61 | max_file = max(files_with_2160p, key=os.path.getsize) 62 | files_with_h265_x265 = [ 63 | file 64 | for file in files_with_2160p 65 | if "H.265" in file or "X265" in file 66 | ] 67 | if len(files_with_h265_x265) == 1: 68 | max_file = files_with_h265_x265[0] 69 | elif len(files_with_h265_x265) > 1: 70 | max_file = max(files_with_h265_x265, key=os.path.getsize) 71 | for file in video_files: 72 | # 只删除当前分辨的电影文件 73 | if file != max_file and resolution in file: 74 | os.remove(file) 75 | print(f"Deleted file: {file}") 76 | remove_metadata(file) 77 | except: 78 | print(traceback.format_exc()) 79 | print(video_files) 80 | 81 | 82 | # 指定要遍历的文件夹路径 83 | folder_list = [ 84 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/欧美电影", 85 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/华语电影", 86 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/日韩电影", 87 | "/Users/shenxian/CloudNAS/CloudDrive2/115/看剧/影视合集/电影/Remux/动画电影", 88 | ] 89 | folder_num = 0 90 | resolution_list = ["2160p", "1080p"] 91 | for folder_path in folder_list: 92 | # 遍历文件夹的子文件夹并获取视频文件 93 | subfolders = [f.path for f in os.scandir(folder_path) if f.is_dir()] 94 | for subfolder in subfolders: 95 | for resolution in resolution_list: 96 | video_files = get_video_files(subfolder) 97 | if len(video_files) == 0: 98 | try: 99 | shutil.rmtree(subfolder) 100 | except: 101 | pass 102 | process_video_files(video_files, resolution) 103 | folder_num += 1 104 | print(f"已处理{folder_num}个文件夹.\n") 105 | -------------------------------------------------------------------------------- /电视剧文件夹加tmdbid-season-nfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | working_directory = os.path.dirname(os.path.abspath(__file__)) 5 | os.chdir(working_directory) 6 | 7 | 8 | def rename_folders(root_dir): 9 | # 遍历指定文件夹下的所有子文件夹 10 | for subdir, dirs, files in os.walk(root_dir, topdown=False): 11 | for file in files: 12 | if file == "tvshow.nfo": 13 | nfo_file_path = os.path.join(subdir, file) 14 | folder_path = os.path.dirname(nfo_file_path) 15 | folder_name = os.path.basename(folder_path) 16 | if "tmdbid" in folder_name: 17 | continue 18 | try: 19 | with open(nfo_file_path, "r", encoding="utf-8") as f: 20 | nfo_content = f.read() 21 | # 使用正则表达式获取tmdbid 22 | except: 23 | with open("error.txt", "a") as f: 24 | f.write(f"{folder_path}\n") 25 | continue 26 | match = re.search( 27 | r'(\d+)', nfo_content 28 | ) 29 | try: 30 | if match: 31 | tmdbid = match.group(1) 32 | new_folder_name = f"{folder_name} {{tmdbid={tmdbid}}}" 33 | new_folder_name = ( 34 | new_folder_name.replace("(", " (") 35 | .replace(")", ") ") 36 | .replace(" ", " ") 37 | ) 38 | new_folder_path = os.path.join( 39 | os.path.dirname(folder_path), new_folder_name 40 | ) 41 | os.rename(folder_path, new_folder_path) 42 | # print(folder_path) 43 | # print(new_folder_path) 44 | print(f"Renamed folder '{folder_name}' to '{new_folder_name}'") 45 | else: 46 | with open( 47 | "/Users/shenxian/Desktop/error.txt", "a", encoding="utf-8" 48 | ) as f: 49 | f.write(folder_path + "\n") 50 | print( 51 | f"No uniqueid found in .nfo files in folder '{folder_name}'" 52 | ) 53 | except: 54 | with open( 55 | "/Users/shenxian/Desktop/error.txt", "a", encoding="utf-8" 56 | ) as f: 57 | f.write(folder_path + "\n") 58 | 59 | 60 | # 指定要遍历的文件夹路径 61 | root_directory = "/Users/shenxian/CloudNAS/CloudDrive2/115/我的接收/日韩剧" 62 | 63 | # 调用函数进行文件夹重命名 64 | rename_folders(root_directory) 65 | -------------------------------------------------------------------------------- /电视剧文件夹加tmdbid-tmdb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | # 设置 TMDb API 密钥 5 | api_key = "896c4c7d5af3899b0f56be6824560848" 6 | 7 | 8 | def extract_show_info(folder_name): 9 | """从文件夹名中提取剧集名称和年份""" 10 | try: 11 | show_name, show_year = folder_name.split(" (") 12 | show_year = show_year[:-1] 13 | return show_name, show_year 14 | except (ValueError, IndexError): 15 | print(f"Error extracting show info from folder name: {folder_name}") 16 | return None, None 17 | 18 | 19 | def search_tmdb(show_name, show_year): 20 | """使用 TMDb API 搜索剧集""" 21 | try: 22 | search_url = f"https://api.themoviedb.org/3/search/tv?api_key={api_key}&query={show_name}" 23 | response = requests.get(search_url) 24 | response.raise_for_status() 25 | data = response.json() 26 | for result in data["results"]: 27 | if str(result["first_air_date"]).startswith(show_year): 28 | return result["id"] 29 | except (requests.exceptions.RequestException, KeyError): 30 | print(f'Error searching TMDb for "{show_name} ({show_year})"') 31 | return None 32 | 33 | 34 | def rename_folder(folder_path, folder_name, tmdb_id): 35 | global num 36 | """重命名文件夹""" 37 | try: 38 | new_folder_name = f"{folder_name} {{tmdb-{tmdb_id}}}" 39 | new_folder_path = os.path.join(folder_path, new_folder_name) 40 | os.rename(os.path.join(folder_path, folder_name), new_folder_path) 41 | num += 1 42 | print(f"Folder renamed: {folder_name} -> {new_folder_name} {num}") 43 | except OSError as e: 44 | print(f"Error renaming folder {folder_name}: {e}") 45 | 46 | 47 | def process_folder(folder_path, folder_name): 48 | """处理单个文件夹""" 49 | # 跳过包含 "tmdb" 的文件夹 50 | if "tmdb" in folder_name.lower(): 51 | return 52 | 53 | show_name, show_year = extract_show_info(folder_name) 54 | if show_name is None or show_year is None: 55 | return 56 | 57 | tmdb_id = search_tmdb(show_name, show_year) 58 | if tmdb_id: 59 | rename_folder(folder_path, folder_name, tmdb_id) 60 | else: 61 | print(f"No match found for: {folder_name}") 62 | 63 | 64 | def main(folder_path): 65 | """主函数""" 66 | for folder_name in os.listdir(folder_path): 67 | process_folder(folder_path, folder_name) 68 | 69 | 70 | if __name__ == "__main__": 71 | num = 0 72 | dir_list = [ 73 | "/Users/shenxian/CloudNAS/CloudDrive2/115(devdong)/我的接收/MYTVSUPER/MYTVSUPER_1", 74 | "/Users/shenxian/CloudNAS/CloudDrive2/115(devdong)/我的接收/MYTVSUPER/MYTVSUPER_2", 75 | "/Users/shenxian/CloudNAS/CloudDrive2/115(devdong)/我的接收/MYTVSUPER/MYTVSUPER_3", 76 | "/Users/shenxian/CloudNAS/CloudDrive2/115(devdong)/我的接收/MYTVSUPER/MYTVSUPER_4", 77 | "/Users/shenxian/CloudNAS/CloudDrive2/115(devdong)/我的接收/MYTVSUPER/MYTVSUPER_5", 78 | ] 79 | for dir in dir_list: 80 | # 使用示例 81 | main(dir) 82 | -------------------------------------------------------------------------------- /自动合并Emby版本.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # 配置Emby服务器信息 4 | EMBY_SERVER_URL = "http://192.168.9.89:8096" 5 | API_KEY = "" 6 | # 如果电影文件的实际路径带有下列某个字符串,则跳过合并,以;隔开 7 | exclude_str = "Remux - 特效字幕;" 8 | 9 | import time 10 | 11 | 12 | def measure_time(func): 13 | def wrapper(*args, **kwargs): 14 | start_time = time.time() 15 | result = func(*args, **kwargs) 16 | end_time = time.time() 17 | total_time = end_time - start_time 18 | print(f"总耗时: {total_time:.2f} 秒") 19 | return result 20 | 21 | return wrapper 22 | 23 | 24 | # 获取所有电影的信息 25 | def get_all_movies(): 26 | url = f"{EMBY_SERVER_URL}/emby/Items" 27 | params = { 28 | "api_key": API_KEY, 29 | "IncludeItemTypes": "Movie", 30 | "Recursive": True, 31 | "Fields": "ProviderIds,Path", 32 | } 33 | try: 34 | response = requests.get(url, params=params) 35 | response.raise_for_status() # 检查请求是否成功 36 | return response.json()["Items"] 37 | except requests.exceptions.HTTPError as http_err: 38 | print(f"HTTP error occurred: {http_err}") # 输出HTTP错误 39 | except requests.exceptions.RequestException as err: 40 | print(f"Other error occurred: {err}") # 输出其他错误 41 | except ValueError: 42 | print("Error parsing JSON response") 43 | print("Response content:", response.content) # 输出响应内容以便调试 44 | return [] 45 | 46 | 47 | # 按照TMDb ID分组 48 | def group_movies_by_tmdbid(movies): 49 | grouped_movies = {} 50 | if ";" in exclude_str: 51 | exclude_list = exclude_str.split(";") 52 | exclude_list = [item for item in exclude_list if len(item) > 0] 53 | else: 54 | exclude_list = [exclude_str] 55 | for movie in movies: 56 | tmdb_id = movie.get("ProviderIds", {}).get("Tmdb", "") 57 | file_path = movie.get("Path", "") 58 | if any(item in file_path for item in exclude_list): 59 | continue 60 | if tmdb_id: 61 | if tmdb_id not in grouped_movies: 62 | grouped_movies[tmdb_id] = [] 63 | grouped_movies[tmdb_id].append(movie) 64 | return grouped_movies 65 | 66 | 67 | # 合并同一个TMDb ID下的不同版本 68 | @measure_time 69 | def merge_movie_versions(grouped_movies): 70 | merged_movies = [] 71 | for tmdb_id, movies in grouped_movies.items(): 72 | if len(movies) > 1: 73 | name = movies[0]["Name"] 74 | print(f"已发现相同版本的电影::: {name} \n") 75 | # for movie in movies: 76 | # print(f" - {movie['Name']} ({movie['Id']})") 77 | # 调用Emby API进行合并 78 | item_ids = ",".join(movie["Id"] for movie in movies) 79 | merge_url = f"{EMBY_SERVER_URL}/emby/Videos/MergeVersions" 80 | payload = { 81 | "Ids": item_ids, 82 | "X-Emby-Token": API_KEY, 83 | } 84 | print(f"合并版本成功::: {name}\n") 85 | response = requests.post(merge_url, params=payload) 86 | if response.status_code == 204: 87 | print(f"合并版本成功::: {name}") 88 | else: 89 | print(f"合并版本失败::: {name}") 90 | merged_movies.append(movies[0]) # 暂时保留第一部电影为合并结果 91 | return merged_movies 92 | 93 | 94 | # 主函数 95 | def main(): 96 | all_movies = get_all_movies() 97 | if not all_movies: 98 | print("没有多版本的电影") 99 | return 100 | grouped_movies = group_movies_by_tmdbid(all_movies) 101 | merged_movies = merge_movie_versions(grouped_movies) 102 | print(f"共合并电影数: {len(merged_movies)}") 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /视频封面制作/CoverMaker.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont, ImageSequence 2 | import os, re 3 | import traceback 4 | 5 | 6 | class CoverMaker: 7 | def __init__(self) -> None: 8 | self.image_path = "" 9 | 10 | def set_image_path(self, file_path): 11 | self.image_path = file_path 12 | 13 | def start_to_make(self, text, eng_text, eng_font_path=""): 14 | if self.image_path.lower().endswith(".gif"): 15 | out_put = self.crop_and_draw_text_gif(text, eng_text, eng_font_path) 16 | else: 17 | out_put = self.crop_and_draw_text( 18 | text, eng_text, eng_font_path=eng_font_path 19 | ) 20 | return out_put 21 | 22 | def crop_and_draw_text(self, text, eng_text, font_size_ratio=0.2, eng_font_path=""): 23 | try: 24 | image_path = self.image_path 25 | # 保存处理后的图像 26 | image_dir = os.path.dirname(image_path) 27 | _, ext = os.path.splitext(image_path) 28 | eng_font_name = ( 29 | os.path.basename(eng_font_path).replace(".ttf", "").replace(".otf", "") 30 | ) 31 | output_path = os.path.join(image_dir, f"{text}-封面-{eng_font_name}{ext}") 32 | # if os.path.exists(output_path): 33 | # return 34 | # 打开图像 35 | image = Image.open(image_path) 36 | 37 | # 裁剪图像为16:9的比例 38 | width, height = image.size 39 | target_ratio = 16 / 9 40 | current_ratio = width / height 41 | if current_ratio > target_ratio: 42 | new_width = int(height * target_ratio) 43 | left = (width - new_width) // 2 44 | right = left + new_width 45 | image = image.crop((left, 0, right, height)) 46 | elif current_ratio < target_ratio: 47 | new_height = int(width / target_ratio) 48 | top = (height - new_height) // 2 49 | bottom = top + new_height 50 | image = image.crop((0, top, width, bottom)) 51 | 52 | width, height = image.size 53 | new_width = 960 54 | new_height = int(new_width * height / width) 55 | 56 | # 调整图片大小 57 | image = image.resize((new_width, new_height)) 58 | width, height = image.size 59 | 60 | draw = ImageDraw.Draw(image) 61 | 62 | # 计算字体大小 63 | font_size = int(height * font_size_ratio) 64 | eng_font_size = int(font_size / 2) 65 | eng_font = font = ImageFont.truetype( 66 | eng_font_path, 67 | eng_font_size, 68 | ) # 可替换为您喜欢的字体和大小 69 | font = ImageFont.truetype( 70 | "方正综艺简体.ttf", 71 | font_size, 72 | ) # 可替换为您喜欢的字体和大小 73 | 74 | eng_text_color = (0, 0, 0) # 黑色 75 | 76 | text_color = (255, 255, 255) # 白色 77 | shadow_color = (0, 0, 0) # 黑色 78 | shadow_offset = (5, 5) # 阴影偏移量 79 | 80 | # 计算文本宽度和高度 81 | bbox = draw.textbbox((0, 0), eng_text, font=eng_font) 82 | eng_text_width = bbox[2] - bbox[0] 83 | eng_text_height = bbox[3] - bbox[1] 84 | 85 | # 创建一个纯白色的图像 86 | white_height = eng_text_height + 19 87 | white_image = Image.new("RGB", (image.width, white_height), (255, 255, 255)) 88 | 89 | eng_text_position = ( 90 | (image.width - eng_text_width) // 2, 91 | image.height 92 | - white_height 93 | + (white_height - eng_text_height) // 2 94 | - 4.5, 95 | ) 96 | 97 | # 将白色图像粘贴到指定区域 98 | image.paste(white_image, (0, image.height - white_height)) 99 | 100 | bbox = draw.textbbox((0, 0), text, font=eng_font) 101 | # 计算文本宽度和高度 102 | text_width = bbox[2] - bbox[0] 103 | text_height = bbox[3] - bbox[1] 104 | 105 | # # 确定文本位置 106 | # text_position = ( 107 | # (image.width - text_width) // 2, 108 | # image.height - text_height - eng_text_height - 30, 109 | # ) 110 | 111 | # 确定文本位置 112 | text_position = ( 113 | 35, 114 | image.height - text_height - eng_text_height - 97, 115 | ) 116 | # 绘制阴影 117 | shadow_position = ( 118 | text_position[0] + shadow_offset[0], 119 | text_position[1] + shadow_offset[1], 120 | ) 121 | text = self.add_spaces_between_strings(text) 122 | draw.text(shadow_position, text, font=font, fill=shadow_color) 123 | 124 | # 绘制文本 125 | draw.text(eng_text_position, eng_text, font=eng_font, fill=eng_text_color) 126 | draw.text(text_position, text, font=font, fill=text_color) 127 | 128 | image.save(output_path) 129 | return os.path.split(output_path)[-1] 130 | # 显示处理后的图像 131 | # image.show() 132 | except: 133 | print(f"生成封面图片出错:{traceback.format_exc()}") 134 | 135 | def crop_and_draw_text_gif(self, text, eng_text, font_size_ratio=0.2, eng_font=""): 136 | try: 137 | image_path = self.image_path 138 | # 保存处理后的图像 139 | image_dir = os.path.dirname(image_path) 140 | _, ext = os.path.splitext(image_path) 141 | output_path = os.path.join(image_dir, f"{text}-封面.png") 142 | # if os.path.exists(output_path): 143 | # return 144 | # 打开图像 145 | gif_image = Image.open(image_path) 146 | frames = [] 147 | # 迭代每一帧 148 | for image in ImageSequence.Iterator(gif_image): 149 | chinese_text = "" 150 | # 裁剪图像为16:9的比例 151 | image = image.convert("RGB") 152 | width, height = image.size 153 | target_ratio = 16 / 9 154 | current_ratio = width / height 155 | if current_ratio > target_ratio: 156 | new_width = int(height * target_ratio) 157 | left = (width - new_width) // 2 158 | right = left + new_width 159 | image = image.crop((left, 0, right, height)) 160 | elif current_ratio < target_ratio: 161 | new_height = int(width / target_ratio) 162 | top = (height - new_height) // 2 163 | bottom = top + new_height 164 | image = image.crop((0, top, width, bottom)) 165 | width, height = image.size 166 | new_width = 300 167 | new_height = int(new_width * height / width) 168 | 169 | # 调整图片大小 170 | image = image.resize((new_width, new_height)) 171 | width, height = image.size 172 | 173 | draw = ImageDraw.Draw(image) 174 | 175 | # 计算字体大小 176 | font_size = int(height * font_size_ratio) 177 | eng_font_size = int(font_size / 2) 178 | eng_font = font = ImageFont.truetype( 179 | "./static/font/方正综艺简体.ttf", 180 | eng_font_size, 181 | ) # 可替换为您喜欢的字体和大小 182 | font = ImageFont.truetype( 183 | "./static/font/方正综艺简体.ttf", 184 | font_size, 185 | ) # 可替换为您喜欢的字体和大小 186 | 187 | eng_text_color = (0, 0, 0) # 黑色 188 | 189 | text_color = (255, 255, 255) # 白色 190 | shadow_color = (0, 0, 0) # 黑色 191 | shadow_offset = (2, 2) # 阴影偏移量 192 | 193 | # 计算文本宽度和高度 194 | bbox = draw.textbbox((0, 0), eng_text, font=eng_font) 195 | eng_text_width = bbox[2] - bbox[0] 196 | eng_text_height = bbox[3] - bbox[1] 197 | 198 | # 创建一个纯白色的图像 199 | white_height = eng_text_height + 10 200 | white_image = Image.new( 201 | "RGB", (image.width, white_height), (255, 255, 255) 202 | ) 203 | 204 | eng_text_position = ( 205 | (image.width - eng_text_width) // 2, 206 | image.height 207 | - white_height 208 | + (white_height - eng_text_height) // 2 209 | - 3, 210 | ) 211 | 212 | # 将白色图像粘贴到指定区域 213 | image.paste(white_image, (0, image.height - white_height)) 214 | 215 | bbox = draw.textbbox((0, 0), chinese_text, font=eng_font) 216 | # 计算文本宽度和高度 217 | text_width = bbox[2] - bbox[0] 218 | text_height = bbox[3] - bbox[1] 219 | 220 | # # 确定文本位置 221 | # text_position = ( 222 | # (image.width - text_width) // 2, 223 | # image.height - text_height - eng_text_height - 30, 224 | # ) 225 | 226 | # 确定文本位置 227 | text_position = ( 228 | 10, 229 | image.height - text_height - eng_text_height - 50, 230 | ) 231 | # 绘制阴影 232 | shadow_position = ( 233 | text_position[0] + shadow_offset[0], 234 | text_position[1] + shadow_offset[1], 235 | ) 236 | chinese_text = self.add_spaces_between_strings(text) 237 | 238 | draw.text(shadow_position, chinese_text, font=font, fill=shadow_color) 239 | 240 | # 绘制文本 241 | draw.text( 242 | eng_text_position, eng_text, font=eng_font, fill=eng_text_color 243 | ) 244 | draw.text(text_position, chinese_text, font=font, fill=text_color) 245 | frames.append(image) 246 | 247 | # 保存截取后的 GIF 248 | frames[0].save(output_path, save_all=True, append_images=frames[1:]) 249 | return os.path.split(output_path)[-1] 250 | except: 251 | print(f"生成封面图片出错:{traceback.format_exc()}") 252 | # 显示处理后的图像 253 | # image.show() 254 | 255 | def add_spaces_between_strings(self, input_string): 256 | # 使用正则表达式在相邻的中文字符之间插入空格 257 | # result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 258 | result = re.sub(r"([\u4e00-\u9fff])", "\\1 ", input_string) 259 | return result 260 | 261 | 262 | if __name__ == "__main__": 263 | cover_maker = CoverMaker() 264 | cover_maker.set_image_path("/Users/shenxian/Downloads/封面/欧美电影.jpeg") 265 | for root, dirs, files in os.walk("/Users/shenxian/Downloads/未命名文件夹 2"): 266 | for file in files: 267 | if file.endswith(".ttf") or file.endswith(".otf"): 268 | file_path = os.path.join(root, file) 269 | cover_maker.start_to_make( 270 | "欧美电影", "WESTERN MOVIES", eng_font_path=file_path 271 | ) 272 | -------------------------------------------------------------------------------- /视频封面制作/动态图封面制作.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont, ImageSequence 2 | import os 3 | import traceback 4 | import re 5 | 6 | 7 | class GifTextAdder: 8 | def __init__(self, image_path): 9 | self.image_path = image_path 10 | 11 | def crop_and_draw_text_gif(self, text, eng_text, font_size_ratio=0.2): 12 | try: 13 | image_path = self.image_path 14 | # 保存处理后的图像 15 | image_dir = os.path.dirname(image_path) 16 | _, ext = os.path.splitext(image_path) 17 | output_path = os.path.join(image_dir, f"{text}-封面{ext}") 18 | # if os.path.exists(output_path): 19 | # return 20 | # 打开图像 21 | gif_image = Image.open(image_path) 22 | frames = [] 23 | # 迭代每一帧 24 | for image in ImageSequence.Iterator(gif_image): 25 | chinese_text = "" 26 | # 裁剪图像为16:9的比例 27 | image = image.convert("RGB") 28 | width, height = image.size 29 | target_ratio = 16 / 9 30 | current_ratio = width / height 31 | if current_ratio > target_ratio: 32 | new_width = int(height * target_ratio) 33 | left = (width - new_width) // 2 34 | right = left + new_width 35 | image = image.crop((left, 0, right, height)) 36 | elif current_ratio < target_ratio: 37 | new_height = int(width / target_ratio) 38 | top = (height - new_height) // 2 39 | bottom = top + new_height 40 | image = image.crop((0, top, width, bottom)) 41 | width, height = image.size 42 | new_width = 960 43 | new_height = int(new_width * height / width) 44 | 45 | # 调整图片大小 46 | image = image.resize((new_width, new_height)) 47 | width, height = image.size 48 | 49 | draw = ImageDraw.Draw(image) 50 | 51 | # 计算字体大小 52 | font_size = int(height * font_size_ratio) 53 | eng_font_size = int(font_size / 2) 54 | eng_font = font = ImageFont.truetype( 55 | "./static/font/方正综艺简体.ttf", 56 | eng_font_size, 57 | ) # 可替换为您喜欢的字体和大小 58 | font = ImageFont.truetype( 59 | "./static/font/方正综艺简体.ttf", 60 | font_size, 61 | ) # 可替换为您喜欢的字体和大小 62 | 63 | eng_text_color = (0, 0, 0) # 黑色 64 | 65 | text_color = (255, 255, 255) # 白色 66 | shadow_color = (0, 0, 0) # 黑色 67 | shadow_offset = (5, 5) # 阴影偏移量 68 | 69 | # 计算文本宽度和高度 70 | bbox = draw.textbbox((0, 0), eng_text, font=eng_font) 71 | eng_text_width = bbox[2] - bbox[0] 72 | eng_text_height = bbox[3] - bbox[1] 73 | 74 | # 创建一个纯白色的图像 75 | white_height = eng_text_height + 10 76 | white_image = Image.new( 77 | "RGB", (image.width, white_height), (255, 255, 255) 78 | ) 79 | 80 | eng_text_position = ( 81 | (image.width - eng_text_width) // 2, 82 | image.height 83 | - white_height 84 | + (white_height - eng_text_height) // 2 85 | - 5, 86 | ) 87 | 88 | # 将白色图像粘贴到指定区域 89 | image.paste(white_image, (0, image.height - white_height)) 90 | 91 | bbox = draw.textbbox((0, 0), chinese_text, font=eng_font) 92 | # 计算文本宽度和高度 93 | text_width = bbox[2] - bbox[0] 94 | text_height = bbox[3] - bbox[1] 95 | 96 | # # 确定文本位置 97 | # text_position = ( 98 | # (image.width - text_width) // 2, 99 | # image.height - text_height - eng_text_height - 30, 100 | # ) 101 | 102 | # 确定文本位置 103 | text_position = ( 104 | 35, 105 | image.height - text_height - eng_text_height - 135, 106 | ) 107 | # 绘制阴影 108 | shadow_position = ( 109 | text_position[0] + shadow_offset[0], 110 | text_position[1] + shadow_offset[1], 111 | ) 112 | chinese_text = self.add_spaces_between_strings(text) 113 | 114 | draw.text(shadow_position, chinese_text, font=font, fill=shadow_color) 115 | 116 | # 绘制文本 117 | draw.text( 118 | eng_text_position, eng_text, font=eng_font, fill=eng_text_color 119 | ) 120 | draw.text(text_position, chinese_text, font=font, fill=text_color) 121 | frames.append(image) 122 | 123 | # 保存截取后的 GIF 124 | frames[0].save(output_path, save_all=True, append_images=frames[1:]) 125 | print("GIF 图像截取成功!") 126 | return os.path.split(output_path)[-1] 127 | except: 128 | print(f"生成封面图片出错:{traceback.format_exc()}") 129 | # 显示处理后的图像 130 | # image.show() 131 | 132 | def add_spaces_between_strings(self, input_string): 133 | # 使用正则表达式在相邻的中文字符之间插入空格 134 | # result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 135 | result = re.sub(r"([\u4e00-\u9fff])", "\\1 ", input_string) 136 | return result 137 | 138 | 139 | # 示例用法 140 | gif_text_adder = GifTextAdder("/Users/shenxian/Downloads/港台剧.gif") 141 | text = "日韩电影" # 水印文本 142 | eng_text = "English Text" # 英文文本 143 | output_path = gif_text_adder.crop_and_draw_text(text, eng_text) 144 | print("Output path:", output_path) 145 | -------------------------------------------------------------------------------- /视频封面制作/封面制作-加横幅.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import os, re 3 | 4 | 5 | def crop_image(image): 6 | # 裁剪图像为16:9的比例 7 | width, height = image.size 8 | target_ratio = 16 / 9 9 | current_ratio = width / height 10 | if current_ratio > target_ratio: 11 | new_width = int(height * target_ratio) 12 | left = (width - new_width) // 2 13 | right = left + new_width 14 | image = image.crop((left, 0, right, height)) 15 | elif current_ratio < target_ratio: 16 | new_height = int(width / target_ratio) 17 | top = (height - new_height) // 2 18 | bottom = top + new_height 19 | image = image.crop((0, top, width, bottom)) 20 | return image 21 | 22 | 23 | def crop_and_draw_text(image_path, text, eng_text, font_size_ratio): 24 | eng_text_color = (0, 0, 0) # 黑色 25 | text_color = (255, 255, 255) # 白色 26 | shadow_color = (0, 0, 0) # 黑色 27 | shadow_offset = (5, 5) # 阴影偏移量 28 | 29 | # 保存处理后的图像 30 | image_dir = os.path.dirname(image_path) 31 | _, ext = os.path.splitext(image_path) 32 | output_path = os.path.join(image_dir, f"{text}-封面{ext}") 33 | # if os.path.exists(output_path): 34 | # return 35 | # 打开图像 36 | image = Image.open(image_path) 37 | 38 | image = crop_image(image) 39 | 40 | width, height = image.size 41 | new_width = 960 42 | new_height = int(new_width * height / width) 43 | image = image.resize((new_width, new_height)) 44 | width, height = image.size 45 | 46 | draw = ImageDraw.Draw(image) 47 | 48 | # 计算字体大小 49 | font_size = int(height * font_size_ratio) 50 | eng_font_size = int(font_size / 1.5) 51 | eng_font = font = ImageFont.truetype( 52 | "方正综艺简体.ttf", 53 | eng_font_size, 54 | ) # 可替换为您喜欢的字体和大小 55 | font = ImageFont.truetype( 56 | "方正综艺简体.ttf", 57 | font_size, 58 | ) # 可替换为您喜欢的字体和大小 59 | 60 | # 计算文本宽度和高度 61 | eng_text_width, eng_text_height = draw.textsize(eng_text, font=eng_font) 62 | 63 | # 创建一个纯白色的图像 64 | white_height = eng_text_height + 10 65 | white_image = Image.new( 66 | "RGB", (image.width, image.height + white_height), (255, 255, 255) 67 | ) 68 | 69 | white_image.paste(image, (0, 0)) 70 | image = white_image 71 | image = crop_image(image) 72 | 73 | width, height = image.size 74 | new_width = 960 75 | new_height = int(new_width * height / width) 76 | 77 | eng_text_width = int(eng_text_width * new_width / width * 0.3) 78 | eng_text_height = int(eng_text_height * new_width / width * 0.3) 79 | white_height = int(white_height * new_width / width) 80 | 81 | # 调整图片大小 82 | image = image.resize((new_width, new_height)) 83 | width, height = image.size 84 | 85 | eng_text_position = ( 86 | (image.width - eng_text_width) // 2, 87 | image.height - white_height + (white_height - eng_text_height) // 2 - 1, 88 | ) 89 | 90 | draw = ImageDraw.Draw(image) 91 | 92 | # 计算文本宽度和高度 93 | text_width, text_height = draw.textsize(text, font=font) 94 | 95 | # 确定文本位置 96 | text_position = ( 97 | 35, 98 | image.height - text_height - eng_text_height - 30, 99 | ) 100 | # 绘制阴影 101 | shadow_position = ( 102 | text_position[0] + shadow_offset[0], 103 | text_position[1] + shadow_offset[1], 104 | ) 105 | text = add_spaces_between_strings(text) 106 | draw.text(shadow_position, text, font=font, fill=shadow_color) 107 | # 绘制文本 108 | draw.text(eng_text_position, eng_text, font=eng_font, fill=eng_text_color) 109 | draw.text(text_position, text, font=font, fill=text_color) 110 | 111 | image.save(output_path) 112 | 113 | # 显示处理后的图像 114 | # image.show() 115 | 116 | 117 | def add_spaces_between_strings(input_string): 118 | # 使用正则表达式在相邻的中文字符之间插入空格 119 | result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 120 | return result 121 | 122 | 123 | cover_list = { 124 | "日剧": "Japan Dramas", 125 | "韩剧": "Korea Dramas", 126 | "国产剧": "China Dramas", 127 | "欧美剧": "Western Dramas", 128 | "华语电影": "China Movies", 129 | "欧美电影": "Western Movies", 130 | "日韩电影": "Japan & Korea Movies", 131 | "动画电影": "Anime Movies", 132 | "蓝光电影": "BlueRay Movies", 133 | "周星驰系列": "Stephen Chou", 134 | "日漫": "Japan Anime", 135 | "国漫": "China Anime", 136 | "儿童动漫": "Children Anime", 137 | "纪录片": "Documentries", 138 | "电视剧集": "Tv Shows", 139 | "动画片": "Anime Shows", 140 | "动漫": "Japan Anime", 141 | "国外电影": "Foreign Movies", 142 | "韩国": "Korea Media", 143 | "中国": "China Media", 144 | "综艺": "Varieties", 145 | "TOP 250": "Top 250", 146 | } 147 | 148 | image_dir = "/Users/shenxian/Downloads/用所选项目新建的文件夹_副本" 149 | font_size_ratio = 0.15 150 | for file in os.listdir(image_dir): 151 | filename, _ = os.path.splitext(file) 152 | if ( 153 | not file.endswith((".png", ".jpg", ".jpeg")) 154 | or filename not in cover_list.keys() 155 | ): 156 | continue 157 | image_path = os.path.join(image_dir, file) 158 | text = filename 159 | eng_text = cover_list[filename] 160 | crop_and_draw_text(image_path, text, eng_text, font_size_ratio) 161 | # for text, eng_text in cover_list.items(): 162 | # image_path = os.path.join(image_dir, f"{text}.jpeg") 163 | # if not os.path.exists(image_path): 164 | # continue 165 | # crop_and_draw_text(image_path, text, eng_text, font_size_ratio) 166 | -------------------------------------------------------------------------------- /视频封面制作/封面制作-双语.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import os, re 3 | 4 | working_directory = os.path.dirname(os.path.abspath(__file__)) 5 | os.chdir(working_directory) 6 | 7 | 8 | def crop_and_draw_text(image_path, text, eng_text, font_size_ratio=0.2): 9 | # 保存处理后的图像 10 | image_dir = os.path.dirname(image_path) 11 | _, ext = os.path.splitext(image_path) 12 | output_path = os.path.join(image_dir, f"{text}-封面{ext}") 13 | # if os.path.exists(output_path): 14 | # return 15 | # 打开图像 16 | image = Image.open(image_path) 17 | eng_text = eng_text 18 | # 裁剪图像为16:9的比例 19 | width, height = image.size 20 | target_ratio = 16 / 9 21 | current_ratio = width / height 22 | if current_ratio > target_ratio: 23 | new_width = int(height * target_ratio) 24 | left = (width - new_width) // 2 25 | right = left + new_width 26 | image = image.crop((left, 0, right, height)) 27 | elif current_ratio < target_ratio: 28 | new_height = int(width / target_ratio) 29 | top = (height - new_height) // 2 30 | bottom = top + new_height 31 | image = image.crop((0, top, width, bottom)) 32 | 33 | width, height = image.size 34 | new_width = 960 35 | new_height = int(new_width * height / width) 36 | 37 | # 调整图片大小 38 | image = image.resize((new_width, new_height)) 39 | width, height = image.size 40 | 41 | draw = ImageDraw.Draw(image) 42 | 43 | # 计算字体大小 44 | font_size = int(height * font_size_ratio) 45 | eng_font_size = int(font_size / 2) 46 | eng_font = font = ImageFont.truetype( 47 | "点字倔强黑.ttf", 48 | eng_font_size, 49 | ) # 可替换为您喜欢的字体和大小 50 | font = ImageFont.truetype( 51 | "方正综艺简体.ttf", 52 | font_size, 53 | ) # 可替换为您喜欢的字体和大小 54 | 55 | eng_text_color = (0, 0, 0) # 黑色 56 | 57 | text_color = (255, 255, 255) # 白色 58 | shadow_color = (0, 0, 0) # 黑色 59 | shadow_offset = (5, 5) # 阴影偏移量 60 | 61 | # 计算文本宽度和高度 62 | bbox = draw.textbbox((0, 0), eng_text, font=eng_font) 63 | eng_text_width = bbox[2] - bbox[0] 64 | eng_text_height = bbox[3] - bbox[1] 65 | 66 | # 创建一个纯白色的图像 67 | white_height = eng_text_height + 20 68 | white_image = Image.new("RGB", (image.width, white_height), (255, 255, 255)) 69 | 70 | eng_text_position = ( 71 | (image.width - eng_text_width) // 2, 72 | image.height - white_height + (white_height - eng_text_height) // 2 - 15, 73 | ) 74 | 75 | # 将白色图像粘贴到指定区域 76 | image.paste(white_image, (0, image.height - white_height)) 77 | 78 | print(image.height, white_height, eng_text_height, eng_text_position) 79 | bbox = draw.textbbox((0, 0), text, font=eng_font) 80 | # 计算文本宽度和高度 81 | text_width = bbox[2] - bbox[0] 82 | text_height = bbox[3] - bbox[1] 83 | 84 | # # 确定文本位置 85 | # text_position = ( 86 | # (image.width - text_width) // 2, 87 | # image.height - text_height - eng_text_height - 30, 88 | # ) 89 | 90 | # 确定文本位置 91 | text_position = ( 92 | 35, 93 | image.height - text_height - eng_text_height - 100, 94 | ) 95 | # 绘制阴影 96 | shadow_position = ( 97 | text_position[0] + shadow_offset[0], 98 | text_position[1] + shadow_offset[1], 99 | ) 100 | text = add_spaces_between_strings(text) 101 | draw.text(shadow_position, text, font=font, fill=shadow_color) 102 | 103 | # 绘制文本 104 | draw.text(eng_text_position, eng_text, font=eng_font, fill=eng_text_color) 105 | draw.text(text_position, text, font=font, fill=text_color) 106 | 107 | image.save(output_path) 108 | 109 | # 显示处理后的图像 110 | # image.show() 111 | 112 | 113 | def add_spaces_between_strings(input_string): 114 | # 使用正则表达式在相邻的中文字符之间插入空格 115 | # result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 116 | print(input_string) 117 | result = re.sub(r"([\u4e00-\u9fff])", "\\1 ", input_string) 118 | return result 119 | 120 | 121 | cover_list = { 122 | "观影": "Movies", 123 | "追剧": "Tv Shows", 124 | "日剧": "Japan Dramas", 125 | "韩剧": "Korea Dramas", 126 | "国产剧": "China Dramas", 127 | "欧美剧": "Western Dramas", 128 | "华语电影": "China Movies", 129 | "欧美电影": "Western Movies", 130 | "日韩电影": "Japan Korea Movies", 131 | "动画电影": "Anime Movies", 132 | "蓝光电影": "BlueRay Movies", 133 | "周星驰系列": "Stephen Chou", 134 | "日漫": "Japan Anime", 135 | "国漫": "China Anime", 136 | "儿童动漫": "Children Anime", 137 | "纪录片": "Documentries", 138 | "电视剧集": "Tv Shows", 139 | "动画片": "Anime Shows", 140 | "动漫": "Japan Anime", 141 | "国外电影": "Foreign Movies", 142 | "韩国": "Korea Media", 143 | "中国": "China Media", 144 | "综艺": "Varieties", 145 | "音乐": "Music", 146 | "4K REMUX": "4K REMUX", 147 | "Top 250": "Top 250", 148 | } 149 | 150 | image_dir = "/Users/shenxian/Downloads/封面" 151 | font_size_ratio = 0.2 152 | for file in os.listdir(image_dir): 153 | filename, _ = os.path.splitext(file) 154 | if ( 155 | not file.endswith((".png", ".jpg", ".jpeg")) 156 | or filename not in cover_list.keys() 157 | ): 158 | continue 159 | image_path = os.path.join(image_dir, file) 160 | text = filename 161 | eng_text = cover_list[filename] 162 | crop_and_draw_text(image_path, text, eng_text, font_size_ratio) 163 | -------------------------------------------------------------------------------- /视频封面制作/封面制作-居中.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import os, re 3 | 4 | 5 | def crop_and_draw_text(image_path, text, font_size_ratio): 6 | # 保存处理后的图像 7 | image_dir = os.path.dirname(image_path) 8 | _, ext = os.path.splitext(image_path) 9 | output_path = os.path.join(image_dir, f"{text}{ext}") 10 | # 打开图像 11 | image = Image.open(image_path) 12 | 13 | # 裁剪图像为16:9的比例 14 | width, height = image.size 15 | target_ratio = 16 / 9 16 | current_ratio = width / height 17 | if current_ratio > target_ratio: 18 | new_width = int(height * target_ratio) 19 | left = (width - new_width) // 2 20 | right = left + new_width 21 | image = image.crop((left, 0, right, height)) 22 | elif current_ratio < target_ratio: 23 | new_height = int(width / target_ratio) 24 | top = (height - new_height) // 2 25 | bottom = top + new_height 26 | image = image.crop((0, top, width, bottom)) 27 | 28 | new_width = 960 29 | new_height = int(new_width * height / width) 30 | 31 | # 调整图片大小 32 | image = image.resize((new_width, new_height)) 33 | width, height = image.size 34 | 35 | draw = ImageDraw.Draw(image) 36 | 37 | # 计算字体大小 38 | font_size = int(height * font_size_ratio) 39 | font = ImageFont.truetype( 40 | "方正综艺简体.ttf", 41 | font_size, 42 | ) # 可替换为您喜欢的字体和大小 43 | text_color = (255, 255, 255) # 白色 44 | shadow_color = (0, 0, 0) # 黑色 45 | shadow_offset = (5, 5) # 阴影偏移量 46 | 47 | # 计算文本宽度和高度 48 | text_width, text_height = draw.textsize(text, font=font) 49 | 50 | # 确定文本位置 51 | text_position = ((image.width - text_width) // 2, image.height - text_height - 60) 52 | 53 | # 绘制阴影 54 | shadow_position = ( 55 | text_position[0] + shadow_offset[0], 56 | text_position[1] + shadow_offset[1], 57 | ) 58 | text = add_spaces_between_strings(text) 59 | # draw.text(shadow_position, text, font=font, fill=shadow_color) 60 | 61 | # 绘制文本 62 | draw.text(text_position, text, font=font, fill=text_color) 63 | 64 | image.save(output_path) 65 | 66 | # 显示处理后的图像 67 | image.show() 68 | 69 | 70 | def add_spaces_between_strings(input_string): 71 | # 使用正则表达式在相邻的中文字符之间插入空格 72 | result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 73 | return result 74 | 75 | 76 | # 示例用法 77 | input_path = "/Users/shenxian/Downloads/1.jpeg,韩剧" 78 | font_size_ratio = 0.2 79 | image_path, text = input_path.split(",") 80 | crop_and_draw_text(image_path, text, font_size_ratio) 81 | -------------------------------------------------------------------------------- /视频封面制作/封面制作.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import os, re 3 | 4 | 5 | def crop_and_draw_text(image_path, text, eng_text, font_size_ratio): 6 | # 保存处理后的图像 7 | image_dir = os.path.dirname(image_path) 8 | _, ext = os.path.splitext(image_path) 9 | output_path = os.path.join(image_dir, f"{text}-封面{ext}") 10 | # if os.path.exists(output_path): 11 | # return 12 | # 打开图像 13 | image = Image.open(image_path) 14 | 15 | # 裁剪图像为16:9的比例 16 | width, height = image.size 17 | target_ratio = 16 / 9 18 | current_ratio = width / height 19 | if current_ratio > target_ratio: 20 | new_width = int(height * target_ratio) 21 | left = (width - new_width) // 2 22 | right = left + new_width 23 | image = image.crop((left, 0, right, height)) 24 | elif current_ratio < target_ratio: 25 | new_height = int(width / target_ratio) 26 | top = (height - new_height) // 2 27 | bottom = top + new_height 28 | image = image.crop((0, top, width, bottom)) 29 | 30 | width, height = image.size 31 | new_width = 960 32 | new_height = int(new_width * height / width) 33 | 34 | # 调整图片大小 35 | image = image.resize((new_width, new_height)) 36 | width, height = image.size 37 | 38 | draw = ImageDraw.Draw(image) 39 | 40 | # 计算字体大小 41 | font_size = int(height * font_size_ratio) 42 | eng_font_size = int(font_size / 2) 43 | eng_font = font = ImageFont.truetype( 44 | "方正综艺简体.ttf", 45 | eng_font_size, 46 | ) # 可替换为您喜欢的字体和大小 47 | font = ImageFont.truetype( 48 | "方正综艺简体.ttf", 49 | font_size, 50 | ) # 可替换为您喜欢的字体和大小 51 | 52 | eng_text_color = (0, 0, 0) # 黑色 53 | 54 | text_color = (255, 255, 255) # 白色 55 | shadow_color = (0, 0, 0) # 黑色 56 | shadow_offset = (5, 5) # 阴影偏移量 57 | 58 | # 计算文本宽度和高度 59 | eng_text_width, eng_text_height = draw.textsize(eng_text, font=eng_font) 60 | 61 | # 创建一个纯白色的图像 62 | white_height = eng_text_height + 10 63 | white_image = Image.new("RGB", (image.width, white_height), (255, 255, 255)) 64 | 65 | eng_text_position = ( 66 | (image.width - eng_text_width) // 2, 67 | image.height - white_height + (white_height - eng_text_height) // 2 - 3, 68 | ) 69 | 70 | # 将白色图像粘贴到指定区域 71 | image.paste(white_image, (0, image.height - white_height)) 72 | 73 | print(image.height, white_height, eng_text_height, eng_text_position) 74 | # 计算文本宽度和高度 75 | text_width, text_height = draw.textsize(text, font=font) 76 | 77 | # # 确定文本位置 78 | # text_position = ( 79 | # (image.width - text_width) // 2, 80 | # image.height - text_height - eng_text_height - 30, 81 | # ) 82 | 83 | # 确定文本位置 84 | text_position = ( 85 | 35, 86 | image.height - text_height - eng_text_height - 30, 87 | ) 88 | # 绘制阴影 89 | shadow_position = ( 90 | text_position[0] + shadow_offset[0], 91 | text_position[1] + shadow_offset[1], 92 | ) 93 | text = add_spaces_between_strings(text) 94 | draw.text(shadow_position, text, font=font, fill=shadow_color) 95 | 96 | # 绘制文本 97 | draw.text(eng_text_position, eng_text, font=eng_font, fill=eng_text_color) 98 | draw.text(text_position, text, font=font, fill=text_color) 99 | 100 | image.save(output_path) 101 | 102 | # 显示处理后的图像 103 | # image.show() 104 | 105 | 106 | def add_spaces_between_strings(input_string): 107 | # 使用正则表达式在相邻的中文字符之间插入空格 108 | result = re.sub(r"(?<=\S)(?=\S)", " ", input_string) 109 | return result 110 | 111 | 112 | # 示例用法 113 | input_path = input("请输入图片路径,中文名,英文名,以','隔开:") 114 | font_size_ratio = 0.2 115 | image_path, text, eng_text = input_path.split(",") 116 | crop_and_draw_text(image_path, text, eng_text, font_size_ratio) 117 | -------------------------------------------------------------------------------- /视频封面制作/拼接图片.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | 4 | 5 | def concatenate_images(folder_path): 6 | # 获取文件夹内的所有图片文件,并按文件名排序 7 | image_files = sorted( 8 | [f for f in os.listdir(folder_path) if f.endswith((".jpg", ".jpeg", ".png"))] 9 | ) 10 | 11 | # 获取尺寸最小的图片作为模板 12 | template_image = Image.open(os.path.join(folder_path, image_files[0])) 13 | min_width = template_image.width 14 | min_height = template_image.height 15 | 16 | # 计算拼接后的图像尺寸 17 | total_width = min_width * len(image_files) 18 | max_height = min_height 19 | 20 | # 创建一个空白画布用于拼接图像 21 | concatenated_image = Image.new("RGB", (total_width, max_height)) 22 | 23 | # 遍历所有图片并进行拼接 24 | for i, image_file in enumerate(image_files): 25 | image_path = os.path.join(folder_path, image_file) 26 | image = Image.open(image_path) 27 | 28 | # 调整图片尺寸以匹配模板图像 29 | image = image.resize((min_width, min_height), Image.ANTIALIAS) 30 | 31 | # 将图片粘贴到拼接图像上 32 | x_offset = i * min_width 33 | concatenated_image.paste(image, (x_offset, 0)) 34 | 35 | # 保存拼接后的图像在与图片所在文件夹相同的位置 36 | output_path = os.path.join(folder_path, "output.jpg") 37 | concatenated_image.save(output_path) 38 | 39 | print(f"拼接完成,保存到 {output_path}") 40 | 41 | 42 | # 示例用法 43 | folder_path = input("请输入文件夹路径:") 44 | concatenate_images(folder_path) 45 | -------------------------------------------------------------------------------- /视频封面制作/方正综艺简体.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/视频封面制作/方正综艺简体.ttf -------------------------------------------------------------------------------- /视频封面制作/方正超粗黑_GBK.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/视频封面制作/方正超粗黑_GBK.ttf -------------------------------------------------------------------------------- /视频封面制作/方正跃进体_GBK.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/视频封面制作/方正跃进体_GBK.ttf -------------------------------------------------------------------------------- /视频封面制作/方正韵动特黑_GBK.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/视频封面制作/方正韵动特黑_GBK.ttf -------------------------------------------------------------------------------- /视频封面制作/点字倔强黑.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenxianmq/MediaHelper/f6ae3869f8457b583df8498ab043b54f28723223/视频封面制作/点字倔强黑.ttf -------------------------------------------------------------------------------- /视频添加封面.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from PIL import Image, ImageDraw, ImageFont 4 | import re 5 | import math 6 | import jieba 7 | 8 | 9 | def extract_chinese(text): 10 | """提取第一个中文字符及之后的部分,并移除括号内的内容以及类似'10集'的字样""" 11 | text = re.sub(r"\(.*?\)|\(.*?\)", "", text) # 移除括号及其内容,使用非贪婪模式 12 | text = re.sub(r"\d+集", "", text) # 移除类似'10集'的字样 13 | for i, char in enumerate(text): 14 | if "\u4e00" <= char <= "\u9fff": 15 | return text[i:] 16 | return text 17 | 18 | 19 | def calculate_font_size( 20 | image_height, text_length, shadow_offset, max_chars_per_column, margin 21 | ): 22 | """计算字体大小以使文字竖直排列时填满图片的高度,同时考虑阴影偏移量和边距""" 23 | # 减去阴影偏移量和边距以确保最后一个字符不超出图片边界 24 | return (image_height - shadow_offset - 2 * margin) // min( 25 | text_length, max_chars_per_column 26 | ) 27 | 28 | 29 | def calculate_columns(text, max_chars_per_column): 30 | """根据jieba分词结果计算列数""" 31 | words = jieba.lcut(text) 32 | columns = [] 33 | column = "" 34 | for word in words: 35 | if len(column) + len(word) > max_chars_per_column: 36 | columns.append(column) 37 | column = word 38 | else: 39 | column += word 40 | if column: 41 | columns.append(column) 42 | return columns 43 | 44 | 45 | # 文件夹路径 46 | folder = "T:\\爆火短剧【10月·11月·12月】\\2024年1月" 47 | font_path = "d:/ziti2.ttf" # 字体文件路径 48 | shadow_offset = 8 # 阴影偏移量 49 | max_chars_per_column = 8 # 每列最多字符数 50 | margin = 75 # 边距 51 | 52 | for root, dirs, files in os.walk(folder): 53 | for dir in dirs: 54 | dir_path = os.path.join(root, dir) 55 | print(f"Checking directory: {dir_path}") # 打印当前检查的目录 56 | 57 | jpg_files = [f for f in os.listdir(dir_path) if f.endswith(".jpg")] 58 | if jpg_files: 59 | source_file = os.path.join(dir_path, jpg_files[0]) 60 | destination_file = os.path.join(dir_path, "poster.jpg") 61 | 62 | # 如果poster.jpg存在,则删除 63 | if os.path.exists(destination_file): 64 | os.remove(destination_file) 65 | # 重新获取jpg文件列表 66 | jpg_files = [f for f in os.listdir(dir_path) if f.endswith(".jpg")] 67 | if not jpg_files: 68 | print(f"No .jpg files found in {dir_path}") 69 | continue 70 | source_file = os.path.join(dir_path, jpg_files[0]) 71 | 72 | try: 73 | shutil.copy2(source_file, destination_file) 74 | 75 | # 打开图片 76 | image = Image.open(destination_file) 77 | image_width, image_height = image.size 78 | 79 | # 提取文字 80 | text = extract_chinese(dir) 81 | 82 | # 计算列数 83 | columns = calculate_columns(text, max_chars_per_column) 84 | num_columns = len(columns) 85 | 86 | # 计算字体大小 87 | font_size = calculate_font_size( 88 | image_height, len(text), shadow_offset, max_chars_per_column, margin 89 | ) 90 | font = ImageFont.truetype(font_path, font_size) 91 | 92 | draw = ImageDraw.Draw(image) 93 | 94 | # 竖直排列文字 95 | text_position_x = 50 # 文字起始横坐标,可以根据需要调整 96 | for i, column in enumerate(columns): 97 | for j, char in enumerate(column): 98 | text_position_y = margin + j * font_size # 每个字符的纵坐标 99 | text_position_x = 50 + i * ( 100 | font_size + shadow_offset 101 | ) # 每个字符的横坐标 102 | 103 | # 绘制阴影 104 | draw.text( 105 | ( 106 | text_position_x + shadow_offset, 107 | text_position_y + shadow_offset, 108 | ), 109 | char, 110 | font=font, 111 | fill=(0, 0, 0), 112 | ) # 阴影颜色为黑色 113 | 114 | # 绘制文字 115 | draw.text( 116 | (text_position_x, text_position_y), 117 | char, 118 | font=font, 119 | fill=(255, 255, 255), 120 | ) # 文字颜色为白色 121 | 122 | # 保存图片 123 | image.save(destination_file) 124 | 125 | print(f"Processed and saved {destination_file}") 126 | except Exception as e: 127 | print(f"Error processing {source_file}: {e}") 128 | else: 129 | print(f"No .jpg files found in {dir_path}") 130 | -------------------------------------------------------------------------------- /集数偏移.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import pyperclip 4 | 5 | #文件名中必须包括“第”集 6 | #文件所在文件夹必须以Season 1的格式命名 7 | def offset_vedio(folder_path): 8 | files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f)) and '第' in f] 9 | 10 | if not files: 11 | if any('Season' in i for i in os.listdir(folder_path)): 12 | folder_list = [os.path.join(folder_path,f) for f in os.listdir(folder_path) if os.path.isdir(os.path.join(folder_path, f)) and 'Season' in f] 13 | for folder in folder_list: 14 | offset_vedio(folder) 15 | return 16 | else: 17 | print("没有找到包含'第'的文件名。") 18 | return 19 | 20 | # 提取所有文件名中的集数 21 | episode_numbers = [int(re.findall(r'第(\d+)集', file)[0]) for file in files] 22 | # 找到最小的集数 23 | min_episode_number = min(episode_numbers) 24 | 25 | # 计算偏移量,使最小集数变为1 26 | offset = 1 - min_episode_number 27 | season_num = '' 28 | if 'Season' in folder_path: 29 | season_num = int(re.findall('.*? (\d+)',folder_path)[0]) 30 | for file in files: 31 | file_path = os.path.join(folder_path,file) 32 | num = re.findall('第(\d+)集',file)[0] 33 | now_season_num = re.findall('S0*(\d+)E',file)[0] 34 | if not season_num: 35 | season_num = now_season_num 36 | new_num = int(num) + offset 37 | new_filepath = file_path.replace(f'E{int(num):02d}',f'E{new_num:02d}').replace(f'第{num}集',f'第{new_num}集').replace(f'S{int(now_season_num):02d}E',f'S{int(season_num):02d}E') 38 | os.rename(file_path,new_filepath) 39 | print(f'已成功偏移集数{new_filepath}') 40 | 41 | if __name__ == '__main__': 42 | folder_path = pyperclip.paste() 43 | offset_vedio(folder_path) --------------------------------------------------------------------------------