class VideoRipper extends AbstractRipper {

    private int bytesTotal = 1;
    private int bytesCompleted = 1;

    protected VideoRipper(URL url) throws IOException {
        super(url);
    }

    public abstract void rip() throws IOException;

    public abstract String getHost();

    public abstract String getGID(URL url) throws MalformedURLException;

    @Override
    public void setBytesTotal(int bytes) {
        this.bytesTotal = bytes;
    }

    @Override
    public void setBytesCompleted(int bytes) {
        this.bytesCompleted = bytes;
    }

    @Override
    public String getAlbumTitle(URL url) {
        return "videos";
    }

    @Override
    public boolean addURLToDownload(URL url, File saveAs) {
        if (Utils.getConfigBoolean("urls_only.save", false)) {
            // Output URL to file
            String urlFile = this.workingDir + File.separator + "urls.txt";

            try (FileWriter fw = new FileWriter(urlFile, true)) {
                fw.write(url.toExternalForm());
                fw.write("\n");

                RipStatusMessage msg = new RipStatusMessage(STATUS.DOWNLOAD_COMPLETE, urlFile);
                observer.update(this, msg);
            } catch (IOException e) {
                LOGGER.error("Error while writing to " + urlFile, e);
                return false;
            }
        } else {
            if (isThisATest()) {
                // Tests shouldn't download the whole video
                // Just change this.url to the download URL so the test knows we found it.
                LOGGER.debug("Test rip, found URL: " + url);
                this.url = url;
                return true;
            }
            threadPool.addThread(new DownloadVideoThread(url, saveAs, this));
        }
        return true;
    }

    @Override
    public boolean addURLToDownload(URL url, File saveAs, String referrer, Map<String, String> cookies, Boolean getFileExtFromMIME) {
        return addURLToDownload(url, saveAs);
    }

    /**
     * Creates & sets working directory based on URL.
     *
     * @param url Target URL
     */
    @Override
    public void setWorkingDir(URL url) throws IOException {
        String path = Utils.getWorkingDirectory().getCanonicalPath();

        if (!path.endsWith(File.separator)) {
            path += File.separator;
        }

        path += "videos" + File.separator;
        workingDir = new File(path);

        if (!workingDir.exists()) {
            LOGGER.info("[+] Creating directory: " + Utils.removeCWD(workingDir));
            workingDir.mkdirs();
        }

        LOGGER.debug("Set working directory to: " + workingDir);
    }

    /**
     * @return Returns % of video done downloading.
     */
    @Override
    public int getCompletionPercentage() {
        return (int) (100 * (bytesCompleted / (float) bytesTotal));
    }

    /**
     * Runs if download successfully completed.
     *
     * @param url    Target URL
     * @param saveAs Path to file, including filename.
     */
    @Override
    public void downloadCompleted(URL url, File saveAs) {<FILL_FUNCTION_BODY>}

    /**
     * Runs if the download errored somewhere.
     *
     * @param url    Target URL
     * @param reason Reason why the download failed.
     */
    @Override
    public void downloadErrored(URL url, String reason) {
        if (observer == null) {
            return;
        }

        observer.update(this, new RipStatusMessage(STATUS.DOWNLOAD_ERRORED, url + " : " + reason));
        checkIfComplete();
    }

    /**
     * Runs if user tries to redownload an already existing File.
     *
     * @param url  Target URL
     * @param file Existing file
     */
    @Override
    public void downloadExists(URL url, File file) {
        if (observer == null) {
            return;
        }

        observer.update(this, new RipStatusMessage(STATUS.DOWNLOAD_WARN, url + " already saved as " + file));
        checkIfComplete();
    }

    /**
     * Gets the status and changes it to a human-readable form.
     *
     * @return Status of current download.
     */
    @Override
    public String getStatusText() {
        return String.valueOf(getCompletionPercentage()) +
                "%  - " +
                Utils.bytesToHumanReadable(bytesCompleted) +
                " / " +
                Utils.bytesToHumanReadable(bytesTotal);
    }

    /**
     * Sanitizes URL.
     * Usually just returns itself.
     */
    @Override
    public URL sanitizeURL(URL url) throws MalformedURLException {
        return url;
    }

    /**
     * Notifies observers and updates state if all files have been ripped.
     */
    @Override
    protected void checkIfComplete() {
        if (observer == null) {
            return;
        }

        if (bytesCompleted >= bytesTotal) {
            super.checkIfComplete();
        }
    }

}

        if (observer == null) {
            return;
        }

        try {
            String path = Utils.removeCWD(saveAs);
            RipStatusMessage msg = new RipStatusMessage(STATUS.DOWNLOAD_COMPLETE, path);
            observer.update(this, msg);

            checkIfComplete();
        } catch (Exception e) {
            LOGGER.error("Exception while updating observer: ", e);
        }
    
<methods>public void <init>(java.net.URL) throws java.io.IOException,public abstract boolean addURLToDownload(java.net.URL, java.io.File) ,public abstract void downloadCompleted(java.net.URL, java.io.File) ,public abstract void downloadErrored(java.net.URL, java.lang.String) ,public abstract void downloadExists(java.net.URL, java.io.File) ,public java.lang.String getAlbumTitle(java.net.URL) throws java.net.MalformedURLException,public abstract int getCompletionPercentage() ,public static java.lang.String getFileName(java.net.URL, java.lang.String, java.lang.String) ,public abstract java.lang.String getGID(java.net.URL) throws java.net.MalformedURLException,public abstract java.lang.String getHost() ,public static com.rarchives.ripme.ripper.AbstractRipper getRipper(java.net.URL) throws java.lang.Exception,public static List<Constructor<?>> getRipperConstructors(java.lang.String) throws java.lang.Exception,public abstract java.lang.String getStatusText() ,public java.net.URL getURL() ,public java.io.File getWorkingDir() ,public boolean hasASAPRipping() ,public boolean isStopped() ,public void markAsTest() ,public java.lang.String normalizeUrl(java.lang.String) ,public void retrievingSource(java.lang.String) ,public abstract void rip() throws java.io.IOException,public void run() ,public void sendUpdate(com.rarchives.ripme.ui.RipStatusMessage.STATUS, java.lang.Object) ,public void setBytesCompleted(int) ,public void setBytesTotal(int) ,public void setObserver(com.rarchives.ripme.ui.RipStatusHandler) ,public abstract void setWorkingDir(java.net.URL) throws java.io.IOException,public void setup() throws java.io.IOException,public void stop() <variables>protected static final Logger LOGGER,private final java.lang.String URLHistoryFile,public static final java.lang.String USER_AGENT,public int alreadyDownloadedUrls,private boolean completed,com.rarchives.ripme.ui.RipStatusHandler observer,private boolean shouldStop,private static boolean thisIsATest,com.rarchives.ripme.ripper.DownloadThreadPool threadPool,protected java.net.URL url,protected java.io.File workingDir