net.straylightlabs.tivolibre.DecoderApp.java Source code

Java tutorial

Introduction

Here is the source code for net.straylightlabs.tivolibre.DecoderApp.java

Source

/*
 * Copyright 2015 Todd Kulesza <todd@dropline.net>.
 *
 * This file is part of TivoLibre. TivoLibre is derived from
 * TivoDecode 0.4.4 by Jeremy Drake. See the LICENSE-TivoDecode
 * file for the licensing terms for TivoDecode.
 *
 * TivoLibre is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TivoLibre is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TivoLibre.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package net.straylightlabs.tivolibre;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.apache.commons.cli.*;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.prefs.Preferences;

/**
 * Simple driver application for command line use.
 */
public class DecoderApp {
    private Options options;
    private CommandLine cli;

    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(DecoderApp.class);

    private static final String PREF_MAK = "mak";

    public static void main(String args[]) {
        DecoderApp app = new DecoderApp();
        if (app.parseCommandLineArgs(args)) {
            app.run();
        }
    }

    public boolean parseCommandLineArgs(String[] args) {
        try {
            options = buildCliOptions();
            CommandLineParser parser = new DefaultParser();
            cli = parser.parse(options, args);
        } catch (ParseException e) {
            logger.error("Parsing command line options failed: {}", e.getLocalizedMessage());
            showUsage();
            return false;
        }

        return true;
    }

    public void run() {
        if (options == null || cli == null) {
            throw new IllegalStateException("Must call parseCommandLineArgs() before calling run()");
        }

        if (cli.hasOption('v')) {
            System.out.format("TivoLibre %s%n", TivoDecoder.VERSION);
            System.exit(0);
        } else if (cli.hasOption('h')) {
            showUsage();
            System.exit(0);
        }

        Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        if (cli.hasOption('d')) {
            root.setLevel(Level.DEBUG);
        } else {
            root.setLevel(Level.ERROR);
        }

        DecoderOptions decoderOptions = new DecoderOptions();

        decoderOptions.mak = loadMak();
        if (!cli.hasOption('m')) {
            if (decoderOptions.mak == null) {
                System.err.format("Error: You must provide your media access key%n");
                showUsage();
                System.exit(1);
            }
        } else {
            decoderOptions.mak = cli.getOptionValue('m');
            saveMak(decoderOptions.mak);
        }

        if (cli.hasOption("compat-mode")) {
            logger.debug("Running in compatibility mode");
            decoderOptions.compatibilityMode = true;
        }

        if (cli.hasOption('D')) {
            decoderOptions.dumpMetadata = true;
        }

        if (cli.hasOption('p')) {
            decoderOptions.pytivoMetadata = true;
        }

        if (cli.hasOption('x')) {
            decoderOptions.noVideo = true;
        }

        try {
            InputStream inputStream = null;
            OutputStream outputStream = null;
            try {
                if (cli.hasOption('i')) {
                    inputStream = new FileInputStream(cli.getOptionValue('i'));
                } else {
                    inputStream = System.in;
                }
                if (cli.hasOption('o')) {
                    String outputLocation = cli.getOptionValue('o');
                    outputStream = new FileOutputStream(outputLocation);
                    decoderOptions.pytivoMetadataPath = appendToPath(Paths.get(outputLocation), ".txt");
                } else {
                    outputStream = System.out;
                    decoderOptions.pytivoMetadataPath = Paths.get("pytivo.txt");
                }
                decode(inputStream, outputStream, decoderOptions);
            } catch (FileNotFoundException e) {
                logger.error("Input file {} not found: {}", cli.getOptionValue('i'), e.getLocalizedMessage());
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            }
        } catch (IOException e) {
            logger.error("IOException: {}", e.getLocalizedMessage(), e);
        }
    }

    private Path appendToPath(Path path, String suffix) {
        assert (path != null);
        Path dir = path.getParent();
        Path file = path.getFileName();
        logger.info("dir = '{}', file = '{}'", dir, file);
        if (dir != null) {
            return Paths.get(dir.toString(), file.toString() + suffix);
        } else {
            return Paths.get(file.toString() + suffix);
        }
    }

    private void showUsage() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("java -jar tivo-libre.jar -i input.TiVo -o output.mpg -m 0123456789", options);
    }

    private void decode(InputStream input, OutputStream output, DecoderOptions options) {
        try (BufferedInputStream inputStream = new BufferedInputStream(input);
                BufferedOutputStream outputStream = new BufferedOutputStream(output)) {
            TivoDecoder decoder = new TivoDecoder.Builder().input(inputStream).output(outputStream).mak(options.mak)
                    .compatibilityMode(options.compatibilityMode).build();

            boolean decodeSuccessful;
            if (options.noVideo) {
                decodeSuccessful = decoder.decodeMetadata();
            } else {
                decodeSuccessful = decoder.decode();
            }

            if (decodeSuccessful && options.dumpMetadata) {
                dumpMetadata(decoder);
            }
            if (decodeSuccessful && options.pytivoMetadata) {
                dumpPytivoMetadata(decoder, options.pytivoMetadataPath);
            }

        } catch (FileNotFoundException e) {
            logger.error("Error: {}", e.getLocalizedMessage());
        } catch (IOException e) {
            logger.error("Error reading/writing files: {}", e.getLocalizedMessage());
        }
    }

    private Options buildCliOptions() {
        Options options = new Options();

        options.addOption("D", "metadata", false, "Dump TiVo recording metadata to XML files");
        options.addOption("d", "debug", false, "Show debugging information while decoding");
        options.addOption("h", "help", false, "Show this help message and exit");
        options.addOption("p", "pytivo", false, "Dump TiVo recording metadata in pyTivo format");
        options.addOption("v", "version", false, "Show version and exit");
        options.addOption("x", "no-video", false, "Exit after processing metadata; doesn't decode the video");
        Option option = Option.builder().longOpt("compat-mode")
                .desc("Don't fix problems in the TiVo file; produces output that "
                        + "is binary compatible with the TiVo DirectShow filter")
                .build();
        options.addOption(option);
        option = Option.builder("o").argName("FILENAME").longOpt("output").hasArg()
                .desc("Output file (defaults to standard output)").build();
        options.addOption(option);
        option = Option.builder("i").argName("FILENAME").longOpt("input").hasArg()
                .desc("File to decode (defaults to standard input)").build();
        options.addOption(option);
        option = Option.builder("m").argName("MAK").longOpt("mak").hasArg()
                .desc("Your media access key (will be saved between program executions)").build();
        options.addOption(option);

        return options;
    }

    private void dumpMetadata(TivoDecoder decoder) {
        int counter = 0;
        for (Document d : decoder.getMetadata()) {
            String chunkFilename = String.format("chunk-%02d.xml", counter++);
            logger.debug("Saving metadata chunk {} to {}...", counter, chunkFilename);
            try {
                OutputStream out = new FileOutputStream(chunkFilename);
                printDocument(d, out);
            } catch (IOException | TransformerException e) {
                logger.error("Error saving file {}: ", chunkFilename, e);
            }
        }
    }

    private void dumpPytivoMetadata(TivoDecoder decoder, Path metadataPath) {
        decoder.saveMetadata(metadataPath);
    }

    // From http://stackoverflow.com/questions/2325388/java-shortest-way-to-pretty-print-to-stdout-a-org-w3c-dom-document
    private static void printDocument(Document doc, OutputStream out) throws IOException, TransformerException {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

        transformer.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(out, "UTF-8")));
    }

    private void saveMak(String mak) {
        Preferences prefs = getPrefs();
        prefs.put(PREF_MAK, mak);
    }

    private String loadMak() {
        Preferences prefs = getPrefs();
        return prefs.get(PREF_MAK, null);
    }

    private Preferences getPrefs() {
        return Preferences.userNodeForPackage(DecoderApp.class);
    }

    private static class DecoderOptions {
        String mak;
        boolean compatibilityMode;
        boolean noVideo;
        boolean dumpMetadata;
        boolean pytivoMetadata;
        Path pytivoMetadataPath;
    }
}