最近呢,买了一个 iPad,虽然家里笔记本台式都有,显示器都是 2个,比较方便看代码(边打游戏边追剧)。
但是在床上拿笔记本始终还是不方便,手机在家看还是小了点,自从有 iPad 之后,拿个大屏在家里用着确实舒服不少。
能追剧,能玩玩其他应用,那还得听听音乐不是,但是懂的都懂,苹果里导入文件是个麻烦事,更别说音乐播放。
所以这件事就得研究研究,因为在电脑上已经把音乐都按照文件夹整理好了。
在 Android 中很简单,adb 一推到 Music 目录中,更新一下就行。
但是 iOS 搜了一圈发现还真不好弄,基本都是要通过 Apple 这个音乐导入到资料库,接着再通过 iTunes(新版已经合并在 Finder 中) 进行同步。
我试了下音乐这个应用,确实是可以通过新建歌单后,把需要添加的音乐文件夹直接拖入到歌单中,这样一次就可以添加多首,这个虽然要操作一下,但是也还可以接受。
正当我发现这个方案可行的时候,我看了一眼歌单,发现只有部分歌,就有点纳闷为啥部分歌曲没有导入。
在网上一搜,发现原来不支持无损,就是 FLAC 格式的文件。
这不是尴尬了么,所以看来还需要一个操作把 FLAC 文件转为 mp3 格式再导入才可以。
如果选第三方的工具,比如格式工厂或者狸窝,文件夹太多的情况,都要自己动手就太折腾了,比如我这里有几十个文件夹。(别问为啥这么多,强迫症就是歌手区分,各种风格也要区分)
用过 shell 的朋友都知道,这种批量的工作最好就交给脚本来做,遍历文件夹批量转化所有文件就行。
批量转化音乐
当然这里还有一些其他的逻辑,比如歌曲中已经是 mp3 的格式了,那应该就直接复制,除了 mp3 还有 wav 格式,同理针对 lrc 歌词文件也应该是直接复制。
所以和 ChatGPT “对线”几轮后,终于得到了一个满意的脚本,就不卖关子了。(对线真的考验心态和血压,最好自己能懂部分,可以自己动手改一下 shell)
#!/bin/bash# 检查是否安装了 ffmpeg
if ! command -v ffmpeg &> /dev/null
thenecho "Error: ffmpeg 未安装。请先安装 ffmpeg。"exit 1
fi# 检查参数是否足够
if [ "$#" -ne 2 ]; thenecho "Usage: $0 <import_directory> <export_directory>"exit 1
fi# 输入和输出目录
import_dir="$1"
export_dir="$2"
error_log="$export_dir/error_log.txt"# 检查导入目录是否存在
if [ ! -d "$import_dir" ]; thenecho "Error: 导入目录 $import_dir 不存在。"exit 1
fi# 创建导出目录(如果不存在)
mkdir -p "$export_dir"# 清空错误日志文件
: > "$error_log"# 查找所有文件并计算文件总数
total_files=$(find "$import_dir" -type f | wc -l)
if [ "$total_files" -eq 0 ]; thenecho "Error: 未找到文件。"exit 1
fiecho "共找到 $total_files 个文件,开始处理..."# 初始化计数器
counter=1# 遍历所有歌手目录
for artist_dir in "$import_dir"/*; doif [ -d "$artist_dir" ]; then# 遍历每个歌手目录下的所有文件for song in "$artist_dir"/*; doif [ -f "$song" ]; then# 确定输出文件夹结构rel_path="${song#$import_dir/}"output_dir="$export_dir/$(dirname "$rel_path")"mkdir -p "$output_dir"# 获取文件扩展名ext="${song##*.}"# 处理不同文件类型if [ "$ext" = "flac" ]; then# 转换 FLAC 文件为 MP3,指定比特率 320k,并显式指定编码器output_file="$output_dir/$(basename "${song%.flac}.mp3")"echo "正在转换文件 ($counter/$total_files): $song -> $output_file"# 使用 libmp3lame 编码器,忽略非音频流,并增加 analyzeduration 和 probesizeffmpeg -analyzeduration 100M -probesize 50M -i "$song" -vn -c:a libmp3lame -b:a 320k "$output_file" > /dev/null 2> ffmpeg_errors.txtif [ $? -ne 0 ]; thenecho "Error: 转换 $song 失败。" | tee -a "$error_log"elseecho "转换成功: $output_file"fielif [ "$ext" = "lrc" ] || [ "$ext" = "mp3" ] || [ "$ext" = "wav" ]; then# 直接复制 LRC 和 MP3 文件echo "正在复制文件 ($counter/$total_files): $song -> $output_dir/"cp "$song" "$output_dir/"if [ $? -ne 0 ]; thenecho "Error: 复制 $song 失败。" | tee -a "$error_log"elseecho "复制成功: $song"fielseecho "跳过不支持的文件 ($counter/$total_files): $song"fi# 更新计数器counter=$((counter + 1))fidonefi
doneecho "所有文件已处理完成。"
总结几个点:
- 这里是通过 ffmpeg 进行转换,毕竟这个开源工具很强大,视频都能随便处理,音频处理不是手到擒来么。
- 加入了处理进度,会在控制台输出,这样我们比较好知道处理到哪了,大概还有多久的时间。
- 第三是加入了错误日志导出,这样知道哪些歌曲出错了,没有处理。
因为脚本上也有对应的注释,如果知道一点编程的朋友应该能知道怎么修改一下。但是呢,考虑到可能会有非程序员的朋友看到该文章,还是简单讲一下这里的操作的流程。
打开终端应用。
在里面输入下面的语句,这个是通过 brew 命令安装 ffmpeg 库。
brew install ffmpeg
当命令行自己运行一会,光标重新开始闪烁时一般就是安装完毕。可以通过查看下 ffmpeg 版本看下是否安装好了。
ffmpeg --version
这样就完成了第一步 ffmpeg 安装。接着我们通过命令要新建一个普通文件,命名为 cvt.sh ,意思就是 converte 缩写,当然可以换个任意你喜欢的名字。
touch cvt.sh
一般来说命令行首次打开会在自己的 home 目录下,那么新建也是在这里。
如果会用命令修改的话可以直接通过 vi 打开复制,不会的朋友找到这个文件,然后用文本编辑应用打开。把刚刚那一长串代码复制进去,就像这样,记得保存一下。(Command + s)
第二部脚本文件可以说准备好了,但这里还差一点,就是新建的脚本文件需要加上可执行的权限。
在命令行中输入,这样我们一会才能执行这个转化的脚本。
chmod 711 cvt.sh
万事具备,讲一讲这个用法。(输出的文件夹可以不用存在,会自动创建)
#这里需要把对应的文件夹名字换一下。
bash cvt.sh <输入的文件夹> <输出的文件夹>
这里还要说明一下,脚本扫描的路径层级是这样:
输入的文件夹 - 二级目录(一般是歌手或者歌曲风格) - 该目录下所有歌曲
如果二级目录这个位置是歌曲是不会处理的,因为这么设计是为了方便后续导入 Apple 歌单.
我这里示范一下,假如我的音乐 testMusic 和脚本在一个地方,都在 home 目录下。
bash cvt.sh testMusic outputMusic
这样就开始了,可以看到有复制的,有转换的,也有对应进度。
需要注意的是,因为这里把错误信息导出到文件了,所以当第一次跑脚本,中途取消了,重新跑会发现,命令行卡着不动,实际上可以在当前目录中看到有错误日志,这里提示问是否覆盖。
所以建议如果用这个脚本,就一次性跑完,或者需要重新跑的时候把目标文件夹清除一下。
当然更优秀的朋友应该知道根据自己需求改下脚本,比如文件是直接强行覆盖不用询问么,或者还是需要手动对比。当然每个人的想法不一样,这里就是抛砖引玉。
这样的话,音乐的转换就完成了。
如果只有几个歌单需要添加的朋友,那么手动拖一下到 音乐 中就可以解决问题了。
批量导入歌单
接着就是到歌曲导入为 Apple 的歌单了。
从我前面的强迫症发言来看,就知道我需要导入的歌单不少,那这么多都需要操作一遍岂不是很麻烦,所得想个招,比如有没有办法用脚本来做,所以懒惰才是人类的第一生产力。
问了下 gpt ,好消息-有方案,坏消息-是其他脚本。
Gpt 提到可以用 Mac 自带的脚本编辑器来做,虽然我不会它这个脚本的语法,但是我有 gpt 呀,它会≈我会。😎
把导入的诉求告诉了它,又是一顿 battle 。
算是最后拿出了一个脚本,你还别说,shell 都算语法奇怪的了,苹果这个更奇怪,不过 …… 反正能跑就是好代码不是。
照例加入进度打印,错误输出。
on run argv-- 确保传入的参数数量正确if (count of argv) is not 1 thenerror "请提供一个参数:音乐文件夹的根路径。"end if-- 获取传入路径set inputPath to item 1 of argv-- 检查是否为相对路径,若是则转换为绝对路径if inputPath does not start with "/" thenset currentDirectory to (POSIX path of (do shell script "pwd"))set rootFolderPathString to currentDirectory & "/" & inputPathelseset rootFolderPathString to inputPathend if-- 转换路径为 POSIX file 类型set rootFolderPath to POSIX file rootFolderPathString-- 设置日志文件路径set logFilePath to POSIX file (rootFolderPathString & "/import_log.txt")-- 强制启动音乐应用tell application "Music"launch -- 确保 Music 应用已启动end tell-- 获取根文件夹下的所有文件夹tell application "Finder"set musicFolders to every folder of folder rootFolderPathend tell-- 清空日志文件tryset logFile to open for access logFilePath with write permissionset eof of logFile to 0 -- 清空文件close access logFileon error-- 如果日志文件不存在,则创建它set logFile to open for access logFilePath with write permissionclose access logFileend try-- 总文件夹数量set totalFolders to count of musicFolders-- 遍历每个文件夹repeat with musicFolder in musicFoldersset playlistName to name of musicFolder -- 使用文件夹名作为播放列表名称set musicFolderPath to (musicFolder as alias)tell application "Music"-- 检查是否已经存在同名播放列表set playlistExists to falserepeat with aPlaylist in (get user playlists)if (name of aPlaylist) is equal to playlistName thenset playlistExists to trueset existingPlaylist to aPlaylistexit repeatend ifend repeat-- 如果不存在同名播放列表,则创建新的播放列表if playlistExists thenset targetPlaylist to existingPlaylistelseset targetPlaylist to make new user playlist with properties {name:playlistName}log "Created new playlist: " & playlistNameend if-- 获取该文件夹中的所有音乐文件tell application "Finder"set musicFiles to every file of musicFolderend tell-- 当前文件夹的已处理文件计数set processedFilesInFolder to 0 -- 初始化当前文件夹处理计数-- 将每个文件导入到音乐应用并添加到播放列表repeat with aFile in musicFilesset fileName to name of aFileset fileExtension to (name extension of aFile)log "Checking fileName " & fileName & " ;fileExtension: " & fileExtension-- 只处理 .mp3 和 .wav 文件if fileExtension is "mp3" or fileExtension is "wav" thentry-- 检查文件是否已经在播放列表中set songAlreadyInPlaylist to falselog "File name: " & (name of aFile) -- 查看文件名repeat with aTrack in (get tracks of targetPlaylist)if (name of aTrack) is equal to fileName thenset songAlreadyInPlaylist to truelog "Found existing song in playlist: " & fileNameexit repeatend ifend repeat-- 如果歌曲尚未在播放列表中,才导入if not songAlreadyInPlaylist thenlog "Importing song to playlist: " & fileName-- 确保 aFile 以 alias 形式导入set importedTrack to add (aFile as alias) to targetPlaylistlog "Successfully added: " & fileName--delay 1 -- 添加 1 秒的延迟elselog "Skipping already existing song: " & fileName--delay 1 -- 添加 1 秒的延迟end ifon error errorMsg-- 处理可能的错误,记录详细信息set logMessage to "Error importing file: " & fileName & return & errorMsgmy appendToLog(logMessage, logFilePath)end tryelse-- 记录不支持的文件到日志文件log "Skipping unsupported file: " & fileNameend if-- 增加当前文件夹的已处理文件数量set processedFilesInFolder to processedFilesInFolder + 1-- 打印当前进度my displayProgress(processedFilesInFolder, (count of musicFiles), playlistName)end repeatend tellend repeat-- 打印总的处理完成信息display dialog "所有歌曲已处理完成!" buttons {"OK"} default button 1
end run-- 函数:将消息附加到日志文件
on appendToLog(logMessage, logFilePath)set logFile to open for access logFilePath with write permissionwrite logMessage to logFile starting at eofclose access logFile
end appendToLog-- 函数:显示处理进度
on displayProgress(folderProcessed, totalInFolder, playlistName)set progressPercent to (folderProcessed / totalInFolder) * 100set formattedProgress to round progressPercent * 10 / 10.0 -- 保留一位小数-- 在终端输出进度log "Processing " & playlistName & ": " & (folderProcessed as string) & "/" & (totalInFolder as string) & " (" & (formattedProgress as string) & "%)"
end displayProgress
关于这个脚本的用法,简单讲一下,估计大部分朋友都没有接触过,毕竟不通用。
打开这个编辑器,把刚才的脚本拷贝上,然后保存为 脚本格式。
我这里文件名用的是 importMusic.scpt ,说一下用法。
osascript <脚本名称> <导入的文件夹>
和刚才一样,我的命令行在 home 目录下,新建的 importMusic.scpt 也挪到这个目录,处理后的音乐还在刚才的位置。
那我就可以这么用。
osascript importMusic.scpt outputMusic
接下来就是见证奇迹的时刻。
轻轻松松导入,真是省大心。
当然最后可以把这一段执行代码再组合在前面的 shell 文档中,不过分开一下也好,各个朋友有各自的需求,需求什么用什么。
脚本真是提升效率的利器。
后续计划录个视频把操作和代码上传一下,如果有看视频来的朋友用起来就比较方便了。
如果对你有帮助请点赞收藏支持一下,感谢 ~