使用 Shell 工具将网络电台转换为播客

作者:Phil Salkie

这一切都始于我想收听 WBAI 电台的“狼时”节目——这是一个由吉姆·弗罗因德主持的精彩科幻广播节目,节目内容包括朗读、音乐、作者访谈以及精彩的“我当时在场……”之类的故事。不幸的是,WBAI 从纽约长岛播出,离我太远,接收效果不好。此外,该节目在星期六早上美国东部时间 5 点到 7 点播出——对于我们这些上班族来说,这不是一个非常友好的时间段。

后来,我发现 WBAI 网站上有流媒体 MP3 音频,这解决了接收问题。剩下的就是凌晨时分的问题——我通常在这个时候进入深度睡眠。而且,无论是否是科幻迷,我都不可能很快赶上吉姆的直播。

搜索

我需要的是一台用于网络电台的录像机。具体来说,我想要捕获流媒体并将其保存到磁盘上,作为 MP3 文件,并以节目名称和日期命名。我需要添加正确的 MP3 ID 标签,以便我可以将其加载到我的 Neuros 音频播放器中,以便于收听。如果我可以让 RSS 兼容的软件知道我已经捕获了这些文件,那也将是非常好的。这样,它们就会出现在 Firefox 实时书签中,或者可以在充电期间传输到 iPod。最终效果将是创建一个自动播客——一个动态更新的 RSS 源,其中包含指向已保存录音的链接——通过定期从互联网媒体流中截取单个节目。

因此,我开始在 Google 上搜索“mp3 stream recording”和“tivo radio”等等。我找到了许多软件包和网站,但似乎都不太合适。然后,我听到了来自我过去的声音——埃里克·S·雷蒙德的《无根之根》中伟大的傅大师的声音,他对我说:“一行 shell 脚本比一万行 C 语言更具 UNIX 性质。” 因此,我想知道我是否可以使用系统上已有的工具,通过一个简单的 shell 脚本连接起来,来完成这项任务。

收集工具

你看,我已经可以使用优秀的 MPlayer 媒体播放器软件播放流媒体了。由于专利问题,Fedora Core 3 没有附带 MP3 支持,因此我之前从源代码下载并构建了 MPlayer,作为启用 MP3 系统的过程的一部分。顺便说一句,MPlayer 广泛使用了每种不同 CPU 类型的特定硬件功能,因此,如果在计划使用的机器上从源代码构建它,它作为视频播放器的性能会更好。命令

mplayer -cache 128 \
-playlist http://www.2600.com/wbai/wbai.m3u

出色地服务于通过我的扬声器播放流媒体。剩下的就是说服 MPlayer 改为保存到磁盘。MPlayer 手册页显示了 -dumpaudio 和 -dumpfile <filename>,它们协同工作以读取流媒体并静默地将其保存到磁盘,永远永远。没有超时,因此它会捕获直到您杀死 MPlayer 进程。因此,我编写了这个脚本

#!/bin/bash

mplayer -cache 128 \
  -playlist http://www.2600.com/wbai/wbai.m3u \
  -dumpaudio -dumpfile test.mp3 &
# the & sets the job running in the background

sleep 30s

kill $! # kill the most recently backgrounded job

它很好地将一个 30 秒左右的 MP3 文件捕获到磁盘。在mplayer上面的命令末尾的 & 字符至关重要;它使 MPlayer 作为后台任务运行,因此 shell 脚本可以继续执行下一个命令,即定时休眠。一旦休眠完成,脚本就会终止最后一个后台任务,从而结束录制。您可能需要调整 -cache 值以适应您的互联网连接,甚至替换为 -nocache。

现在第一部分完成了,我开始进行第二部分——插入 MP3 ID 标签。回到 Google,我找到了 id3v2,这是一个方便的小型命令行程序,可以向 MP3 文件添加标签——而且它已经在 Fedora Core 发行版中!令人惊叹的是,您的硬盘驱动器上潜伏着这些东西。

创建播客

我现在已经有了捕获和标记我最喜欢的节目的工具。有了这些工具,我剩下的任务就是想出一些方法来从文件堆栈中制作联合提要。事实证明,RSS 提要是简单的可扩展标记语言 (XML) 文件,其中包含指向我们想要馈送的实际数据的链接,无论是网页还是本例中的 MP3 文件。

再次快速浏览 Google,我找到了 Perl 的 XML::RSS 模块。它是一套完整的工具,既可以创建新的 RSS 文件,也可以向现有文件添加条目。在这一点上,我以为我快完成了,并整理了一个不错的代码示例,但几乎没有奏效。然而,按照真正的项目时间表传统,项目的最后 5% 最终需要花费总时间的 95%。

RSS:XML 罐中的蠕虫

一旦我编写了一个脚本,它可以完成我想要的所有事情,我就将其与本文的第一个版本一起发送给了LJLJ 主编唐·马蒂指出,我遗漏了一个关键组件:我的程序正在生成 RSS 1.0 版本提要,但所有支持播客的程序都在寻找 2.0 版本提要——特别是名为 enclosure 的 XML 标签。当然,我认为这对我的软件来说只是一个微不足道的更改,只需切换版本并添加 enclosure 标签即可。然而,我很快了解到 XML::RSS Perl 模块可以写入 RSS 2.0,但无法读取它。随之而来的是几个不眠之夜,直到我确定有可用的 Perl 工具可以读取 RSS 2.0,但无法写入它。因此,是时候添加一些粘合剂了。

我首先向我的系统添加了两个 Perl 模块——您可以使用以下命令(以 root 身份)安装它们

perl -e "install XML::RSS,XML::Simple" -MCPAN

您可能会对它提出的任何问题回答默认值感到满意。如果您尚未使用综合 Perl 存档网络 (CPAN),它会询问很多设置问题,例如选择几个离您较近的镜像站点。否则,它只会询问一两个依赖项;说是。

安装了这两个模块及其所需的依赖项后,您需要创建一个新的 XML 文件,其中包含有关您要捕获的节目的信息。XML 的优点在于您可以使用任何文本编辑器来制作人类和机器都可读取的文件,从而轻松创建、查看、测试和修改 RSS 提要文件。让我们从这个骨架开始,其中包含一个基本的标题部分

<?xml version="1.0" encoding="UTF-8"?>

<rss version="2.0">

<channel>
<title>Hour of the Wolf</title>
<link>http://www.hourwolf.com</link>
<description>Science Fiction Talk Radio
  with Jim Freund</description>
<generator>WBAI Stream Capture
  using Linux shell tools</generator>
</channel>
</rss>

如果您以前从未玩过 XML,那么现在是时候开始入门了。快速浏览一下文件,可以看到数据项被类似 HTML 的标签包围,其中每个 <something> 标签都有一个对应的 </something> 来关闭 something 部分。不过,当我们稍后添加替代语法时,这会变得更加令人困惑,替代语法看起来像 <tagname a=“A” b=“B” />。

应用粘合剂

一旦我收集了所有需要的工具,我就添加了一些 shell 魔力,得到了这个简单的脚本

#!/bin/bash
# catchthewolf - capture "Hour of the Wolf"

# For capturing the stream
DATE=`date +%F`  # Save the date as YYYY-MM-DD
YEAR=`date +%Y` # Save just the year as YYYY
FILE=/home/phil/wolf.$DATE.mp3 # Where to save it
STREAM=http://www.2600.com/wbai/wbai.m3u
DURATION=2.1h # enough to catch the show, plus a bit
#DURATION=30s # a quick run, just for testing

# For the RSS syndication
XML="/home/phil/wolfrss.xml" # file for the RSS feed
ITEMS=15  # Maximum items in RSS list
XTITLE="Hour of the Wolf - $DATE Broadcast"
XDATE=`date -R` # Date in RFC 822 format for RSS
i=\$i;o=\$o;m=\$m # replace "$" in the perl script

# For the id3v2 Tags
AUTHOR="Jim Freund"
ALBUM="WBAI Stream Rip"
TITLE="Hour of the Wolf - $DATE"

# Use mplayer to capture the stream
# at $STREAM to the file $FILE
/usr/local/bin/mplayer -really-quiet -cache 128 \
    -dumpfile $FILE -dumpaudio -playlist $STREAM &
# the & turns the capture into a background job

sleep $DURATION  # wait for the show to be over

kill $! # end the stream capture

# Tag the resulting captured .mp3
id3v2 -a "$AUTHOR" -A "$ALBUM" \
    -t "$TITLE" -y $YEAR -T 1/1 -g 255 \
    --TCON "Radio" $FILE

# Add a new entry in the rss file,
# keep the file to a max of $ITEMS entries,
# and change the file's date to right now.
/usr/bin/perl -e "use XML::RSS; use XML::Simple; \
    $i=XMLin('$XML');$o=$i;bless $o,XML::RSS; \
    $m=$i->{channel}{item};if((ref $m)ne ARRAY) \
    {$o->add_item(%$m);} else \
    {foreach $m (@{$m}) {$o->add_item(%$m);}} \
    $o->channel(lastBuildDate=>'$XDATE', \
    pubDate=>'$XDATE'); \
    $o->add_item(title=>'$XTITLE', \
    link=>$o->{'channel'}{'link'}, \
    pubDate=>'$XDATE', \
    enclosure=>{url=>'file://$FILE', \
    length=>(stat('$FILE'))[7], \
    type=>'audio/mpeg'}, mode=>'insert'); \
    pop(@{$o->{'items'}}) \
    while (@{$o->{'items'}}>$ITEMS); \
    $o->{encoding}='UTF-8'; $o->save('$XML');"

echo "Caught the wolf."

不过,这看起来不太简单。让我们稍微剖析一下这个脚本,看看它是如何工作的。请注意日期命令周围的反引号 (`)。它们获取``标记中包含的任何内容并将其作为命令运行,然后将整个 `whatevercommand` 替换为该命令的输出。如果我只需要日期一次,我可以写

FILE=wolf.`/bin/date +%F`.mp3

甚至

/usr/local/bin/mplayer -dumpaudio \
  -dumpfile "wolf.`/bin/date +%F`.mp3" \

但是因为我想要用于文件名、标签和 RSS 提要的日期,所以我将其存储在 $DATE shell 变量中。这也使得更改脚本变得更加容易。我现在有几个捕获流媒体的脚本,唯一需要更改的是顶部的变量赋值。

反引号是 shell 的工具之一,它允许我们将简单的命令合并为强大的程序集。您可以使用 echo 命令进行更多尝试。例如,尝试

echo "wolf.`date +%F`.mp3"

看看在上次调用 MPlayer 时文件名会是什么。

我们使用 +%F 格式化选项来表示日期,因为默认日期字符串充满了空格。此外,我的美国语言环境的日期字符串中包含 / 字符——这不是尝试将其放入文件名中的最佳选择。此外,yyyy-mm-dd 格式意味着当您列出目录时,文件可以按日期很好地排序。RSS 提要需要 RFC 822 格式的日期,因此我们最终总共调用了三次 /bin/date。

另请注意,我给出了某些可执行命令的完整路径。我这样做是为了当脚本作为定时任务运行时,它不会具有我的个人 shell 的路径设置。如果您不确定文件位于何处,请使用 which 查找它

[phil@asylumhouse]$ which date
/bin/date

您可以放心地省略 /bin 和 /usr/bin,但任何其他路径都应明确指定,对于在多个位置以不同版本存在的任何可执行文件也是如此。

调用 id3v2 会将文件标记为曲目 1/1,并带有正确的作者、专辑、标题和年份条目。预定义的流派编号 255 表示其他。--TCON 条目用 Radio 替换任何理解版本 2 MP3 标签的软件上的预定义流派之一。

最后,末尾的单行 Perl 脚本是此脚本的压缩版本

#!/usr/bin/perl

use XML::RSS; use XML::Simple;

$in=XMLin('/home/phil/wolfrss.xml');
$out=$in; # copy the parsed RSS file's tree
bless $out, XML::RSS; # make the copy an XML::RSS

# blessing doesn't copy the items.  Drat!
$item = $in->{channel}{item};
if ((ref $item) ne ARRAY) { # only one item in feed
  $out->add_item(%$item);
} else { # a list of items - foreach the list
  foreach $item (@{$item}) {
    $out->add_item(%$item);
  }
}

# Encoding doesn't transfer either.
$out->{encoding}='UTF-8';

# Date the file so client software knows it changed
$date = `date -R`;
$out->channel( lastBuildDate=>'$date',
    pubDate=>'$date');

# Add our newest captured file
$file = "/home/phil/wolfcaught.mp3";
$out->add_item( title => "Hour of the Wolf",
    link => $out->{'channel'}{'link'},
    pubDate => '$date',
    enclosure => { url=>"file://$file",
      length => (stat($file))[7],
      type => 'audio/mpeg'
    },
    mode => 'insert');

# Don't have more than 15 items in the podcast
while (@{$out->{'items'}} > 15) {
	pop(@{$out->{'items'}};
}

# Write out the finished file
$out->save('/home/phil/wolfrss.xml');"

在这里,我使用 XML::Simple 读取和解析现有的 .RSS 文件,并使用 XML::RSS 添加我们的新项目并写入修改后的版本。bless 函数告诉 Perl XML::Simple 对象 $out 现在应被视为 XML::RSS 对象。这之所以有用,唯一的原因是这两个模块在内部使用几乎相同的变量名,这些变量名源自传入 RSS 文件的标签名称。

此 bless 函数复制 RSS 文件标头中的几乎所有内容,但不会引入 item 或 encoding 标签。因此,我随后在 foreach 循环中复制了每个项目,添加了今天的日期作为构建和发布日期,并将刚刚捕获的文件添加为新项目。此项目具有从标头复制的网页链接、今天的日期作为发布日期以及最重要的 enclosure 标签。enclosure 具有 URL,在本例中为 file:// 引用,因为我们正在本地文件系统上执行所有操作。它还具有文件长度和 MIME 类型 audio/mpeg。

Shell 变量替换所有带引号的字符串,超级隐蔽的 shell 变量 $i、$o 和 $m 被替换为 \$i、\$o 和 \$m。换句话说,在 Perl 脚本中您看到的任何 $i,Perl 解释器实际上都会获得 Perl 变量名 $i。如果没有这种替换,shell 会将每个 $i 替换为空字符串,或者更糟糕的是,shell 变量 i 在脚本执行之前恰好持有的任何内容。对实际 MP3 文件的引用是 URL,file:///home/phil/wolf.2005-03-19.mp3,而不仅仅是文件名。当我们将 RSS 提要文件输入 Firefox 或提要聚合器程序时,我们也使用 URL 表示法来引用它,file:///home/phil/wolfrss.xml。

为什么不直接用 Perl 完成?

我从另一种脚本语言中调用一种脚本语言似乎很奇怪。关键是我正在使用每种语言来做它最擅长的事情。Bash 旨在执行命令,并且很容易启动后台进程,找出其进程 ID 并再次杀死它。另一方面,尝试使用 Bash 中更基本的字符串处理工具(例如 sed 和 grep)添加 XML 条目,嗯,这正是促使拉里·沃尔编写 Perl 的那种事情。

现在我们有了一个脚本,我们将文件设为可执行文件并运行它

chmod +x catchthewolf
./catchthewolf

这会生成一个正确标记的 MP3 文件和 wolfrss.xml RSS 提要中的新条目。在测试时,您可以取消注释 30 秒的测试行,以确保一切正常运行,但在尝试捕获节目之前,请务必将其注释掉。

现在剩下要做的就是让我们的计算机在星期六凌晨 5 点运行此程序。这可以通过使用系统的 cron 实用程序来完成——调用crontab -e——并添加如下条目

MAILTO=phil # Testing: mail script output to me

# Catch hour of the wolf 5AM Saturdays
59 4 * * sat /home/phil/catchthewolf

crontab 的编辑器很可能设置为 vi 风格的命令,因此您必须使用 i 开始键入,并使用 <Esc>:wq 保存并退出。完成后,您应该看到此消息

crontab: installing new crontab

这表示您已全部设置完毕。查看man 5 crontab以获取有关如何使作业每天重复、每月重复一次或任何其他时间的更多信息。您还需要确保您的用户名位于文件 /etc/cron.allow 中——该文件列出了谁可以在系统调度程序上运行作业。如果您在远程系统上运行,请与管理员确认您是否被允许运行 cron 作业。

要查看生成的播客,请将您的 RSS 感知软件指向脚本创建的 XML 文件。在 Firefox 中,使用书签→管理书签→添加实时书签,并记住输入以 file:// 开头的 URL,而不是文件名本身。

总结

通过使用硬盘驱动器上已有的两个程序,下载两个 Perl 模块并编写几行 shell 脚本,我们组装了一个自制的 Webcast 录制系统,该系统可以保存我们最喜欢的节目,以便我们随时收听。它还可以通过在 Firefox 中弹出实时书签并自动将录音传输到我们的 MP3 播放器来告知我们它已完成的工作。用于捕获其他网络电台节目的一些脚本将在Linux Journal FTP 站点上提供(请参阅在线资源)。现在我只需要记住删除旧文件,以免我的硬盘驱动器被剩余的 Webcast 填满。

感谢 Anne Troop、Jen Hamilton 和 Chris Riley 多年来提供的许多 shell 脚本提示;感谢 Anne 的朋友 Janeen Pisciotta 首先为我们找到了“狼时”节目;并感谢LJ 主编唐·马蒂的酷播客想法。

流媒体格式

当流媒体电台刚出现时,它通常以专有数据格式传输,这使得 Linux 用户难以收听。现在大多数流媒体都是 MP3,但仍然可能存在您想要捕获的不同格式的内容,例如 BBC Radio 的 RealPlayer 流媒体——有关链接,请参阅在线资源。假设 MPlayer 可以处理它,我们可以稍微调整一下我们的流程。告诉 MPlayer 以 WAV 文件的形式将音频数据写入磁盘,然后使用 lame for MP3 或 oggenc for ogg 文件对其进行编码。但请注意,由于专利问题,Fedora 未包含 lame。

音频捕获命令将如下所示

# Use mplayer to capture the stream
# at $STREAM to the file $FILE
/usr/local/bin/mplayer -really-quiet -cache 500 \
    -ao pcm:file="$FILE.wav" -playlist $STREAM &
# the & turns the capture into a background job

sleep $DURATION  # wait for the show to be over

kill $! # kill the stream capture

# Encode to .ogg, quality 2, and tag the file
oggenc -q 2  -t $TITLE -a $AUTHOR -l $ALBUM \
  -n "1/1" -G "Radio" -R 16000 -o $FILE $FILE.wav

rm $FILE.wav # Remove the raw audio data file

然后是原始的 Perl 脚本调用。这里不需要使用 id3v2,因为 lame 和 oggenc 编码器都在编码过程中插入标签。我们最终得到与直接捕获 MP3 流媒体相同的结果。但是由于中间 WAV 文件的大小很大,我们在实际捕获过程中需要更多的磁盘空间。可选的 -R 16000 指定捕获的 WAV 文件的采样率——只有当 MPlayer 未能正确检测到传入音频流的速度并且您捕获的 MP3 听起来像鲸歌或花栗鼠时才需要这样做。您可能想要注释掉 rm 命令,直到您确定编码工作正常,然后再手动删除 WAV 文件。

这东西叫 RSS 是什么?

RSS 代表富站点摘要。

RSS 代表 RTF 站点摘要。

RSS 代表真正简单的联合。

关于 RSS 的其他一切都像它的首字母缩写词一样令人困惑。这个想法最初的想法是能够从网站上读取标题,而无需下载整个首页。RSS 在可扩展标记语言 (XML) 中实现,这使得人类和计算机都可以轻松读取和写入它。这意味着 RSS 文件的格式是标准化的——不幸的是,内容不是。至少有四个版本的 RSS 在流传——0.9、0.91、1.0 和 2.0——它们具有相似之处、差异以及大量互操作性问题。基本的 RSS 文件包含标题、发布日期和一组项目。每个项目都有自己的标题、日期和指向包含文章内容的文件的链接。版本之间的差异意味着任何想要读取或写入这些文件的软件都必须经过专门编程才能理解每个版本——没有足够的向后兼容性来让事情简单地工作。

即使是版本编号也很奇怪——2.0 版本是从 0.91 版本派生出来的,而不是 1.0 版本。1.0 版本是功能最丰富且可扩展的版本,它通过链接到特殊的机器可读网页来支持标签名称的动态定义。2.0 版本扩展了原始概念,允许更复杂的摘要,包括图像和音乐,而不仅仅是文本行;它通过使用 enclosure 标签来实现这一点。Enclosure 的工作方式类似于电子邮件消息的附件。当支持 RSS 的程序下载站点摘要时,它会注意到附件并也下载它们。这扩展了摘要的概念,使其成为内容列表,加上内容本身——与 RSS 的原始概念相去甚远,但这正成为其当今最大的用途。

本文资源: /article/8402

Phil Salkie 是一位工业控制专家,他从小就喜欢科幻小说和广播剧。他从 2.0.12 左右开始成为 Linux 狂热爱好者,并且拥有最棒、最宽容的家庭——通过电子邮件与他联系:phil@asylumhouse.org

加载 Disqus 评论