[c#]喜马拉雅FM音频批量下载器开发手记

[c#]Winform开发 专栏收录该内容
4 篇文章 0 订阅

[c#]喜马拉雅FM音频批量下载器开发手记

0、序言

第一次写C博,也不知道写些啥,正好近期做了一个用于喜马拉雅FM音频的批量下载器,虽然网络上之前就有,但是功能上来讲还没有满足自己需求,于是就照猫画虎弄了一个,比较懒,模仿了原作者的程序布局,希望看到了原谅则个!

1、程序原理

一直比较喜欢在喜马拉雅上下载有声小说,之前用移动端下载,奈何平时跑步经常只带一个头戴的mp3,所以常常需要把移动端的音频文件导到mp3里,改名什么的非常麻烦,于是萌生了去网站上下载的念头,进入网站,发现除了可以在线收听之外,还是需要在移动端下,虽然可以用资源嗅探挨个下载,费时费力,于是就想着既然浏览器可以解析到下载地址,那我必然可以分析出来,于是打开网站,进入一个专辑列表,点播放,进入开发者工具查看源代码,看到了以下HTML代码:

<div class="detailContent" sound_id="30496241">

注意到sound_id这个属性,英文字面上很好理解,声音编号,转到开发者工具(我使用的是遨游浏览器),点击网络(如图):
遨游浏览器开发者工具的'网络'位置
然后刷新页面,点击播放得到页面元素,并使用搜索过滤功能查找符合sound_id的页面元素:
搜索过滤
我搜索时找到了3个,看到json文件(地址组成为http://www.ximalaya.com/tracks/*.json,*的内容就是sound_id),毫不犹豫点进去:
找到的页面元素
可以看到截图所示的内容:
json文件内容
id、album_title、title、play_path、duration这些属性基本上一看就知道是些什么了。但是这只是一个音频的内容,难道我们要这样一个一个的找么,显然这不符合要求,于是,继续看网页源代码,发现如下代码:

<div class="personal_body" sound_ids="30496241,31457522,31478901,9654201,29151468,28253955,27433095,27015072,30256103,21214704,11213572,5470344,11408078,16069093,23015547,23553784,22941804,23242077,24275776,25617727,25092603,18592189,26049529,25702509,15147957,18153439,24583960,2832441,19796676,6115826,6626242,6366263">

看到sound_ids这个属性基本上万事就妥了,明显是一个id的序列。
于是就有了基本思路:1、获取专辑网址(这个应该由用户输入)–>2、获取专辑网页元素中的sound_ids–>3、从音频编号序列逐个获取音频编号–>4、使用编号按http://www.ximalaya.com/tracks/*.json规则获取json文件–>5、反序列化json文件–>6、获取id、album_title、title、play_path、duration这些属性并记录–>7、完成批量下载。

2、程序布局

程序的基本布局如图所示:
程序基本布局
程序由1个ToolStrip、1个TreeView、4个GroupBox、1个TextBox、3个Button、1个Label、3个CheckBox、1个DataGridView、1个FolderBrowserDialog、1个saveFileDialog、1个openFileDialog、1个timer组成。Toolstrip主要用实现常用功能按钮,TreeView用于显示专辑列表,TextBox支持多行,主要用于用户录入专辑网址,Label用于程序运行状态显示,DataGridView则用于显示下载列表,其他的控件功能在图上都可以看到。

3、程序实现

3.1获取sound_ids

需要获取sound_ids首先需要获取专辑网页的源代码,实现代码如下:

// 获取目标网页源代码
        private static string GetWebClient(string url)
        {
            try
            {
                string strHTML = "";
                WebClient myWebClient = new WebClient();
                Stream myStream = myWebClient.OpenRead(url);
                StreamReader sr = new StreamReader(myStream, System.Text.Encoding.GetEncoding("utf-8"));
                strHTML = sr.ReadToEnd();
                myStream.Close();
                return strHTML;
            }
            catch
            {
                return "网络状况不佳!";
            }
        }

然后分析网页源代码获取sound_ids:

// 获取Html字符串中指定标签的指定属性的值 
        private static List<string> GetHtmlAttr(string html, string tag, string attr)
        {
            Regex re = new Regex(@"(<" + tag + @"[\w\W].+?>)");
            MatchCollection imgreg = re.Matches(html);
            List<string> m_Attributes = new List<string>();
            Regex attrReg = new Regex(@"([a-zA-Z1-9_-]+)\s*=\s*(\x27|\x22)([^\x27\x22]*)(\x27|\x22)", RegexOptions.IgnoreCase);
            for (int i = 0; i < imgreg.Count; i++)
            {
                MatchCollection matchs = attrReg.Matches(imgreg[i].ToString());
                for (int j = 0; j < matchs.Count; j++)
                {
                    GroupCollection groups = matchs[j].Groups;
                    if (attr.ToUpper() == groups[1].Value.ToUpper())
                    {
                        m_Attributes.Add(groups[3].Value);
                        break;
                    }
                }
            }
            return m_Attributes;
        }

将两个方法整合方便使用:

// 获取sound_ids
        public static string GetSoundIDs(string url)
        {
            List<string> strs = GetHtmlAttr(GetWebClient(url), "div", "sound_ids");
            if (strs.Count > 0)
                return strs[0];
            else
                return "当前网址不正确";
        }

在主代码中调用,ResourceBox即为用户输入控件:

// 获取Html字符串中指定标签的指定属性的值 
sound_ids = CommonMethds.GetSoundIDs(ResourceBox.Lines[i]) + ',';

3.2获取json文件并反序列化

下载音频编号对应的json文件并反序列化:

// 获取音频信息方法
        public static AudioInfo GetAudioInfo(string sound_id)
        {
            AudioInfo audio = new AudioInfo();
            try
            {
                string url = @"http://www.ximalaya.com/tracks/" + sound_id + ".json";
                string strHTML = "";
                WebClient myWebClient = new WebClient();
                Stream myStream = myWebClient.OpenRead(url);
                StreamReader sr = new StreamReader(myStream, System.Text.Encoding.GetEncoding("utf-8"));
                strHTML = sr.ReadToEnd();
                myStream.Close();
                MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(strHTML));
                DataContractJsonSerializer deseralizer = new DataContractJsonSerializer(typeof(AudioInfo));
                audio = (AudioInfo)deseralizer.ReadObject(ms);// //反序列化ReadObject
                return audio;
            }
            catch
            {
                return audio;
            }
        }

里面用到了一个AudioInfo的自定义类,主要用于反序列化json文件,定义如下:

namespace XimalayaAudioDownloader
{
    //定义歌曲文件信息
    [DataContract]
    class AudioInfo
    {
        public AudioInfo()
        {
            album_id = "null";
            album_title = "null";
            category_name = "null";
            category_title = "null";
            comments_count = "null";
            cover_url = "null";
            cover_url_142 = "null";
            discounted_price = "null";
            duration = "null";
            favorites_count = "null";
            formatted_created_at = "null";
            have_more_intro = "null";
            id = "null";
            intro = "null";
            is_favorited = "null";
            is_free = "null";
            is_paid = "null";
            nickname = "null";
            play_count = "null";
            play_path = "null";
            play_path_32 = "null";
            play_path_64 = "null";
            played_secs = "null";
            price = "null";
            shares_count = "null";
            time_until_now = "null";
            title = "null";
            uid = "null";
            upload_id = "null";
            waveform = "null";
        }

        [DataMember]
        public string album_id { get; set; }
        [DataMember]
        public string album_title { get; set; }
        [DataMember]
        public string category_name { get; set; }
        [DataMember]
        public string category_title { get; set; }
        [DataMember]
        public string comments_count { get; set; }
        [DataMember]
        public string cover_url { get; set; }
        [DataMember]
        public string cover_url_142 { get; set; }
        [DataMember]
        public string discounted_price { get; set; }
        [DataMember]
        public string duration { get; set; }
        [DataMember]
        public string favorites_count { get; set; }
        [DataMember]
        public string formatted_created_at { get; set; }
        [DataMember]
        public string have_more_intro { get; set; }
        [DataMember]
        public string id { get; set; }
        [DataMember]
        public string intro { get; set; }
        [DataMember]
        public string is_favorited { get; set; }
        [DataMember]
        public string is_free { get; set; }
        [DataMember]
        public string is_paid { get; set; }
        [DataMember]
        public string nickname { get; set; }
        [DataMember]
        public string play_count { get; set; }
        [DataMember]
        public string play_path { get; set; }
        [DataMember]
        public string play_path_32 { get; set; }
        [DataMember]
        public string play_path_64 { get; set; }
        [DataMember]
        public string played_secs { get; set; }
        [DataMember]
        public string price { get; set; }
        [DataMember]
        public string shares_count { get; set; }
        [DataMember]
        public string time_until_now { get; set; }
        [DataMember]
        public string title { get; set; }
        [DataMember]
        public string uid { get; set; }
        [DataMember]
        public string upload_id { get; set; }
        [DataMember]
        public string waveform { get; set; }
    }
}

3.3将sound_ids中的所有音频加入下载列表:

通过不断调用GetAudioInfo函数来获取专辑信息并加入DataGridView(DownloadList):

private void getSound()
        {
            classCount = 0;
            soundsCount = 0;
            if (ResourceBox.Lines.Length > 0)
            {
                string sound_ids = string.Empty;
                State.Text = "正在分析网址,请稍等……";
                for (int i = 0; i < ResourceBox.Lines.Length; i++)
                {
                    if (ResourceBox.Lines[i].IndexOf("ximalaya") > -1)
                    {
                        sound_ids = CommonMethds.GetSoundIDs(ResourceBox.Lines[i]) + ',';
                        if (sound_ids != "当前网址不正确,")
                        {
                            int count = CommonMethds.SoundsCount(sound_ids);
                            classCount++;
                            soundsCount += count;

                            for (int j = 1; j <= count; j++)
                            {
                                AudioInfo audio = CommonMethds.GetAudioInfo(CommonMethds.getSoundIDByIndex(sound_ids, j));
                                if (audio.album_title != "null")
                                {
                                    int index = DownloadList.Rows.Add();
                                    DownloadList.Rows[index].Cells[0].Value = true;
                                    DownloadList.Rows[index].Cells[1].Value = audio.id;
                                    DownloadList.Rows[index].Cells[2].Value = audio.album_title;
                                    DownloadList.Rows[index].Cells[3].Value = audio.title;
                                    DownloadList.Rows[index].Cells[4].Value = audio.play_path;
                                    DownloadList.Rows[index].Cells[5].Value = Int32.Parse(audio.duration) / 60 + "分" + Int32.Parse(audio.duration) % 60 + "秒";
                                    DownloadList.Rows[index].Cells[6].Value = "";
                                    DownloadList.Rows[index].Cells[7].Value = "";
                                    DownloadList.Rows[index].Cells[8].Value = "0%";
                                    State.Text = "正在分析第" + (i + 1) + "张专辑下载地址,已完成" + j + "/" + count + ",请稍等……";
                                }
                                else
                                {
                                    State.Text = "当前网络状况欠佳,请稍后重试!";
                                    break;
                                }
                            }
                        }
                        else
                            State.Text = "请查看网络是否正常,或输入网址是否正确!";
                    }
                }
                StartBtn.Enabled = true;
                SirialTree.ExpandAll();
                if (classCount != 0 && soundsCount != 0)
                    State.Text = "成功添加" + classCount + "张专辑," + soundsCount + "个音频文件。";
                else
                    State.Text = "请查看网络是否正常,或输入网址是否正确!";
                UpdateTreeView();
                if (ResourceBox.Lines.Length > classCount && classCount > 0)
                    MessageBox.Show("分析完毕,但是部分网址不正确,请注意!");
            }
            else
                MessageBox.Show("请输入正确网址!");
        }

3.4批量下载

下载时用到一个方法,代码如下:

// 文件下载方法
        static int totalTime = 0;
        static int SPD_INTERVAL_SEC = 1; //时间常量
        public static void DownloadFile(string URL, string filename, DataGridViewRow row, Label state)
        {
            System.Threading.Timer FileTm = new System.Threading.Timer(SpeedTimer, null, 0, SPD_INTERVAL_SEC * 1000);//使用回调函数,每SPD_INTERVAL_SEC秒执行一次
            try
            {
                if (File.Exists(filename))
                    File.Delete(filename);
                HttpWebRequest Myrq = (HttpWebRequest)HttpWebRequest.Create(URL);
                Myrq.Timeout = 10000;
                HttpWebResponse myrp = (HttpWebResponse)Myrq.GetResponse();
                long totalBytes = myrp.ContentLength;
                row.Cells[6].Value = ((float)totalBytes / 1048576).ToString("f2") + "MB";
                Stream st = myrp.GetResponseStream();
                Stream so = new System.IO.FileStream(filename, FileMode.Create);
                long totalDownloadedByte = 0;
                byte[] by = new byte[1024];
                int osize = st.Read(by, 0, (int)by.Length);
                totalTime = 1;
                while (osize > 0)
                {
                    totalDownloadedByte = osize + totalDownloadedByte;
                    so.Write(by, 0, osize);
                    osize = st.Read(by, 0, (int)by.Length);
                    Application.DoEvents();
                    row.Cells[8].Value = ((int)totalDownloadedByte / (totalBytes / 100)).ToString() + "%";
                    row.Cells[7].Value = ((double)totalDownloadedByte / 1024 / totalTime).ToString("f2") + "KB/S";
                    Application.DoEvents(); //必须加注这句代码,否则将因为循环执行太快而来不及显示信息
                }
                row.Cells[0].Value = false;
                row.Cells[8].Value = "100%";
                so.Close();
                st.Close();
                FileTm.Dispose();
            }
            catch(TimeoutException)
            {
                FileTm.Dispose();
                state.Text = "网络连接超时,请检查网络连接后重试!(Error3)";
            }
            catch (Exception)
            {
                FileTm.Dispose();
                state.Text = "网络连接超时,请检查网络连接后重试!(Error3)";
            }
        }
        private static void SpeedTimer(object state)
        {
            totalTime++;
        }

主代码中调用该方法进行下载:

 private void StartBtn_Click(object sender, EventArgs e)
        {
            isStop = false;
            StartBtn.Enabled = false;
            StopBtn.Enabled = true;
            State.Text = "开始下载!";
            userStop = false;

            Thread GetSoundIdsWork = new Thread(new ThreadStart(download));
            GetSoundIdsWork.IsBackground = true;
            GetSoundIdsWork.Start();
        }

        void download()
        {
            foreach (DataGridViewRow r in DownloadList.Rows)
            {
                if (userStop)
                {
                    StartBtn.Enabled = true;
                    State.Text = "下载已停止!";
                    break;
                }
                if (!Directory.Exists(storePath + @"\" + r.Cells[2].Value.ToString() + @"\"))
                    Directory.CreateDirectory(storePath + @"\" + r.Cells[2].Value.ToString() + @"\");
                string filename = storePath + @"\" + r.Cells[2].Value.ToString() + @"\" + r.Cells[3].Value.ToString() + ".mp3";
                if ((bool)r.Cells[0].Value == true && (string)r.Cells[8].Value != "100%" && State.Text != "网络连接超时,请稍后重试!(Error3)")
                {
                    State.Text = "当前下载(" +r.Index + "/" + DownloadList.Rows.Count + "):" + r.Cells[3].Value.ToString() + ".mp3";
                    CommonMethds.DownloadFile(r.Cells[4].Value.ToString(), filename, r, State);
                }
                if (State.Text == "网络连接超时,请检查网络连接后重试!(Error3)")
                {
                    StartBtn.Enabled = true;
                    StopBtn.Enabled = false;
                    isStop = true;
                    break;
                }
            }

            StopBtn.Enabled = false;
            if (State.Text != "网络连接超时,请检查网络连接后重试!(Error3)")
                State.Text = "下载完成!";
            // 勾选下载完毕自动关机执行
            if (!isStop && AutoPowerDown.Checked)
            {
                ProcessStartInfo ps = new ProcessStartInfo();
                ps.UseShellExecute = false;
                ps.FileName = "shutdown.exe";
                ps.Arguments = "-s -t 30";
                ps.CreateNoWindow = true;
                ps.RedirectStandardError = true;
                ps.RedirectStandardInput = true;
                ps.RedirectStandardOutput = true;
                Process.Start(ps);
            }
        }

最终效果如图:
程序界面

4、资源下载

菜鸟一枚,欢迎拍砖……
放出资源供有缘人下载:
程序:http://download.csdn.net/detail/mnikkqqw/9784335
源代码&工程文件:http://download.csdn.net/detail/mnikkqqw/9784355

类似程序之前也有,功能大致相同,如有冒犯,多多见谅!

  • 0
    点赞
  • 3
    评论
  • 3
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页

打赏作者

LevMe

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值