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

前言

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

成果图

源码下载

https://github.com/yewei02538/TodayNewsVideoDownloader

分析视频地址

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

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

1、将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密。
2、将上面得到的加密值拼接到上面的链接中即可,最终的链接形式是:
http://i.snssdk.com/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()}&s={crc32值}
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 详解

撸代码

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

<activity
            android:name=".ui.MainActivity"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>

                <category android:name="android.intent.category.DEFAULT"/>

                <data android:mimeType="text/plain"/>
            </intent-filter>
        </activity>

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<String, Observable<ResultResponse<VideoModel>>>() {
                    @Override
                    public Observable<ResultResponse<VideoModel>> 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<ResultResponse<VideoModel>, Video>() {
                    @Override
                    public Video call(ResultResponse<VideoModel> 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<Video>() {
                    @Override
                    public void onCompleted() {

                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                        onDecodeError(e);
                    }

                    @Override
                    public void onNext(Video s) {
                        onSuccess(s);
                    }
                });

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

声明

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