手撸一个今日头条视频下载器

阅读 919
收藏 47
2017-02-16
原文链接:www.weyye.me

前言

今日头条是我最喜欢的app之一,当然喜欢并不是因为内容精彩,而是逗比的评论,而且看视频的没有广告,我这个人喜欢收藏,尤其是小视频(手动滑稽),可是却没有下载的按钮,之后在仿今日头条项目里也需要用到视频,进入网页右键另存为也比较麻烦,作为程序猿,这可不是我们的办事风格。于是动手撸了一个视频下载器,喜欢的记得给个Star,当作是给我的鼓励和动力吧。

成果图

源码下载

github.com/yewei02538/…

分析视频地址

详细的视频地址分析请看Python脚本下载今日头条视频(附加Android版本辅助下载器)

这里我摘出来视频的获取流程

1、将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密。
2、将上面得到的加密值拼接到上面的链接中即可,最终的链接形式是:
i.snssdk.com/video/urls/…
3、访问这个链接得到一个json数据,需要解析video_list数组中的main_url值,然后用base64解码得到最终的原始视频链接。
看到上面的步骤并不复杂,但是在操作过程中还是有些地方需要注意的,主要是上面的那个随机数和crc32加密逻辑,videoid可以从视频网页的html源代码里面获取

用正则表达式取出videoid即可

看流程分析,我们需要视频所在的网页地址,在get请求访问成功的回调里面进行正则表达式匹配,然后将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密,然后拼在一起,再get请求,再在请求成功的回调里解析json获取base64编码的地址,然后进行解码,最后获得视频源地址。这一些列操作都是需要请求成功才可以执行的,可以想到嵌套里面再嵌套,那种代码逻辑着实让人看着蛋疼,所以这个时候RxJava的优势就出来了,完美的解决了这个问题,如果还不是很懂RxJava的朋友可以去看下这篇文章 给 Android 开发者的 RxJava 详解

撸代码

首先得先获取到播放视频的网页地址,这里我使用的是分享来接收,今日头条有分享功能,分享的内容里面肯定会有地址,所以我们来配置下接收分享


            
                
                
            
            
                
                
                
            
        

ok,这样就具备的接收的功能,然后在MainActivity里面写上接受的逻辑

protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    Log.i("MainActivity", "onNewIntent");
    String title = intent.getStringExtra(Intent.EXTRA_TEXT);
    parseUrl(title);
}
private void parseUrl(String title) {
    //取出网页地址
    Pattern pattern = Pattern.compile("【(.+)】\\n(http.+)");
    final Matcher matcher = pattern.matcher(title);
    if (matcher.find()) {
        final ProgressDialog dialog = new ProgressDialog(this);
        dialog.setMessage("正在获取视频地址,请稍后~");
        dialog.setCanceledOnTouchOutside(false);
        //解析视频真实地址
        VideoPathDecoder decoder = new VideoPathDecoder() {
            
@Override
            public void onSuccess(Video s) {
                dialog.dismiss();
                s.title = matcher.group(1);
                mDatas.add(s);
                mAdapter.notifyItemInserted(mDatas.size());
                startDownload(s);
            }
            
@Override
            public void onDecodeError(Throwable e) {
                dialog.dismiss();
                Snackbar.make(mRecyclerView, "获取视频失败!", Snackbar.LENGTH_LONG).show();
            }
        };
        dialog.show();
        decoder.decodePath(matcher.group(2));
    } else {
        Snackbar.make(mRecyclerView, "不是分享的链接", Snackbar.LENGTH_LONG).show();
    }
}

首先通过正则表达式取出分享链接,然后进行视频解析

视频解析的核心代码

AppClient.getApiService().getVideoHtml(srcUrl)
               .flatMap(new Func1>>() {
                   
@Override
                   public Observable> call(String response) {
                       Pattern pattern = Pattern.compile("videoid:\'(.+)\'");
                       Matcher matcher = pattern.matcher(response);
                       if (matcher.find()) {
                           String videoId = matcher.group(1);
                           Log.i(TAG,videoId);
                           //1.将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密。
                           String r = getRandom();
                           CRC32 crc32 = new CRC32();
                           String s = String.format(ApiService.URL_VIDEO, videoId, r);
                           //进行crc32加密。
                           crc32.update(s.getBytes());
                           String crcString = crc32.getValue() + "";
                           //2.访问http://i.snssdk.com/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()}&s={crc32值}
                           String url = ApiService.HOST_VIDEO + s + "&s=" + crcString;
                           Log.i(TAG,url);
                           return AppClient.getApiService().getVideoData(url);
                       }
                       return null;
                   }
               })
               .map(new Func1, Video>() {
                   
@Override
                   public Video call(ResultResponse videoModelResultResponse) {
                       VideoModel.VideoListBean data = videoModelResultResponse.data.video_list;
                       if (data.video_3 != null) {
                           return updateVideo(data.video_3);
                       }
                       if (data.video_2 != null) {
                           return updateVideo(data.video_2);
                       }
                       if (data.video_1 != null) {
                           return updateVideo(data.video_1);
                       }
                       return null;
                   }
                   private String getRealPath(String base64) {
                       return new String(Base64.decode(base64.getBytes(), Base64.DEFAULT));
                   }
                   private Video updateVideo(Video video) {
                       //base64解码
                       video.main_url = getRealPath(video.main_url);
                       return video;
                   }
               })
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(new Subscriber

ok,一套流程下来,我们就获取到视频的真实地址

视频获取出来,就可以下载了,

private void startDownload(final Video video) {
    FileDownload download = new FileDownload(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "todayNewsVideo"
            , UUID.randomUUID().toString() + "." + video.vtype);
    download.download(video.main_url, new FileDownload.Callback() {
        
@Override
        public void onError(Exception e) {
            mAdapter.setPercent(video.main_url, -1);
        }
        
@Override
        public void onSuccess(File file) {
            video.file = file;
            mAdapter.setPercent(video.main_url, 100);
        }
        
@Override
        public void inProgress(float progress, long total) {
            mAdapter.setPercent(video.main_url, (int) (progress * 100));
        }
    });
}

这里我没有使用retrofit,因为下载大文件的时候容易oom(希望有大神能解决我这个问题)为了方便,我直接使用原生的okhttp来下载

public void download(String url, final Callback callback) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(url).build();
        client.newCall(request).enqueue(new okhttp3.Callback() {
            
@Override
            public void onFailure(Call call, IOException e) {
                callback.onError(e);
            }
            
@Override
            public void onResponse(Call call, Response response) throws IOException {
                try {
                    final File file = saveFile(response, callback);
                    AppClient.getDelivery().post(new Runnable() {
                        
@Override
                        public void run() {
                            callback.onSuccess(file);
                        }
                    });
                } catch (IOException e) {
                    callback.onError(e);
                }
            }
        });
}

ok,整个下载的逻辑就写完了

参考

Python脚本下载今日头条视频(附加Android版本辅助下载器)

Android Canvas Clear with transparency

声明

这个属于个人开发作品,仅做学习交流使用,切勿使用于商业用途,如用本程序做非法用途后果自负,与作者无关!!

评论