class AlbumMetadata {
        private final String baseURL;
        private final String location;
        private final int sortOrder;
        // cookies for the current page of this album
        private Map<String, String> cookies;
        private Document currPage;
        private int numPages;
        private int pageIndex = 1;

        private AlbumMetadata(JSONObject data) {
            this.baseURL = data.getString("url");
            this.location = data.getString("location")
                                .replace(" ", "_");
            this.sortOrder = data.getInt("sortOrder");
        }

        private String getCurrPageURL(){
            return baseURL + String.format("?sort=%d&page=%d",
                                       sortOrder, pageIndex);
        }
    }

    private final Pattern collDataPattern;
    private final Pattern pbURLPattern;

    // all albums including sub-albums to rip
    private List<AlbumMetadata> albums;
    // the album currently being ripped
    private AlbumMetadata currAlbum;
    // a new index per album downloaded
    private int index = 0;

    public PhotobucketRipper(URL url) throws IOException {
        super(url);
        this.collDataPattern = Pattern.compile(
                "^.*collectionData: (\\{.*}).*$", Pattern.DOTALL
        );
        this.pbURLPattern = Pattern.compile(
                "^https?://([a-zA-Z0-9]+)\\.photobucket\\.com/user/" +
                "([a-zA-Z0-9_\\-]+)/library/([^?]*).*$"
        );
    }

    @Override
    protected String getDomain() {
        return DOMAIN;
    }

    @Override
    public String getHost() {
        return HOST;
    }

    @Override
    public URL sanitizeURL(URL url) throws MalformedURLException {
        LOGGER.info(url);
        String u = url.toExternalForm();
        if (u.contains("?")) {
            // strip options from URL
            u = u.substring(0, u.indexOf("?"));
        }
        if (!u.endsWith("/")) {
            // append trailing slash
            u = u + "/";
        }
        return new URL(u);
    }

    @Override
    public String getGID(URL url) throws MalformedURLException {
        Matcher m;

        URL sanitized = sanitizeURL(url);

        // http://s844.photobucket.com/user/SpazzySpizzy/library/Lady%20Gaga?sort=3&page=1
        m = pbURLPattern.matcher(sanitized.toExternalForm());
        if (m.matches()) {
            // the username is not really a unique GID, because the same user
            // can have multiple albums, but on the other hand, using HOST_GID
            // as save directory means we can group ripped albums of the same
            // user.
            return m.group(2);
        }

        throw new MalformedURLException(
                "Expected photobucket.com gallery formats: "
                        + "http://x###.photobucket.com/username/library/..."
                        + " Got: " + url);
    }



    // Page iteration



    @Override
    public Document getFirstPage() throws IOException {
        if (this.currAlbum == null) {
            this.albums = getAlbumMetadata(this.url.toExternalForm());
            LOGGER.info("Detected " + albums.size() + " albums in total");
        }
        this.currAlbum = this.albums.remove(0);
        // NOTE: Why not just get media count in the metadata json?
        //
        // Because that data might not reflect what the user sees on the page
        // and can lead to iterating more pages than there actually are.
        //
        // An example:
        // Metadata JSON -> AlbumStats: 146 images + 0 videos -> 146 items/7 pages
        // http://s1255.photobucket.com/api/user/mimajki/album/Movie%20gifs/get?subAlbums=48&json=1
        // Actual item count when looking at the album url: 131 items/6 pages
        // http://s1255.photobucket.com/user/mimajki/library/Movie%20gifs?sort=6&page=1
        Connection.Response resp = Http.url(currAlbum.getCurrPageURL()).response();
        this.currAlbum.cookies = resp.cookies();
        this.currAlbum.currPage = resp.parse();
        JSONObject collectionData = getCollectionData(currAlbum.currPage);
        int totalNumItems = collectionData.getInt("total");
        this.currAlbum.numPages = (int) Math.ceil(
                (double)totalNumItems / (double)ITEMS_PER_PAGE);
        this.index = 0;
        return currAlbum.currPage;
    }

    @Override
    public Document getNextPage(Document page) throws IOException {<FILL_FUNCTION_BODY>

        this.currAlbum.pageIndex++;
        boolean endOfAlbum = currAlbum.pageIndex > currAlbum.numPages;
        boolean noMoreSubalbums = albums.isEmpty();
        if (endOfAlbum && noMoreSubalbums){
            throw new IOException("No more pages");
        }
        try {
            Thread.sleep(WAIT_BEFORE_NEXT_PAGE);
        } catch (InterruptedException e) {
            LOGGER.info("Interrupted while waiting before getting next page");
        }
        if (endOfAlbum){
            LOGGER.info("Turning to next album " + albums.get(0).baseURL);
            return getFirstPage();
        } else {
            LOGGER.info("Turning to page " + currAlbum.pageIndex +
                    " of album " + currAlbum.baseURL);
            Connection.Response resp = Http.url(currAlbum.getCurrPageURL()).response();
            currAlbum.cookies = resp.cookies();
            currAlbum.currPage = resp.parse();
            return currAlbum.currPage;
        }
    
<methods>public boolean addURLToDownload(java.net.URL, java.io.File, java.lang.String, Map<java.lang.String,java.lang.String>, java.lang.Boolean) ,public boolean addURLToDownload(java.net.URL, java.io.File) ,public boolean canRip(java.net.URL) ,public void downloadCompleted(java.net.URL, java.io.File) ,public void downloadErrored(java.net.URL, java.lang.String) ,public void downloadExists(java.net.URL, java.io.File) ,public int getCompletionPercentage() ,public int getCount() ,public abstract java.lang.String getHost() ,public Document getNextPage(Document) throws java.io.IOException,public java.lang.String getStatusText() ,public void rip() throws java.io.IOException,public java.net.URL sanitizeURL(java.net.URL) throws java.net.MalformedURLException,public boolean saveText(java.net.URL, java.lang.String, java.lang.String, int) ,public void setWorkingDir(java.net.URL) throws java.io.IOException<variables>private Map<java.net.URL,java.io.File> itemsCompleted,private Map<java.net.URL,java.lang.String> itemsErrored,private Map<java.net.URL,java.io.File> itemsPending