使用JAVA合并哔哩哔哩手机客户端下载的视频

前言

使用哔哩哔哩手机客户端下载的视频在电脑上播放,无奈视频是分段的,每次都只好手动的合并再播放。而且客户端下载的视频不会按网页文件名命名,而是以av号--全数字命名。最可怕的是,每次打开一集的时候,进入的目录层级得吓死人。

视频层级

最最可怕的是,新版客户端默认文件后缀是.blv难道我们要一个一个重命名然后再合并吗?
NO!这种重复的事情交给计算机就好了。自己动手丰衣足食,我们就动手写个JAVA版的哔哩哔哩视频合并小程序。

完整项目地址: http://git.oschina.net/chengww5217/BiliBiliMerge
直接下载使用:
http://git.oschina.net/chengww5217/BiliBiliMerge/raw/master/run/BilibiliMeroV1.2.7z
使用帮助:
http://git.oschina.net/chengww5217/BiliBiliMerge/blob/master/README.md

实现功能

  • 1.自动识别文件夹下视频文件并进行合并
  • 2.合并后以视频播放页视频名称+视频分P名称命名
    F:\(日剧)夺爱之冬\第一话.flv
  • 3.合并完成删除源文件

前期准备

  • 1.得到哔哩哔哩客户端下载的视频目录

将哔哩哔哩手机客户端下载的视频移出手机的Android目录,如移动到根目录
因android MTP限制,电脑无法访问Android目录。此目录是Android应用缓存目录。
视频位于 Android--data--tv.danmaku.bili(最下面)--download下。如图显示的数字目录即为需求目录。请将数字目录移出Android目录外。

手机连上电脑后,将上述数字目录复制或移动到电脑。

  • 2.分析视频目录结构

8896746\1\entry.json 这个json包含了整个播放目录的名称和每一P的名称
8896746\1\lua.flv.bili2api.3\0.blv 这个文件夹就是各分段视频文件了。
注意:视频文件命名逻辑是:0.blv,1.blv...9.blv,10.blv...
也就是说,一旦视频文件超过10个,如 0-10,合并的时候会出现这样的合并顺序:0.blv--1.blv--10.blv--2.blv...
所以说,我们需要先把 0.blv-9.blv 重命名为 00.blv-09.blv

  • 3.FLV科普

FLV是一个二进制文件,由文件头(FLV header)和很多tag组成。tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流(关键字或者文件信息之类)。
FLV文件=FLV头文件+ tag1+tag内容1 + tag2+tag内容2 + ...+... + tagN+tag内容N。

也就是说合并FLV分段视频的时候不能简单粗暴的将多个flv视频片段按字节流的方式写到一个文件中。
这时候来看FLV合并的原理:

(1) flv 文件由1个header和若干个tag组成;
(2) header记录了视频的元数据;
(3) tag 是有时间戳的数据;
(4) flv合并的原理就是把多个文件里的tag组装起来,调整各tag的时间戳。
(5)判断是否为第一个文件,是则安装头部。

  • 了解了这些就可以动手撰写我们的合并程序了。Let's go.

流程逻辑

  • 提示输入哔哩哔哩下载的视频文件夹(输入文件夹),输入输出的文件夹。
    因最后合并完成后要删除源文件,故要求输出文件夹不能和输入文件夹相同。
    一次输入多个输入文件夹以英文逗号隔开。

  • 然后进入输入文件夹下-- entry.json 得到视频名称,和输入文件夹拼接创建目录。
    如:输出到 F:\\视频名称 文件夹

  • 执行合并
    listFiles()执行两次进入到这个文件夹

entry.json得到视频每一P的名称,拼接输出如F:\\视频名称\第一话.flv
判断进入lua.flv.bili2api.3文件夹即可得到所有视频文件
判断对 0.flv-9.flv 进行重命名---> 00.flv-09.flv
进行合并操作

  • 删除源文件

程序

  • 1.首先eclipse建项目
    包结构很简单
包结构
  • 2.输入输出文件夹
    包含main方法的Bilibili.java
    输入输出文件夹
        File out;
        File[] in = null;
        while(true){
            boolean isBreak = true;
            Scanner scanner = new Scanner(System.in);
            String line = scanner.nextLine();
            if(line == null || line.length() == 0){
                System.out.println("输入不为空,请重试:");
                isBreak = false;
            }else{
                String[] lines = line.split(",");
                in = new File[lines.length];
                for(int i = 0;i < lines.length;i++){
                    in[i] = new File(lines[i]);
                    if(!in[i].exists()){
                        System.out.println(in[i].getAbsolutePath() + "文件夹不存在,请重试:");
                        isBreak = false;
                        break;
                    }
                }
            }
            if(isBreak){
                break;
            }
        }
        
        System.out.println("请输入输出路径:");
        while(true){
            Scanner scanner = new Scanner(System.in);
            String line = scanner.nextLine();
            out = new File(line);
            if(!out.exists()){
                System.out.println("文件夹不存在,请重试:");
            }else{
                boolean isEquals = true;
                for(int i = 0;i < in.length;i++){
                    if(out.getAbsolutePath().equals(in[i].getAbsolutePath())){
                        isEquals = false;
                        System.out.println("输出路径和某个输入路径相同,请重试:");
                        break;
                    }
                }
                if(isEquals){
                    break;
                }
            }
        }
  • 3.循环读取多个输入目录的视频名称
//循环
        for(int i = 0;i < in.length;i++){
            //得到播放文件名,如"(日剧)夺爱之冬"
            String path = in[i].getAbsolutePath() +separator+ "1"+separator+"entry.json";
            String line = null;
            try {
                BufferedReader reader =  
                    new BufferedReader(new InputStreamReader(new FileInputStream(path), Charset.forName("utf-8"))); 
                line = reader.readLine();
                reader.close();
                System.out.println("json="+line);
            } catch (Exception e) {
                e.printStackTrace();
            }
        
            //输出路径
            String[] names = tool.json_getName(line);
            String episode_path = out.getAbsolutePath() + separator + names[0];
            File episode = new File(episode_path);
            if(!episode.exists()){
                episode.mkdirs();
            }
            System.out.println("输出:"+episode_path);
            //合并
            tool.doMerge(in[i], episode_path);
        }
  • 4.判断对 0.flv-9.flv 进行重命名---> 00.flv-09.flv 后合并
public void doMerge(File in,String episode_path){
        //1、2、3、4...
        File[] files = in.listFiles();
                
        //循环
        for(File f : files){
            //文件名,如第一话
            String name = null;
            //获得所有名为.blv的文件
            File[] ffs = null;
            File[] fs = f.listFiles();
            for(final File ff : fs){
                if(ff.getName().equals("entry.json")){
                    String json_name = null;
                    try {
                        BufferedReader reader =  
                                      new BufferedReader(new InputStreamReader(new FileInputStream(ff), Charset.forName("utf-8")));
                        json_name = reader.readLine();
                        reader.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                        }
                        name = json_getName(json_name)[1];
                    }
                    if(ff.isDirectory() && ff.getName().startsWith("lua.")){
                        //重命名
                        for(int i = 0; i < ff.list().length;i++){
                            File pathname = ff.listFiles()[i];
                            //0.blv -- 00.blv
                            if(pathname.getName().endsWith(".blv") && pathname.getName().length() == 5){
                                pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".blv"));
                            }
                            if(pathname.getName().endsWith(".flv") && pathname.getName().length() == 5){
                                pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".flv"));
                            }
                            //0.blv.bdl -- 00.blv.bdl
                            if(pathname.getName().endsWith(".blv.bdl") && pathname.getName().length() == 9){
                                pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".blv.bdl"));
                            }
                        }
                        ffs = ff.listFiles(new FileFilter() {
                                
                            public boolean accept(File pathname) {
                                for(int i = 0;i < ff.list().length;i++){
                                    if(pathname.getName().endsWith(".blv") || pathname.getName().endsWith(".flv") || pathname.getName().endsWith(".blv.bdl")){
                                        return true;
                                    }
                                }
                                return false;
                            }
                        });
                        //合并
                        System.out.println("开始合并...");
                        FlvMerge mFlvMerge = new FlvMerge();
                        try {
                            mFlvMerge.merge(ffs, new File(episode_path + File.separator + name + ".flv"));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
    }
  • 5.递归删除操作
public boolean deleteFolder(File file){  
        if(!file.exists()){  
            return false;  
        }  
        if(file.isFile() || file.listFiles().length == 0){  
            file.delete();  
            return true;  
        }else{  
            File[] files = file.listFiles();  
            for(int i=0;i<files.length;i++){  
                deleteFolder(files[i]);  
            }  
            file.delete();  
            return true;
        }
    }
  • 6.具体怎么对FLV视频进行合并的,请点击这里 ,注释比较清晰。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,629评论 18 139
  • 1 IONo18 1.1IO框架 【 IO:Input Output 在程序运行的过程中,可能需要对一些设备进...
    征程_Journey阅读 952评论 0 1
  • File类 File类用来操作文件路径或文件夹路径 绝对路径从根目录开始 相对路径在eclipse中代表当前项目根...
    JerichoPH阅读 575评论 0 3
  • 小子凌晨做噩梦,嚷着“妈妈不要去玉环”。 醒来还强调一次,懒妈安慰:“妈妈和宝贝一起,不去玉环”。 小子想了下,又...
    欧元小姨阅读 134评论 1 1