it.ventuland.ytd.YTDownloadThread.java Source code

Java tutorial

Introduction

Here is the source code for it.ventuland.ytd.YTDownloadThread.java

Source

/**
 *  This file is part of ytd2
 *
 *  ytd2 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.
 *
 *  ytd2 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 ytd2.
 *  If not, see <http://www.gnu.org/licenses/>.
 */
package it.ventuland.ytd;

import it.ventuland.ytd.config.YtdConfigManager;
import it.ventuland.ytd.event.DownloadEvent;
import it.ventuland.ytd.event.DownloadEvent.DOWNLOAD_STATUS;
import it.ventuland.ytd.event.DownloaderListener;
import it.ventuland.ytd.httpclient.IYtdResponse;
import it.ventuland.ytd.httpclient.YTDHttpConfig;
import it.ventuland.ytd.ui.GUIClient;
import it.ventuland.ytd.ui.UrlList;
import it.ventuland.ytd.ui.VideoElement;
import it.ventuland.ytd.utils.UTF8Utils;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;

/**
 * http://www.youtube.com/watch?v=9QFK1cLhytY Javatar and .NOT http://www.youtube.com/watch?v=Mt7zsortIXs 1080p "Lady Java" http://www.youtube.com/watch?v=WowZLe95WDY Tom Petty And the Heartbreakers - Learning to Fly (with lyrics) http://www.youtube.com/watch?v=86OfBExGSE0 URZ 720p http://www.youtube.com/watch?v=cNOP2t9FObw Blade 360 - 480 http://www.youtube.com/watch?v=HvQBrM_i8bU MZ 1000 Street Fighter http://www.youtube.com/watch?v=yVpbFMhOAwE How Linx is build http://www.youtube.com/watch?v=4XpnKHJAok8 Tech Talk: Linus Torvalds on git
 * 
 * ODOT http://sourceforge.net/p/ytd2/bugs/7/ http://www.youtube.com/watch?v=fRVVzXnRsUQ uses RTMPE (http://en.wikipedia.org/wiki/Protected_Streaming), which ytd2 cannot download atm
 * 
 */
public class YTDownloadThread extends Thread {

    static volatile int iThreadcount = 0;

    int iThreadNo = YTDownloadThread.iThreadcount++; // every download thread get its own number

    boolean mForceNoDownload;
    final String ssourcecodeurl = "http://";
    final String ssourcecodeuri = "[a-zA-Z0-9%&=\\.]";

    String sTitle = null; // will be used as filename

    String sURL = null; // main URL (youtube start web page)
    String sVideoURL = null; // one video web resource
    Vector<YTUrl> vNextVideoURL = new Vector<YTUrl>(); // list of URLs from webpage source
    private UrlList mUrlList = null;
    String sFileName = null; // contains the absolute filename
    //boolean bisinterrupted = false; // basically the same as Thread.isInterrupted()
    int iRecursionCount = -1; // counted in downloadone() for the 3 webrequest to one video

    String sContentType = null;

    private boolean isDebug = false;

    private ArrayList<DownloaderListener> mDnlListenerList = new ArrayList<DownloaderListener>();

    public YTDownloadThread(YTDownloadThreadData pSetupData) {
        super();
        mUrlList = pSetupData.videoElementQueue;
        isDebug = pSetupData.isDebug;
        mForceNoDownload = pSetupData.forceNoDowload;
        String sv = "thread started: ".concat(this.getMyName());
        debugoutput(sv);
    }

    public synchronized void addDownloadListener(DownloaderListener listener) {
        if (!mDnlListenerList.contains(listener)) {
            mDnlListenerList.add(listener);
        }
    }

    /**
     * Recursive function that perform video download
     * @param pVideo the video url
     * @return true if all ok, false otherwise
     */
    private boolean downloadone(String pUrl, VideoElement pVideo) {
        BufferedReader textreader = null;
        BufferedInputStream binaryreader = null;
        YTDHttpConfig httpclient = null;
        boolean rc = false;
        // stop recursion
        String sURL = pUrl;
        try {
            if (sURL.equals("")) {
                return (false);
            }
        } catch (NullPointerException npe) {
            return (false);
        }
        if (isWorkerInterrupted()) {
            return (false); // try to get information about application shutdown
        }
        try {

            boolean rc204 = false;
            boolean rc302 = false;

            IYtdResponse response = null;

            iRecursionCount++;

            debugoutput("start.");

            httpclient = new YTDHttpConfig(sURL, GUIClient.getProxy());
            response = httpclient.connect();
            this.sVideoURL = null;
            this.sContentType = response.getFirstHeaderValue("Content-Type").toLowerCase();

            debugoutput("HTTP response status line:".concat(response.getStatusLine()));

            // abort if HTTP response code is != 200, != 302 and !=204 - wrong URL?
            if (!(rc = response.getStatusLine().matches("^(http)(.*)200(.*)"))
                    & !(rc204 = response.getStatusLine().matches("^(http)(.*)204(.*)"))
                    & !(rc302 = response.getStatusLine().matches("^(http)(.*)302(.*)"))) {
                debugoutput(response.getStatusLine().concat(" ").concat(sURL));
                output(response.getStatusLine().concat(" \"").concat(this.sTitle).concat("\""));
                return (rc & rc204 & rc302);
            }
            if (rc204) {
                debugoutput("last response code==204 - download: ".concat(this.vNextVideoURL.get(0).getsYTID()));
                rc = downloadone(this.vNextVideoURL.get(0).getsURL(), pVideo);
                return (rc);
            }
            if (rc302) {
                debugoutput("location from HTTP Header: ".concat(response.getFirstHeaderString("Location")));
            }

            // real file download
            InputStream lResponseStream = response.getEntityContent();
            if (lResponseStream != null) {
                if (this.sContentType.matches("^text/html(.*)")) {
                    textreader = new BufferedReader(new InputStreamReader(lResponseStream));
                    rc = savetextdata(textreader, iRecursionCount, pVideo);
                } else if (this.sContentType.matches("video/(.)*")) {
                    binaryreader = new BufferedInputStream(lResponseStream);
                    if (mForceNoDownload) {
                        reportheaderinfo(response.getNativeResponse());
                    } else {
                        Long lByteMax = Long.parseLong(response.getFirstHeaderValue("Content-Length"));
                        savebinarydata(binaryreader, lByteMax);
                    }
                } else { // content-type is not video/
                    rc = false;
                    this.sVideoURL = null;
                }
            }

            httpclient.close();

            debugoutput("done: ".concat(sURL));
            if (this.sVideoURL == null) {
                this.sVideoURL = ""; // to prevent NPE
            }

            if (!this.sVideoURL.matches(GUIClient.szURLREGEX)) {
                // no more recursion - html source hase been read
                // test !rc than video could not downloaded because of some error (like wrong protocol or restriction)
                if (!rc) {
                    debugoutput("cannot download video - URL does not seem to be valid or could not be found: "
                            .concat(this.sURL));
                    output("there was a problem getting the video URL! perhaps not allowed in your country?!");
                    output(("consider reporting the URL to author! - ").concat(this.sURL));
                    this.sVideoURL = null;
                }
            } else {
                // enter recursion - download video resource
                debugoutput("try to download video from URL: ".concat(this.sVideoURL));
                rc = downloadone(this.sVideoURL, pVideo);
            }
            this.sVideoURL = null;

        } catch (UnknownHostException uhe) {
            output(("error connecting to: ").concat(uhe.getMessage()));
            debugoutput(uhe.getMessage());
            rc = false;
        } catch (Throwable t) {
            debugoutput(t.getMessage());
            rc = false;
        } finally {
            try {
                if (textreader != null) {
                    textreader.close();
                }
            } catch (Exception e) {
            }
            try {
                if (binaryreader != null) {
                    binaryreader.close();
                }
            } catch (Exception e) {
            }
            try {
                if (httpclient != null) {
                    httpclient.close();
                }
            } catch (Exception e) {
            }
        }
        return (rc);
    }

    void reportheaderinfo(HttpResponse response) {
        if (isDebug) {
            debugoutput("");
            debugoutput("NO-DOWNLOAD mode active (ndl on)");
            debugoutput("all HTTP header fields:");
            for (int i = 0; i < response.getAllHeaders().length; i++) {
                debugoutput(response.getAllHeaders()[i].getName().concat("=")
                        .concat(response.getAllHeaders()[i].getValue()));
            }
            debugoutput("filename would be: ".concat(this.getTitle()).concat(".").concat(response
                    .getFirstHeader("Content-Type").getValue().replaceFirst("video/", "").replaceAll("x-", ""))); // title contains just filename, no path
        } else {
            Long iFileSize = Long.parseLong(response.getFirstHeader("Content-Length").getValue());
            output("");
            output("NO-DOWNLOAD active (ndl on)");
            output("some HTTP header fields:");
            output("content-type: ".concat(response.getFirstHeader("Content-Type").getValue()));
            output("content-length: ".concat(iFileSize.toString()).concat(" Bytes").concat(" ~ ")
                    .concat(Long.toString((iFileSize / 1024)).concat(" KiB")).concat(" ~ ")
                    .concat(Long.toString((iFileSize / 1024 / 1024)).concat(" MiB")));
            if (mForceNoDownload) {
                output(("filename would be: ")
                        .concat(this.getTitle().concat(".").concat(response.getFirstHeader("Content-Type")
                                .getValue().replaceFirst("video/", "").replaceAll("x-", "")))); // title contains just filename, no path
            }
        }
    }

    private int addMPEG_HD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;
        // try 3D HD first if 3D is selected
        if (pVideo.is3D) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("84"), this.sURL, "3D")); // mpeg 3D full HD
        }

        // if SDS is on reverse order! - 720p before 1080p for HD and so on
        if (!GUIClient.bSaveDiskSpace) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("37"), this.sURL)); // mpeg full HD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("22"), this.sURL)); // mpeg half HD
        } else {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("22"), this.sURL)); // mpeg half HD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("37"), this.sURL)); // mpeg full HD
        }
        return inewiindex;
    }

    private int addWBEM_HD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;
        // try 3D HD first if 3D is selected
        if (pVideo.is3D) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("100"), this.sURL, "3D")); // webm 3D HD
        }

        if (!GUIClient.bSaveDiskSpace) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("46"), this.sURL)); // webm full HD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("45"), this.sURL)); // webm half HD
        } else {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("45"), this.sURL)); // webm half HD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("46"), this.sURL)); // webm full HD
        }
        return inewiindex;
    }

    private int addWBEM_SD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;
        // try 3D first if 3D is selected
        if (pVideo.is3D) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("102"), this.sURL, "3D")); // webm 3D SD
        }

        if (!GUIClient.bSaveDiskSpace) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("44"), this.sURL)); // webm SD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("43"), this.sURL)); // webm SD
        } else {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("43"), this.sURL)); // webm SD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("44"), this.sURL)); // webm SD
        }
        return inewiindex;
    }

    private int addFLV_SD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;

        if (!GUIClient.bSaveDiskSpace) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("35"), this.sURL)); // flv SD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("34"), this.sURL)); // flv SD
        } else {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("34"), this.sURL)); // flv SD
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("35"), this.sURL)); // flv SD
        }
        return inewiindex;
    }

    private int addMPEG_SD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;

        // try 3D first if 3D is selected
        if (pVideo.is3D) {
            this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("82"), this.sURL, "3D")); // mpeg 3D SD
        }

        this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("18"), this.sURL)); // mpeg SD
        return inewiindex;
    }

    private int addMPEG_LD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;
        this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("36"), this.sURL)); // mpeg LD
        this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("17"), this.sURL)); // mpeg LD
        return inewiindex;
    }

    private int addFLV_LD_Urls(int iindex, HashMap<String, String> ssourcecodevideourls, VideoElement pVideo) {
        int inewiindex = iindex;

        this.vNextVideoURL.add(inewiindex++, new YTUrl(ssourcecodevideourls.get("5"), this.sURL)); // flv LD
        return inewiindex;
    }

    boolean savetextdata(BufferedReader textreader, int iRecursionCount, VideoElement pVideo) throws IOException {
        boolean rc = false;
        // read html lines one by one and search for java script array of video URLs
        String sline = "";
        while (sline != null) {
            sline = textreader.readLine();
            try {
                if (iRecursionCount == 0 && sline.matches("(.*)\"url_encoded_fmt_stream_map\":(.*)")) {
                    rc = true;
                    HashMap<String, String> ssourcecodevideourls = new HashMap<String, String>();

                    sline = sline.replaceFirst(".*\"url_encoded_fmt_stream_map\": \"", "");
                    sline = sline.replaceFirst("\".*", "");
                    sline = sline.replace("%25", "%");
                    sline = sline.replace("\\u0026", "&");
                    sline = sline.replace("\\", "");

                    // by anonymous
                    String[] ssourcecodeyturls = sline.split(",");
                    debugoutput("ssourcecodeuturls.length: ".concat(Integer.toString(ssourcecodeyturls.length)));
                    String sResolutions = "found video URL for resolution: ";

                    for (String urlString : ssourcecodeyturls) {

                        // assuming rtmpe is used for all resolutions, if found once - end download
                        if (urlString.matches(".*conn=rtmpe.*")) {
                            debugoutput("RTMPE found. cannot download this one!");
                            output("Unable to download video due to unsupported protocol (RTMPE). sry!");
                            break;
                        }
                        String[] fmtUrlPair = urlString.split("url=http", 2);
                        fmtUrlPair[1] = "url=http" + fmtUrlPair[1] + "&" + fmtUrlPair[0];
                        // grep itag=xz out and use xy as hash key
                        // 2013-02 itag now has up to 3 digits
                        fmtUrlPair[0] = fmtUrlPair[1].substring(fmtUrlPair[1].indexOf("itag=") + 5,
                                fmtUrlPair[1].indexOf("itag=") + 5 + 1
                                        + (fmtUrlPair[1].matches(".*itag=[0-9]{2}.*") ? 1 : 0)
                                        + (fmtUrlPair[1].matches(".*itag=[0-9]{3}.*") ? 1 : 0));
                        fmtUrlPair[1] = fmtUrlPair[1].replaceFirst("url=http%3A%2F%2F", "http://");
                        fmtUrlPair[1] = fmtUrlPair[1].replaceAll("%3F", "?").replaceAll("%2F", "/")
                                .replaceAll("%3B", ";").replaceAll("%2C", ",").replaceAll("%3D", "=")
                                .replaceAll("%26", "&").replaceAll("%252C", "%2C").replaceAll("sig=", "signature=")
                                .replaceAll("&s=", "&signature=").replaceAll("\\?s=", "?signature=");

                        // remove duplicated &itag=xy
                        if (StringUtils.countMatches(fmtUrlPair[1], "itag=") == 2) {
                            fmtUrlPair[1] = fmtUrlPair[1].replaceFirst("itag=[0-9]{1,3}", "");
                        }

                        try {
                            ssourcecodevideourls.put(fmtUrlPair[0], fmtUrlPair[1]); // save that URL
                            // debugoutput(String.format( "video url saved with key %s: %s",fmtUrlPair[0],ssourcecodevideourls.get(fmtUrlPair[0]) ));
                            sResolutions = sResolutions.concat(fmtUrlPair[0].equals("37") ? "1080p mpeg, " : // HD type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
                                    fmtUrlPair[0].equals("22") ? "720p mpeg, " : // HD type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
                                            fmtUrlPair[0].equals("84") ? "1080p 3d mpeg, " : // HD 3D type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
                                                    fmtUrlPair[0].equals("35") ? "480p flv, " : // SD type=video/x-flv
                                                            fmtUrlPair[0].equals("18") ? "360p mpeg, " : // SD type=video/mp4;+codecs="avc1.42001E,+mp4a.40.2"
                                                                    fmtUrlPair[0].equals("34") ? "360p flv, " : // SD type=video/x-flv
                                                                            fmtUrlPair[0].equals("82")
                                                                                    ? "360p 3d mpeg, "
                                                                                    : // SD 3D type=video/mp4;+codecs="avc1.42001E,+mp4a.40.2"
                                                                                    fmtUrlPair[0].equals("36")
                                                                                            ? "240p mpeg 3gpp, "
                                                                                            : // LD type=video/3gpp;+codecs="mp4v.20.3,+mp4a.40.2"
                                                                                            fmtUrlPair[0].equals(
                                                                                                    "17") ? "114p mpeg 3gpp, "
                                                                                                            : // LD type=video/3gpp;+codecs="mp4v.20.3,+mp4a.40.2"

                                                                                                            fmtUrlPair[0]
                                                                                                                    .equals("46")
                                                                                                                            ? "1080p webm, "
                                                                                                                            : // HD type=video/webm;+codecs="vp8.0,+vorbis"&
                                                                                                                            fmtUrlPair[0]
                                                                                                                                    .equals("45")
                                                                                                                                            ? "720p webm, "
                                                                                                                                            : // HD type=video/webm;+codecs="vp8.0,+vorbis"
                                                                                                                                            fmtUrlPair[0]
                                                                                                                                                    .equals("100")
                                                                                                                                                            ? "1080p 3d webm, "
                                                                                                                                                            : // HD 3D type=video/webm;+codecs="vp8.0,+vorbis"&
                                                                                                                                                            fmtUrlPair[0]
                                                                                                                                                                    .equals("44")
                                                                                                                                                                            ? "480p webm, "
                                                                                                                                                                            : // SD type=video/webm;+codecs="vp8.0,+vorbis"
                                                                                                                                                                            fmtUrlPair[0]
                                                                                                                                                                                    .equals("43")
                                                                                                                                                                                            ? "360p webm, "
                                                                                                                                                                                            : // SD type=video/webm;+codecs="vp8.0,+vorbis"
                                                                                                                                                                                            fmtUrlPair[0]
                                                                                                                                                                                                    .equals("102")
                                                                                                                                                                                                            ? "360p 3d webm, "
                                                                                                                                                                                                            : // SD 3D type=video/webm;+codecs="vp8.0,+vorbis"&
                                                                                                                                                                                                            fmtUrlPair[0]
                                                                                                                                                                                                                    .equals("5")
                                                                                                                                                                                                                            ? "240p flv, "
                                                                                                                                                                                                                            : // LD type=video/x-flv
                                                                                                                                                                                                                            "unknown resolution! ("
                                                                                                                                                                                                                                    .concat(fmtUrlPair[0])
                                                                                                                                                                                                                                    .concat(")"));
                        } catch (java.lang.ArrayIndexOutOfBoundsException aioobe) {
                        }
                    } // for

                    output(sResolutions);
                    debugoutput(sResolutions);

                    int iindex;
                    iindex = 0;
                    this.vNextVideoURL.removeAllElements();

                    debugoutput(
                            "ssourcecodevideourls.length: ".concat(Integer.toString(ssourcecodevideourls.size())));
                    // figure out what resolution-button is pressed now and fill list with possible URLs

                    switch (pVideo.videoQuality) {
                    case "HD": // HD
                        // try 1080p/720p in selected format first. if it's not available than the other format will be used
                        if ("MPEG".equals(pVideo.videoFormat)) {
                            iindex = addMPEG_HD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        if ("WEBM".equals(pVideo.videoFormat)) {
                            iindex = addWBEM_HD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        // there are no FLV HD URLs for now, so at least try mpg,wbem HD then
                        iindex = addMPEG_HD_Urls(iindex, ssourcecodevideourls, pVideo);
                        iindex = addWBEM_HD_Urls(iindex, ssourcecodevideourls, pVideo);

                    case "Std": // SD
                        // try to download desired format first, if it's not available we take the other of same res
                        if ("MPEG".equals(pVideo.videoFormat)) {
                            iindex = addMPEG_SD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        if ("WEBM".equals(pVideo.videoFormat)) {
                            iindex = addWBEM_SD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        if ("FLV".equals(pVideo.videoFormat)) {
                            iindex = addFLV_SD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        iindex = addMPEG_SD_Urls(iindex, ssourcecodevideourls, pVideo);
                        iindex = addWBEM_SD_Urls(iindex, ssourcecodevideourls, pVideo);
                        iindex = addFLV_SD_Urls(iindex, ssourcecodevideourls, pVideo);

                    case "LD": // LD

                        // TODO this.sFilenameResPart = "(LD)"; // adding LD to filename because HD-Videos are almost already named HD (?)
                        if ("MPEG".equals(pVideo.videoFormat)) {
                            iindex = addMPEG_LD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        if ("WEBM".equals(pVideo.videoFormat)) {
                            // there are no wbem LD URLs for now
                        }

                        if ("FLV".equals(pVideo.videoFormat)) {
                            iindex = addFLV_LD_Urls(iindex, ssourcecodevideourls, pVideo);
                        }

                        // we must ensure all (16) possible URLs get added to the list so that the list of URLs is never empty
                        iindex = addMPEG_LD_Urls(iindex, ssourcecodevideourls, pVideo);
                        iindex = addFLV_LD_Urls(iindex, ssourcecodevideourls, pVideo);

                        break;
                    default:
                        // this.vNextVideoURL = null;
                        this.sVideoURL = null;
                        break;
                    }

                    // if the first 2 entries are null than there are no URLs for the selected resolution
                    // strictly speaking this is only true for HD as there are only two URLs in contrast to three of SD - in this case the output will not be shown but downloading should work anyway
                    if (this.vNextVideoURL.get(0).getsURL() == null
                            && this.vNextVideoURL.get(1).getsURL() == null) {
                        String smsg = "could not find video url for selected resolution! trying lower res...";
                        output(smsg);
                        debugoutput(smsg);
                    }

                    // remove null entries in list - we later try to download the first (index 0) and if it fails the next one (at index 1) and so on
                    for (int x = this.vNextVideoURL.size() - 1; x >= 0; x--) {
                        if (this.vNextVideoURL.get(x).getsURL() == null) {
                            this.vNextVideoURL.remove(x);
                        }
                    }

                    try {
                        this.sVideoURL = this.vNextVideoURL.get(0).getsURL();
                        debugoutput(String.format("trying this one: %s %s %s", this.vNextVideoURL.get(0).getsITAG(),
                                this.vNextVideoURL.get(0).getsQUALITY(), this.vNextVideoURL.get(0).getsTYPE()));
                    } catch (ArrayIndexOutOfBoundsException aioobe) {
                    }

                    this.setTitle(this.getTitle()
                            .concat(!this.vNextVideoURL.get(0).getsRESPART().equals("")
                                    ? "." + this.vNextVideoURL.get(0).getsRESPART()
                                    : ""));

                }

                if (iRecursionCount == 0 && sline.matches("(.*)<meta name=\"title\" content=(.*)")) {
                    String stmp = sline.replaceFirst("(.*)<meta name=\"title\" content=", "").trim();
                    // change html characters to their UTF8 counterpart
                    stmp = UTF8Utils.changeHTMLtoUTF8(stmp);
                    stmp = stmp.replaceFirst("^\"", "").replaceFirst("\">$", "");

                    // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
                    //
                    stmp = stmp.replaceAll("<", "");
                    stmp = stmp.replaceAll(">", "");
                    stmp = stmp.replaceAll(":", "");
                    stmp = stmp.replaceAll("/", " ");
                    stmp = stmp.replaceAll("\\\\", " ");
                    stmp = stmp.replaceAll("|", "");
                    stmp = stmp.replaceAll("\\?", "");
                    stmp = stmp.replaceAll("\\*", "");
                    stmp = stmp.replaceAll("/", " ");
                    stmp = stmp.replaceAll("\"", " ");
                    stmp = stmp.replaceAll("%", "");

                    this.setTitle(stmp); // complete file name without path
                }

            } catch (NullPointerException npe) {
            }
        } // while
        return rc;
    } // savetextdata()

    void savebinarydata(BufferedInputStream binaryreader, Long iBytesMax) throws IOException {
        FileOutputStream fos = null;
        try {
            File f;
            Integer idupcount = 0;
            String sdirectorychoosed = YtdConfigManager.getInstance().getSaveDirectoryPath();

            String sfilename = this.getTitle();
            debugoutput("title: ".concat(this.getTitle()).concat("sfilename: ").concat(sfilename));
            do {
                f = new File(sdirectorychoosed,
                        sfilename.concat((idupcount > 0 ? "(".concat(idupcount.toString()).concat(")") : ""))
                                .concat(".")
                                .concat(this.sContentType.replaceFirst("video/", "").replaceAll("x-", "")));
                idupcount += 1;
            } while (f.exists());
            this.setFileName(f.getAbsolutePath());

            Long iBytesReadSum = (long) 0;
            Long iPercentage = (long) -1;
            fos = new FileOutputStream(f);

            debugoutput(String.format("writing %d bytes to: %s", iBytesMax, this.getFileName()));
            output(("file size of \"").concat(this.getTitle()).concat("\" = ").concat(iBytesMax.toString())
                    .concat(" Bytes").concat(" ~ ").concat(Long.toString((iBytesMax / 1024)).concat(" KiB"))
                    .concat(" ~ ").concat(Long.toString((iBytesMax / 1024 / 1024)).concat(" MiB")));

            byte[] bytes = new byte[4096];
            Integer iBytesRead = 1;

            // adjust blocks of percentage to output - larger files are shown with smaller pieces
            Integer iblocks = 10;
            if (iBytesMax > 20 * 1024 * 1024) {
                iblocks = 4;
            }
            if (iBytesMax > 32 * 1024 * 1024) {
                iblocks = 2;
            }
            if (iBytesMax > 56 * 1024 * 1024) {
                iblocks = 1;
            }
            while (!isWorkerInterrupted() && iBytesRead > 0) {
                iBytesRead = binaryreader.read(bytes);
                iBytesReadSum += iBytesRead;
                // drop a line every x% of the download
                if ((((iBytesReadSum * 100 / iBytesMax) / iblocks) * iblocks) > iPercentage) {
                    iPercentage = (((iBytesReadSum * 100 / iBytesMax) / iblocks) * iblocks);
                    processDownloadEvent(new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.DOWNLOADING,
                            iPercentage.intValue(), sURL, null));
                }
                // TODO calculate and show ETA for bigger downloads (remaining time > 60s) - every 20%?

                try {
                    fos.write(bytes, 0, iBytesRead);
                } catch (IndexOutOfBoundsException ioob) {
                }
            }

            // rename files if download was interrupted before completion of download
            if (isWorkerInterrupted() && iBytesReadSum < iBytesMax) {
                try {
                    // this part is especially for our M$-Windows users because of the different behavior of File.renameTo() in contrast to non-windows
                    // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6213298 and others
                    // even with Java 1.6.0_22 the renameTo() does not work directly on M$-Windows!
                    fos.close();
                } catch (Exception e) {
                }
                // System.gc(); // we don't have to do this but to be sure the file handle gets released we do a thread sleep
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                }
                // this part runs on *ix platforms without closing the FileOutputStream explicitly
                debugoutput(String.format("download canceled. (%d)", (iBytesRead)));
                changeFileNamewith("CANCELED.");
                String smsg = "renaming unfinished file to: ".concat(this.getFileName());
                output(smsg);
                debugoutput(smsg);

                // CANCELED filenames overwrite others as we do not test for CANCELED one, two...
                if (!f.renameTo(new File(this.getFileName()))) {
                    smsg = "error renaming unfinished file to: ".concat(this.getFileName());
                    output(smsg);
                    debugoutput(smsg);
                }
            }
            debugoutput("done writing.");
        } catch (FileNotFoundException fnfe) {
            throw (fnfe);
        } catch (IOException ioe) {
            debugoutput("IOException");
            throw (ioe);
        } finally {
            this.sVideoURL = null;
            try {
                fos.close();
            } catch (Exception e) {
            }
        }
    }

    void changeFileNamewith(String string) {
        File f = null;
        Integer idupcount = 0;
        String sfilesep = System.getProperty("file.separator");
        if (sfilesep.equals("\\")) {
            sfilesep += sfilesep; // on m$-windows we need to escape the \
        }

        String sdirectorychoosed = "";
        String[] srenfilename = this.getFileName().split(sfilesep);

        try {
            for (int i = 0; i < srenfilename.length - 1; i++) {
                sdirectorychoosed += srenfilename[i].concat((i < srenfilename.length - 1) ? sfilesep : ""); // constructing folder where file is saved now (could be changed in GUI already)
            }
        } catch (ArrayIndexOutOfBoundsException aioobe) {
        }

        String sfilename = srenfilename[srenfilename.length - 1];
        debugoutput("changeFileNamewith() sfilename: ".concat(sfilename));
        do {
            // filename will be prepended with a parameter string and possibly a duplicate counter
            f = new File(sdirectorychoosed, string
                    .concat((idupcount > 0 ? "(".concat(idupcount.toString()).concat(")") : "")).concat(sfilename));
            idupcount += 1;
        } while (f.exists());

        debugoutput("changeFileNamewith() new filename: ".concat(f.getAbsolutePath()));
        this.setFileName(f.getAbsolutePath());

    }

    String getTitle() {
        if (this.sTitle != null) {
            return this.sTitle;
        } else {
            return ("");
        }
    }

    void setTitle(String sTitle) {
        this.sTitle = sTitle;
    }

    String getFileName() {
        if (this.sFileName != null) {
            return this.sFileName;
        } else {
            return ("");
        }
    }

    void setFileName(String sFileName) {
        this.sFileName = sFileName;
    }

    synchronized void debugoutput(String s) {
        if (!isDebug) {
            return;
        }
        // sometimes this happens: Exception in thread "Thread-2" java.lang.Error: Interrupted attempt to aquire write lock (on quit only)
        try {
            GUIClient.addTextToConsole("#DEBUG ".concat(this.getMyName()).concat(" ").concat(s));
        } catch (Exception e) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e1) {
            }
            try {
                GUIClient.addTextToConsole("#DEBUG ".concat(this.getMyName()).concat(" ").concat(s));
            } catch (Exception e2) {
            }
        }
    }

    void output(String s) {
        if (isDebug) {
            return;
        }
        GUIClient.addTextToConsole("#info - ".concat(s));
    }

    String getMyName() {
        return this.getClass().getName().concat(Integer.toString(this.iThreadNo));
    }

    @Override
    public void run() {
        boolean lbDownloadOk = false;
        boolean lbRun = true;
        while (lbRun && !isWorkerInterrupted()) {
            try {
                VideoElement lUrl = mUrlList.getElement();

                if (lUrl != null) {
                    sURL = lUrl.videoUrl;
                    this.processDownloadEvent(
                            new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.STARTED, 0, sURL, null));

                    // download one webresource and show result
                    lbDownloadOk = downloadone(sURL, lUrl);
                    this.iRecursionCount = -1;
                    if (lbDownloadOk && !this.mForceNoDownload) {
                        output(("download complete: ").concat("\"").concat(this.getTitle()).concat("\"")
                                .concat(" to ").concat(this.getFileName()));
                        this.processDownloadEvent(
                                new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.COMPLETED, 0, sURL, null));
                    } else {
                        if (!mForceNoDownload) {
                            this.processDownloadEvent(
                                    new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.FAILED, 0, sURL, null));
                        } else {
                            this.processDownloadEvent(new DownloadEvent(this, iThreadNo,
                                    DOWNLOAD_STATUS.COMPLETED_NOT_DOWNLOAD, 0, sURL, null));
                        }
                    }
                } else {
                    this.processDownloadEvent(
                            new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.IDLE, 0, sURL, null));
                }

            } catch (InterruptedException e) {
                lbRun = false;
            } catch (Throwable t) {
                t.printStackTrace();
                lbRun = false;
            }
        }
        debugoutput("thread ended: ".concat(this.getMyName()));
        this.processDownloadEvent(new DownloadEvent(this, iThreadNo, DOWNLOAD_STATUS.ABORTING, 0, null, null));
        YTDownloadThread.iThreadcount--;
    }

    private boolean isWorkerInterrupted() {
        boolean isInterrupted = isInterrupted();

        return isInterrupted;
    }

    @SuppressWarnings("unchecked")
    private void processDownloadEvent(DownloadEvent pDnlEvent) {
        List<DownloaderListener> lDnlListenerList;

        synchronized (this) {
            if (mDnlListenerList.size() == 0) {
                return;
            }
            lDnlListenerList = (ArrayList<DownloaderListener>) mDnlListenerList.clone();
        }

        for (DownloaderListener lListener : lDnlListenerList) {
            DOWNLOAD_STATUS lStatus = pDnlEvent.getStatus();
            switch (lStatus) {
            case STARTED:
                lListener.downloadStarted(pDnlEvent);
                output(("trying to download: ").concat(this.sURL));
                break;
            case DOWNLOADING:
                lListener.downloadProgress(pDnlEvent);
                break;
            case FAILED:
                output(("download failed: ").concat("\"").concat(this.getTitle()).concat("\"")); // not downloaded does not mean it was erroneous
                lListener.downloadFailure(pDnlEvent);
                break;
            case COMPLETED:
                lListener.downloadCompleted(pDnlEvent);
                break;
            case COMPLETED_NOT_DOWNLOAD:
                output(("not downloaded: ").concat("\"").concat(this.getTitle()).concat("\"")); // not downloaded does not mean it was erroneous
                lListener.downloadCompletedNotDownloaded(pDnlEvent);
                break;
            case IDLE:
                lListener.threadIdle(pDnlEvent);
                break;
            case ABORTING:
                lListener.threadAborted(pDnlEvent);
            default:
                break;
            }
        }
    }

}