diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/nb-configuration.xml b/nb-configuration.xml
new file mode 100644
index 0000000..a1e6ac1
--- /dev/null
+++ b/nb-configuration.xml
@@ -0,0 +1,19 @@
+
+
+    
+    
+        
+        true
+        JDK_16
+    
+
diff --git a/nbactions.xml b/nbactions.xml
new file mode 100644
index 0000000..cc17048
--- /dev/null
+++ b/nbactions.xml
@@ -0,0 +1,17 @@
+
+
+        
+            run
+            
+                jar
+            
+            
+                process-classes
+                org.codehaus.mojo:exec-maven-plugin:1.5.0:exec
+            
+            
+                -classpath %classpath com.greinet.tvtotalripper.Main
+                java
+            
+        
+    
diff --git a/pom.xml b/pom.xml
index 1a2a2b7..5f5ba0f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,6 +5,33 @@
     TvTotalRipper
     1.0
     jar
+    
+        
+            org.seleniumhq.selenium
+            selenium-java
+            3.141.59
+        
+        
+            org.apache.logging.log4j
+            log4j-core
+            2.13.0
+        
+        
+            commons-io
+            commons-io
+            2.8.0
+        
+        
+            org.apache.logging.log4j
+            log4j-api
+            2.13.0
+        
+        
+            org.mp4parser
+            isoparser
+            1.9.41
+        
+    
     
         UTF-8
         1.8
diff --git a/resources/chromedriver.exe b/resources/chromedriver.exe
new file mode 100644
index 0000000..d51602d
Binary files /dev/null and b/resources/chromedriver.exe differ
diff --git a/src/main/java/com/greinet/tvtotalripper/Main.java b/src/main/java/com/greinet/tvtotalripper/Main.java
new file mode 100644
index 0000000..5ddcb0e
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/Main.java
@@ -0,0 +1,172 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.greinet.tvtotalripper;
+
+import com.greinet.tvtotalripper.crawler.CrawlerUtil;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.mp4parser.IsoFile;
+import org.mp4parser.boxes.apple.AppleNameBox;
+import org.mp4parser.tools.Path;
+import org.openqa.selenium.By;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+/**
+ *
+ * @author agreiner
+ */
+public class Main {
+    
+    private static final Logger logger = LogManager.getLogger(Main.class);
+
+    
+    public static void main(String[] args) throws InterruptedException, IOException {
+        //System.setProperty("webdriver.chrome.driver", "resources/chromedriver.exe");
+        File f = new File("H:/Users/Andreas/Music/Bass/videoplayback.mp4");
+        
+        MetaDataWriter mdp = new MetaDataWriter();
+        mdp.writeMetadata(f.getAbsolutePath(), "Raab im Dschungel", "Stefan Raab", "TV Total", "TV Total vom 3.2.2021");
+        
+        
+        System.exit(0);
+        
+        
+        
+        System.out.println(CrawlerUtil.getFetchfileURL("https://www.myspass.de/shows/tvshows/tv-total/TV-total-Sendung-vom-08031999--/5716/"));
+        
+        
+        
+        
+        
+        downloadFile(test());
+        
+        
+        
+            
+        ChromeOptions options = new ChromeOptions();
+        options.addArguments("start-maximized");
+        
+        WebDriver driver = new ChromeDriver(options);
+        
+        initialize(driver);
+        
+        List seriesElements = getSeriesElements(driver);
+        //seriesElements.forEach(e -> System.out.println(e.getAttribute("href").replaceAll("https://www.myspass.de/shows/tvshows/", "")));
+        
+        System.out.println("\nTV shows\n");
+        // TV shows
+        List tvshows = seriesElements.stream().filter(e -> e.getAttribute("href").contains("tvshows") || e.getAttribute("href").contains("UNKNOWN")).collect(Collectors.toList());
+        tvshows.forEach(e -> System.out.println(e.getAttribute("href")));
+        
+        System.out.println("\nWebshows\n");
+        List webshows = seriesElements.stream().filter(e -> e.getAttribute("href").contains("webshows")).collect(Collectors.toList());
+        webshows.forEach(e -> System.out.println(e.getAttribute("href")));
+        
+        
+        tvshows.get(3).click();
+        Thread.sleep(3000);
+        navigateToEpisodeOverview(driver);
+        Thread.sleep(3000);
+        getSeasonElements(driver).forEach(e -> System.out.println(e.getText()));
+        Thread.sleep(3000);
+        System.out.println("-------------------------------------------------------------------------------------------------------------------------------------");
+        getEpisodeElements(driver).forEach(e -> System.out.println(e.getAttribute("href")));
+    }
+    
+    private static boolean clickWhenClickable(WebDriver driver, By by, int timeout){
+        WebDriverWait wait = new WebDriverWait(driver, timeout);
+        try{
+            WebElement acceptCookiesButton = wait.until(ExpectedConditions.elementToBeClickable(by));
+            acceptCookiesButton.click();
+        }catch(TimeoutException ex){
+            logger.warn("Element represented by ["+by+"] not clickable.");
+            return false;
+        }
+        return true;
+    }
+    
+    private static void initialize(WebDriver driver){
+        //Load start page
+        //String urlStartPage = "https://www.myspass.de/shows/tvshows/tv-total/#bob-subnavi";
+        String urlStartPage = "https://www.myspass.de/sendungen-a-bis-z/";
+        logger.info("Loading start page ["+urlStartPage+"].");
+        driver.get(urlStartPage);
+        
+        // Accept cookies if needed
+        logger.info("Accepting cookies.");
+        boolean cookiesSuccess = clickWhenClickable(driver, By.id("cmpbntyestxt"),5);
+        if(!cookiesSuccess){
+            logger.info("No cookie popup present.");
+        }
+    }
+    
+    private static boolean navigateToEpisodeOverview(WebDriver driver){
+        logger.info("Navigating to episode overview.");
+        return clickWhenClickable(driver, By.xpath("/html/body/div[4]/div[1]/div[2]/ul/li[2]/a"),5);
+    }
+    
+    private static List getSeriesElements(WebDriver driver){
+        return driver.findElements(By.xpath("/html/body/div[5]/div/div/div/div/a"));
+    }
+    
+    private static List getSeasonElements(WebDriver driver){
+        return driver.findElements(By.xpath("/html/body/div[4]/div[1]/div[3]/div[2]/div[1]/select/option"));
+    }
+    
+    private static List getEpisodeElements(WebDriver driver){
+        return driver.findElements(By.xpath("/html/body/div[4]/div[1]/div[3]/div[2]/div[4]/div/div/div/a"));
+    }
+    
+    private static String test(){
+        WebDriver driver = new ChromeDriver();
+        driver.get("https://de.fetchfile.net/herunterladen-von-myspass/");
+        
+        WebElement textbox = driver.findElement(By.id("videoPath"));
+        textbox.sendKeys("https://www.myspass.de/shows/tvshows/tv-total/TV-total-Sendung-vom-05012015--/20674/");
+        
+        WebElement dlButton = driver.findElement(By.id("home-submit"));
+        dlButton.click();
+        
+        WebDriverWait wait = new WebDriverWait(driver, 20);
+        WebElement vidButton = wait.until(ExpectedConditions.elementToBeClickable(By.className("download-link")));
+        return vidButton.getAttribute("href");
+        
+    }
+    
+    
+    private static void downloadFile(String urlString){
+        try {        
+            File f = new File("download.mp4");
+            f.createNewFile();
+            URL url = new URL(urlString);
+            ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
+            FileOutputStream fileOutputStream = new FileOutputStream(f);
+            fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
+        } catch (MalformedURLException ex) {
+            
+        } catch (IOException ex) {
+            java.util.logging.Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        
+    }
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/MetaDataParser.java b/src/main/java/com/greinet/tvtotalripper/MetaDataParser.java
new file mode 100644
index 0000000..3cb2ff2
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/MetaDataParser.java
@@ -0,0 +1,189 @@
+package com.greinet.tvtotalripper;
+
+
+import org.mp4parser.Box;
+import org.mp4parser.Container;
+import org.mp4parser.IsoFile;
+import org.mp4parser.boxes.apple.AppleItemListBox;
+import org.mp4parser.boxes.apple.AppleNameBox;
+import org.mp4parser.boxes.iso14496.part12.*;
+import org.mp4parser.tools.Path;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.util.List;
+
+/**
+ * Change metadata and make sure chunkoffsets are corrected.
+ */
+public class MetaDataParser {
+
+    public FileChannel splitFileAndInsert(File f, long pos, long length) throws IOException {
+        FileChannel read = new RandomAccessFile(f, "r").getChannel();
+        File tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert");
+        FileChannel tmpWrite = new RandomAccessFile(tmp, "rw").getChannel();
+        read.position(pos);
+        tmpWrite.transferFrom(read, 0, read.size() - pos);
+        read.close();
+        FileChannel write = new RandomAccessFile(f, "rw").getChannel();
+        write.position(pos + length);
+        tmpWrite.position(0);
+        long transferred = 0;
+        while ((transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)) != tmpWrite.size()) {
+            System.out.println(transferred);
+        }
+        System.out.println(transferred);
+        tmpWrite.close();
+        tmp.delete();
+        return write;
+    }
+
+
+    private boolean needsOffsetCorrection(IsoFile isoFile) {
+        if (Path.getPath(isoFile, "moov[0]/mvex[0]") != null) {
+            // Fragmented files don't need a correction
+            return false;
+        } else {
+            // no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
+            for (Box box : isoFile.getBoxes()) {
+                if ("moov".equals(box.getType())) {
+                    return true;
+                }
+                if ("mdat".equals(box.getType())) {
+                    return false;
+                }
+            }
+            throw new RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense");
+        }
+    }
+
+    public void writeRandomMetadata(String videoFilePath, String title) throws IOException {
+
+        File videoFile = new File(videoFilePath);
+        if (!videoFile.exists()) {
+            throw new FileNotFoundException("File " + videoFilePath + " not exists");
+        }
+
+        if (!videoFile.canWrite()) {
+            throw new IllegalStateException("No write permissions to file " + videoFilePath);
+        }
+        IsoFile isoFile = new IsoFile(videoFilePath);
+
+        MovieBox moov = isoFile.getBoxes(MovieBox.class).get(0);
+        FreeBox freeBox = findFreeBox(moov);
+
+        boolean correctOffset = needsOffsetCorrection(isoFile);
+        long sizeBefore = moov.getSize();
+        long offset = 0;
+        for (Box box : isoFile.getBoxes()) {
+            if ("moov".equals(box.getType())) {
+                break;
+            }
+            offset += box.getSize();
+        }
+
+        // Create structure or just navigate to Apple List Box.
+        UserDataBox userDataBox;
+        if ((userDataBox = Path.getPath(moov, "udta")) == null) {
+            userDataBox = new UserDataBox();
+            moov.addBox(userDataBox);
+        }
+        MetaBox metaBox;
+        if ((metaBox = Path.getPath(userDataBox, "meta")) == null) {
+            metaBox = new MetaBox();
+            HandlerBox hdlr = new HandlerBox();
+            hdlr.setHandlerType("mdir");
+            metaBox.addBox(hdlr);
+            userDataBox.addBox(metaBox);
+        }
+        AppleItemListBox ilst;
+        if ((ilst = Path.getPath(metaBox, "ilst")) == null) {
+            ilst = new AppleItemListBox();
+            metaBox.addBox(ilst);
+
+        }
+        if (freeBox == null) {
+            freeBox = new FreeBox(128 * 1024);
+            metaBox.addBox(freeBox);
+        }
+        // Got Apple List Box
+
+        AppleNameBox nam;
+        if ((nam = Path.getPath(ilst, "©nam")) == null) {
+            nam = new AppleNameBox();
+        }
+        nam.setDataCountry(0);
+        nam.setDataLanguage(0);
+        nam.setValue(title);
+        ilst.addBox(nam);
+
+        long sizeAfter = moov.getSize();
+        long diff = sizeAfter - sizeBefore;
+        // This is the difference of before/after
+
+        // can we compensate by resizing a Free Box we have found?
+        if (freeBox.getData().limit() > diff) {
+            // either shrink or grow!
+            freeBox.setData(ByteBuffer.allocate((int) (freeBox.getData().limit() - diff)));
+            sizeAfter = moov.getSize();
+            diff = sizeAfter - sizeBefore;
+        }
+        if (correctOffset && diff != 0) {
+            correctChunkOffsets(moov, diff);
+        }
+        BetterByteArrayOutputStream baos = new BetterByteArrayOutputStream();
+        moov.getBox(Channels.newChannel(baos));
+        isoFile.close();
+        FileChannel fc;
+        if (diff != 0) {
+            // this is not good: We have to insert bytes in the middle of the file
+            // and this costs time as it requires re-writing most of the file's data
+            fc = splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore);
+        } else {
+            // simple overwrite of something with the file
+            fc = new RandomAccessFile(videoFile, "rw").getChannel();
+        }
+        fc.position(offset);
+        fc.write(ByteBuffer.wrap(baos.getBuffer(), 0, baos.size()));
+        fc.close();
+    }
+
+    FreeBox findFreeBox(Container c) {
+        for (Box box : c.getBoxes()) {
+            System.err.println(box.getType());
+            if (box instanceof FreeBox) {
+                return (FreeBox) box;
+            }
+            if (box instanceof Container) {
+                FreeBox freeBox = findFreeBox((Container) box);
+                if (freeBox != null) {
+                    return freeBox;
+                }
+            }
+        }
+        return null;
+    }
+
+    private void correctChunkOffsets(MovieBox movieBox, long correction) {
+        List chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]");
+        if (chunkOffsetBoxes.isEmpty()) {
+            chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]");
+        }
+        for (ChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
+            long[] cOffsets = chunkOffsetBox.getChunkOffsets();
+            for (int i = 0; i < cOffsets.length; i++) {
+                cOffsets[i] += correction;
+            }
+        }
+    }
+
+    private static class BetterByteArrayOutputStream extends ByteArrayOutputStream {
+        byte[] getBuffer() {
+            return buf;
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/greinet/tvtotalripper/MetaDataWriter.java b/src/main/java/com/greinet/tvtotalripper/MetaDataWriter.java
new file mode 100644
index 0000000..f8bca9d
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/MetaDataWriter.java
@@ -0,0 +1,231 @@
+package com.greinet.tvtotalripper;
+
+
+import org.mp4parser.Box;
+import org.mp4parser.Container;
+import org.mp4parser.IsoFile;
+import org.mp4parser.boxes.apple.AppleItemListBox;
+import org.mp4parser.boxes.apple.AppleNameBox;
+import org.mp4parser.boxes.iso14496.part12.*;
+import org.mp4parser.tools.Path;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.util.List;
+import org.mp4parser.boxes.apple.AppleArtistBox;
+import org.mp4parser.boxes.apple.AppleTVEpisodeBox;
+import org.mp4parser.boxes.apple.AppleTVEpisodeNumberBox;
+import org.mp4parser.boxes.apple.AppleTVSeasonBox;
+import org.mp4parser.boxes.apple.AppleTVShowBox;
+
+/**
+ * Change metadata and make sure chunkoffsets are corrected.
+ */
+public class MetaDataWriter {
+
+    private FileChannel splitFileAndInsert(File f, long pos, long length) throws IOException {
+        FileChannel read = new RandomAccessFile(f, "r").getChannel();
+        File tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert");
+        FileChannel tmpWrite = new RandomAccessFile(tmp, "rw").getChannel();
+        read.position(pos);
+        tmpWrite.transferFrom(read, 0, read.size() - pos);
+        read.close();
+        FileChannel write = new RandomAccessFile(f, "rw").getChannel();
+        write.position(pos + length);
+        tmpWrite.position(0);
+        long transferred = 0;
+        while ((transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)) != tmpWrite.size()) {
+        }
+        tmpWrite.close();
+        tmp.delete();
+        return write;
+    }
+
+
+    private boolean needsOffsetCorrection(IsoFile isoFile) {
+        if (Path.getPath(isoFile, "moov[0]/mvex[0]") != null) {
+            // Fragmented files don't need a correction
+            return false;
+        } else {
+            // no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
+            for (Box box : isoFile.getBoxes()) {
+                if ("moov".equals(box.getType())) {
+                    return true;
+                }
+                if ("mdat".equals(box.getType())) {
+                    return false;
+                }
+            }
+            throw new RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense");
+        }
+    }
+
+    public void writeMetadata(String videoFilePath, String title, String artist, String show, String episode) throws IOException {
+
+        File videoFile = new File(videoFilePath);
+        if (!videoFile.exists()) {
+            throw new FileNotFoundException("File " + videoFilePath + " not exists");
+        }
+
+        if (!videoFile.canWrite()) {
+            throw new IllegalStateException("No write permissions to file " + videoFilePath);
+        }
+        IsoFile isoFile = new IsoFile(videoFilePath);
+
+        MovieBox moov = isoFile.getBoxes(MovieBox.class).get(0);
+        FreeBox freeBox = findFreeBox(moov);
+
+        boolean correctOffset = needsOffsetCorrection(isoFile);
+        long sizeBefore = moov.getSize();
+        long offset = 0;
+        for (Box box : isoFile.getBoxes()) {
+            if ("moov".equals(box.getType())) {
+                break;
+            }
+            offset += box.getSize();
+        }
+
+        // Create structure or just navigate to Apple List Box.
+        UserDataBox userDataBox;
+        if ((userDataBox = Path.getPath(moov, "udta")) == null) {
+            userDataBox = new UserDataBox();
+            moov.addBox(userDataBox);
+        }
+        MetaBox metaBox;
+        if ((metaBox = Path.getPath(userDataBox, "meta")) == null) {
+            metaBox = new MetaBox();
+            HandlerBox hdlr = new HandlerBox();
+            hdlr.setHandlerType("mdir");
+            metaBox.addBox(hdlr);
+            userDataBox.addBox(metaBox);
+        }
+        AppleItemListBox ilst;
+        if ((ilst = Path.getPath(metaBox, "ilst")) == null) {
+            ilst = new AppleItemListBox();
+            metaBox.addBox(ilst);
+
+        }
+        if (freeBox == null) {
+            freeBox = new FreeBox(128 * 1024);
+            metaBox.addBox(freeBox);
+        }
+        
+        if(title != null){
+            // Write the title
+            AppleNameBox nam;
+            if ((nam = Path.getPath(ilst, "©nam")) == null) {
+                nam = new AppleNameBox();
+            }
+            nam.setDataCountry(0);
+            nam.setDataLanguage(0);
+            nam.setValue(title);
+            ilst.addBox(nam);
+        }
+
+        if(artist!=null){
+            // Write the artist
+            AppleArtistBox art;
+            if ((art = Path.getPath(ilst, "©ART")) == null) {
+                art = new AppleArtistBox();
+            }
+            art.setDataCountry(0);
+            art.setDataLanguage(0);
+            art.setValue(artist);
+            ilst.addBox(art);
+        }
+
+        if(episode!=null){
+            // Write the episode title
+            AppleTVEpisodeNumberBox ep;
+            if ((ep = Path.getPath(ilst, "tven")) == null) {
+                ep = new AppleTVEpisodeNumberBox();
+            }
+            ep.setDataCountry(0);
+            ep.setDataLanguage(0);
+            ep.setValue(episode);
+            ilst.addBox(ep);
+        }
+
+        if(show!=null){
+            //Write the show
+            AppleTVShowBox sh;
+            if ((sh = Path.getPath(ilst, "tvsh")) == null) {
+                sh = new AppleTVShowBox();
+            }
+            sh.setDataCountry(0);
+            sh.setDataLanguage(0);
+            sh.setValue(show);
+            ilst.addBox(sh); 
+        }
+
+        
+        
+        long sizeAfter = moov.getSize();
+        long diff = sizeAfter - sizeBefore;
+        // This is the difference of before/after
+
+        // can we compensate by resizing a Free Box we have found?
+        if (freeBox.getData().limit() > diff) {
+            // either shrink or grow!
+            freeBox.setData(ByteBuffer.allocate((int) (freeBox.getData().limit() - diff)));
+            sizeAfter = moov.getSize();
+            diff = sizeAfter - sizeBefore;
+        }
+        if (correctOffset && diff != 0) {
+            correctChunkOffsets(moov, diff);
+        }
+        BetterByteArrayOutputStream baos = new BetterByteArrayOutputStream();
+        moov.getBox(Channels.newChannel(baos));
+        isoFile.close();
+        FileChannel fc;
+        if (diff != 0) {
+            // this is not good: We have to insert bytes in the middle of the file
+            // and this costs time as it requires re-writing most of the file's data
+            fc = splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore);
+        } else {
+            // simple overwrite of something with the file
+            fc = new RandomAccessFile(videoFile, "rw").getChannel();
+        }
+        fc.position(offset);
+        fc.write(ByteBuffer.wrap(baos.getBuffer(), 0, baos.size()));
+        fc.close();
+    }
+
+    FreeBox findFreeBox(Container c) {
+        for (Box box : c.getBoxes()) {
+            if (box instanceof FreeBox) {
+                return (FreeBox) box;
+            }
+            if (box instanceof Container) {
+                FreeBox freeBox = findFreeBox((Container) box);
+                if (freeBox != null) {
+                    return freeBox;
+                }
+            }
+        }
+        return null;
+    }
+
+    private void correctChunkOffsets(MovieBox movieBox, long correction) {
+        List chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]");
+        if (chunkOffsetBoxes.isEmpty()) {
+            chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]");
+        }
+        for (ChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
+            long[] cOffsets = chunkOffsetBox.getChunkOffsets();
+            for (int i = 0; i < cOffsets.length; i++) {
+                cOffsets[i] += correction;
+            }
+        }
+    }
+
+    private static class BetterByteArrayOutputStream extends ByteArrayOutputStream {
+        byte[] getBuffer() {
+            return buf;
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/greinet/tvtotalripper/SwingInterface.java b/src/main/java/com/greinet/tvtotalripper/SwingInterface.java
new file mode 100644
index 0000000..06884e3
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/SwingInterface.java
@@ -0,0 +1,77 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.greinet.tvtotalripper;
+
+import java.awt.Dimension;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JProgressBar;
+
+/**
+ *
+ * @author agreiner
+ */
+public class SwingInterface implements PropertyChangeListener {
+    
+    private final JFrame frame;
+    private final JLabel label;
+    private final JProgressBar progressBar;
+    
+    private List changeListener = new ArrayList<>();
+    
+    public SwingInterface(){
+        
+        frame = new JFrame("TV Total Ripper");
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        
+        label = new JLabel("Testlabel");
+        label.setPreferredSize(new Dimension(200, 30));
+        
+        progressBar = new JProgressBar(0, 100);
+        progressBar.setPreferredSize(new Dimension(200, 30));
+        progressBar.setStringPainted(true);
+        
+        
+        frame.add(label);
+        frame.add(progressBar);
+        
+        
+        frame.pack();
+        frame.setVisible(true);
+    }
+    
+    public static void main(String[] args) {
+        
+        javax.swing.SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                SwingInterface i = new SwingInterface();
+                i.startDownload();
+            }
+        });
+        
+    }
+
+    public void startDownload(){
+        //DownloadTask task = new DownloadTask(label, "https://cldf-od.r53.cdn.tv1.eu/secdl/06d6d246daa2c7ec0ffb2f8281149072/6066001f/11021brainpool/ondemand/3583brainpool/163840/myspass2009/11/33/2171/9642/9642_61.mp4", "");
+        //task.addPropertyChangeListener(this);
+        //task.execute();
+    }
+    
+    
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+         if (evt.getPropertyName().equals("progress1")) {
+            int progress = (Integer) evt.getNewValue();
+            progressBar.setValue(progress);
+        }
+    }
+    
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/crawler/CrawlerUtil.java b/src/main/java/com/greinet/tvtotalripper/crawler/CrawlerUtil.java
new file mode 100644
index 0000000..7ee1ac9
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/crawler/CrawlerUtil.java
@@ -0,0 +1,179 @@
+package com.greinet.tvtotalripper.crawler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+/**
+ * Helper class to get show, season und episode elements from the website
+ * @author agreiner
+ */
+public class CrawlerUtil {
+    
+    static{
+        System.setProperty("webdriver.chrome.driver", "resources/chromedriver.exe");
+    }
+    
+    /** The logger */
+    private static final Logger logger = LogManager.getLogger(CrawlerUtil.class);
+    /** The WebDriver for show information */
+    private final static WebDriver driverShows = createShowDriver();
+    /** The WebDriver for season and episode information */
+    private final static WebDriver driverSeasonsAndEpisodes = createChromeDriver();
+    /** The WebDriver for fetchfile.net URL conversion */
+    private final static WebDriver driverFetchFile = createFetchFileDriver();
+    
+    public static String show = "";
+    /**
+     * Create a basic ChromeDriver
+     * @return 
+     */
+    private static WebDriver createChromeDriver(){
+        ChromeOptions options = new ChromeOptions();
+        options.addArguments("start-maximized");
+        return new ChromeDriver(options);
+    }
+    
+    /**
+     * Create the WebDriver to get show information
+     * @return the WebDriver
+     */
+    private static WebDriver createShowDriver(){
+        WebDriver driver = createChromeDriver();
+        driver.get("https://www.myspass.de/sendungen-a-bis-z/");
+        return driver;
+    }
+    
+    private static WebDriver createFetchFileDriver(){
+        WebDriver driver = createChromeDriver();
+        driver.get("https://de.fetchfile.net/herunterladen-von-myspass");
+        return driver;
+    }
+    
+    /**
+     * Navigate to the given URL or do nothing, if already at that URL
+     * @param url the URL
+     */
+    private static void navigateToShow(String url){
+        if(!driverSeasonsAndEpisodes.getCurrentUrl().equals(url)){
+            driverSeasonsAndEpisodes.get(url);
+        }
+        driverSeasonsAndEpisodes.findElement(By.xpath("/html/body/div[4]/div[1]/div[2]/ul/li[2]/a")).click();
+    }
+    
+    /**
+     * Returns the WebElements representing the shows.
+     * @return the show WebElements
+     */
+    public static List getShows(){
+        return driverShows.findElements(By.xpath("/html/body/div[5]/div/div/div/div/a"));
+    }
+    
+    /**
+     * Returns the Webelements represeting the seasons of a specific show
+     * @param url the URL to the show
+     * @return the season WebElements
+     */
+    public static List getSeasons(String url){
+        navigateToShow(url);
+        
+        String urlNotTrailingSlash = url.substring(0,url.lastIndexOf("/"));
+        
+        show = urlNotTrailingSlash.substring(urlNotTrailingSlash.lastIndexOf("/")+1, urlNotTrailingSlash.length());
+        return driverSeasonsAndEpisodes.findElements(By.xpath("/html/body/div[4]/div[1]/div[3]/div[2]/div[1]/select/option"));
+        
+    }
+    
+    /**
+     * Returns the information from the episodes from a specific show and season packed into a wrapper class
+     * @param url the URL of the show
+     * @param seasonName the name of the season
+     * @return the list of episodes
+     */
+    public static List getEpisodes(String url, String seasonName){
+        
+        // navigate the WebDriver to the specified URL
+        navigateToShow(url);
+        
+        // Search the specified season
+        List elements = driverSeasonsAndEpisodes.findElements(By.xpath("/html/body/div[4]/div[1]/div[3]/div[2]/div[1]/select/option"));
+        Optional seasonElement = elements.stream().filter(e -> e.getText().equals(seasonName)).findFirst();
+        
+        // If the season was found, navigate there
+        if(seasonElement.isPresent()){
+            seasonElement.get().click();
+            
+            // Wait for the website to load and then switch to the list view
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException ex) {
+                logger.log(Level.ERROR, "Could not find list view button for url:[{0}], season:[{1}]",url, seasonName);
+            }
+            driverSeasonsAndEpisodes.findElement(By.className("listView--toggle_input")).click();
+            
+            // Get the parent div of the episode information list
+            List episodeListDivs = driverSeasonsAndEpisodes.findElements(By.xpath("/html/body/div[4]/div[1]/div[3]/div[2]/div"));
+            List filteredNoDisplay = episodeListDivs.stream().filter(e -> !"none".equals(e.getCssValue("display"))).collect(Collectors.toList());
+            
+            
+            if(!filteredNoDisplay.isEmpty()){
+                // Get the Webelements with the link and title information
+                List linksToEpisodes = new ArrayList<>();
+                filteredNoDisplay.forEach(e -> 
+                    linksToEpisodes.addAll(e.findElements(By.xpath(".//div/table/tbody/tr/td/a")))
+                );
+                // Get the WebElements with the duration information
+                List durationOfEpisodes = new ArrayList<>();
+                filteredNoDisplay.forEach(e -> 
+                    durationOfEpisodes.addAll(e.findElements(By.xpath(".//div/table/tbody/tr/td[3]")))
+                );
+                // Put the information into episode wrappers and return the list
+                List wrappers = new ArrayList<>();
+                for(int i=0;i();
+            }
+        }else{
+            logger.log(Level.ERROR, "No seaons found for url:[{0}], season:[{1}]",url, seasonName);
+            return new ArrayList<>();
+        }
+    }
+    
+    public static String getFetchfileURL(String myspassURL){
+        synchronized(driverFetchFile){
+            WebElement videoPathInput = driverFetchFile.findElement(By.id("videoPath"));
+            videoPathInput.sendKeys(myspassURL);
+            WebElement submitButton = driverFetchFile.findElement(By.id("home-submit"));
+            submitButton.click();
+            WebDriverWait wait = new WebDriverWait(driverFetchFile, 20);
+            WebElement downloadButton = wait.until(ExpectedConditions.elementToBeClickable(By.className("download-link")));
+            String downloadLink = downloadButton.getAttribute("href");
+            driverFetchFile.get("https://de.fetchfile.net/herunterladen-von-myspass");
+
+            // Close advertisement tabs
+            List tabs = new ArrayList<>(driverFetchFile.getWindowHandles());
+            for (int i=1;i 0) {
+                    fileName = disposition.substring(index + 10,
+                            disposition.length() - 1);
+                }
+            } else {
+                // extracts file name from URL
+                fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1,
+                        fileUrl.length());
+            }
+ 
+            // output for debugging purpose only
+            System.out.println("Content-Type = " + contentType);
+            System.out.println("Content-Disposition = " + disposition);
+            System.out.println("Content-Length = " + contentLength);
+            System.out.println("fileName = " + fileName);
+ 
+            // opens input stream from the HTTP connection
+            inputStream = httpConn.getInputStream();
+ 
+        } else {
+            throw new IOException(
+                    "No file to download. Server replied HTTP code: "
+                            + responseCode);
+        }
+    }
+    
+    public void disconnect() throws IOException {
+        inputStream.close();
+        httpConn.disconnect();
+    }
+ 
+    public String getFileName() {
+        return this.fileName;
+    }
+ 
+    public int getContentLength() {
+        return this.contentLength;
+    }
+ 
+    public InputStream getInputStream() {
+        return this.inputStream;
+    }
+    
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/download/DownloadTask.java b/src/main/java/com/greinet/tvtotalripper/download/DownloadTask.java
new file mode 100644
index 0000000..edd85b8
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/download/DownloadTask.java
@@ -0,0 +1,95 @@
+package com.greinet.tvtotalripper.download;
+
+import com.greinet.tvtotalripper.MetaDataWriter;
+import com.greinet.tvtotalripper.crawler.CrawlerUtil;
+import com.greinet.tvtotalripper.crawler.EpisodeWrapper;
+import com.greinet.tvtotalripper.ui.SettingsRipperPanel;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import javax.swing.SwingWorker;
+
+/**
+ *
+ * @author agreiner
+ */
+public class DownloadTask extends SwingWorker {
+    private static final int BUFFER_SIZE = 4096;
+    
+    private final EpisodeWrapper episodeWrapper;
+    
+     
+    public DownloadTask(EpisodeWrapper episodeWrapper) {
+        this.episodeWrapper = episodeWrapper;
+    }
+
+    public EpisodeWrapper getEpisodeWrapper() {
+        return episodeWrapper;
+    }
+
+    @Override
+    public String toString() {
+        return episodeWrapper.getTitle();
+    }
+    
+    public long fileSize = 0;
+    
+    public long totalBytesRead = 0;
+    
+    public int percentCompleted = 0;
+    
+    /**
+     * Executed in background thread
+     */
+    @Override
+    protected Void doInBackground() throws Exception {
+        try {
+            String downloadURL = CrawlerUtil.getFetchfileURL(episodeWrapper.getUrl());
+            
+            ConnectionUtil util = new ConnectionUtil();
+            util.prepare(downloadURL);
+             
+            InputStream inputStream = util.getInputStream();
+            
+            String fixedTitle = episodeWrapper.getTitle().replace(":", " ");
+            
+            File outputFile = new File(SettingsRipperPanel.DOWNLOADFOLDER, fixedTitle+".mp4");
+            FileOutputStream outputStream = new FileOutputStream(outputFile);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int bytesRead = -1;
+            totalBytesRead = 0;
+            percentCompleted = 0;
+            int oldPercentCompleted = 0;
+            fileSize = util.getContentLength();
+ 
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+                totalBytesRead += bytesRead;
+                percentCompleted = (int) (totalBytesRead * 100 / fileSize);
+                firePropertyChange(Long.toString(episodeWrapper.getId()), oldPercentCompleted, percentCompleted);
+                oldPercentCompleted = percentCompleted;
+                //setProgress(percentCompleted);
+            }
+ 
+            outputStream.close();
+ 
+            util.disconnect();
+            
+            MetaDataWriter mdp = new MetaDataWriter();
+            mdp.writeMetadata(outputFile.getAbsolutePath(), episodeWrapper.getTitle(), episodeWrapper.getShow(), episodeWrapper.getShow(), episodeWrapper.getEpisode());
+            
+        } catch (IOException ex) {
+            cancel(true);          
+        }
+        return null;
+    }
+ 
+    /**
+     * Executed in Swing's event dispatching thread
+     */
+    @Override
+    protected void done() {
+        firePropertyChange(Long.toString(episodeWrapper.getId()), 0, 101);
+    }  
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/BaseRipperPanel.java b/src/main/java/com/greinet/tvtotalripper/ui/BaseRipperPanel.java
new file mode 100644
index 0000000..cf48cc7
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/BaseRipperPanel.java
@@ -0,0 +1,116 @@
+package com.greinet.tvtotalripper.ui;
+
+import java.awt.Color;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.event.ListSelectionEvent;
+
+/**
+ *
+ * @author agreiner
+ */
+public class BaseRipperPanel {
+    
+    private final JPanel panel;
+    private JList listPanel;
+    private JTextField textField;
+    private final JButton button;
+    private final JLabel label;
+    private Map elements;
+    
+    public BaseRipperPanel(Map elements){
+        this.elements = elements;
+        
+        if(elements.isEmpty()){
+            elements.put("Dummy-Key", "Dummy-URL");
+        }
+        
+        panel = new JPanel(false);
+        panel.setLayout(new GridBagLayout());
+        
+        GridBagConstraints c = new GridBagConstraints();
+        c.gridx = 0;
+        c.gridy = 0;
+        c.gridwidth = 3;
+        c.fill = GridBagConstraints.BOTH;
+        c.weightx = 0.75;
+        
+        textField = new JTextField();
+        textField.setEditable(false);
+        panel.add(textField,c);
+        
+        button = new JButton("Select");
+        c.gridx = 3;
+        c.gridwidth = 2;
+        c.weightx = 0.25;
+        
+        panel.add(button,c);
+
+        
+        List names = new ArrayList<>(elements.keySet());
+        Collections.sort(names);
+        
+        listPanel = new JList<>(names.toArray(new String[elements.keySet().size()]));
+        JScrollPane scrollPane = new JScrollPane();
+        scrollPane.setViewportView(listPanel);
+        listPanel.setLayoutOrientation(JList.VERTICAL);
+        listPanel.setSelectedIndex(0);
+        
+        listPanel.addListSelectionListener((ListSelectionEvent e) -> {
+            textField.setText(listPanel.getSelectedValue());
+        });
+        textField.setText(listPanel.getSelectedValue());
+        
+        c.gridx = 0;
+        c.gridy = 1;
+        c.gridwidth = 5;
+        c.gridheight = 5;
+        c.weightx = 0;
+        
+        panel.add(scrollPane,c);
+        
+        label = new JLabel(Integer.toString(elements.keySet().size()));
+        label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+        
+        
+        c.gridx = 4;
+        c.gridy = 6;
+        c.gridwidth = 1;
+        c.gridheight = 1;
+        c.fill = GridBagConstraints.NONE;
+        c.anchor = GridBagConstraints.EAST;
+        
+        panel.add(label, c);
+        
+        
+        
+    }
+    
+    public JPanel getJPanel(){
+        return panel;
+    }
+    
+    public JButton getJButton(){
+        return button;
+    }
+    
+    public String getCurrentSelected(){
+        return elements.get(listPanel.getSelectedValue());
+    }
+    
+    public JList getJList(){
+        return listPanel;
+    }
+    
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/DownloadRipperPanel.java b/src/main/java/com/greinet/tvtotalripper/ui/DownloadRipperPanel.java
new file mode 100644
index 0000000..a789cba
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/DownloadRipperPanel.java
@@ -0,0 +1,143 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.greinet.tvtotalripper.ui;
+
+import com.greinet.tvtotalripper.download.DownloadTask;
+import java.awt.Color;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import javax.swing.BorderFactory;
+import javax.swing.DefaultListModel;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+
+/**
+ *
+ * @author agreiner
+ */
+public class DownloadRipperPanel implements PropertyChangeListener{
+    
+    private final JPanel panel;
+    private JList listTasks;
+    private DefaultListModel listModel;
+    private JTextField textSelectedTask;
+    private final JLabel labelTaskCount;
+    
+    private final List downloadTasks;
+    
+    private DownloadTaskInformationPanel infoPanel;
+    
+    public DownloadRipperPanel(){
+        
+        downloadTasks = new ArrayList<>();
+        
+        panel = new JPanel();
+        panel.setLayout(new GridBagLayout());
+        
+        GridBagConstraints c = new GridBagConstraints();
+        
+        c.gridx = 0;
+        c.gridy = 0;
+        c.gridwidth = 5;
+        c.fill = GridBagConstraints.BOTH;
+        c.weightx = 0.5;
+        textSelectedTask = new JTextField();
+        textSelectedTask.setEditable(false);
+        panel.add(textSelectedTask, c);
+        
+        
+        listModel = new DefaultListModel<>();
+        listTasks = new JList<>(listModel);
+        listTasks.setSelectedIndex(ListSelectionModel.SINGLE_SELECTION);
+        JScrollPane scrollPane = new JScrollPane();
+        scrollPane.setViewportView(listTasks);
+        listTasks.setLayoutOrientation(JList.VERTICAL);
+        
+        c.gridx = 0;
+        c.gridy = 1;
+        c.gridwidth = 5;
+        c.gridheight = 5;
+        c.weightx = 0.5;
+        panel.add(scrollPane, c);
+        
+        infoPanel = new DownloadTaskInformationPanel();
+        c.gridx = 5;
+        c.gridy = 0;
+        c.gridwidth = 5;
+        c.weightx = 0.5;
+        c.fill = GridBagConstraints.BOTH;
+        panel.add(infoPanel, c);
+        
+        listTasks.addListSelectionListener((ListSelectionEvent e) -> {
+            if(listTasks.getSelectedValue()!=null){
+                textSelectedTask.setText(listTasks.getSelectedValue().toString());
+                infoPanel.setInformation(listTasks.getSelectedValue());
+                infoPanel.selectedDownloadTask = listTasks.getSelectedValue();
+            }
+        });
+        
+        
+        labelTaskCount = new JLabel(Integer.toString(listTasks.getModel().getSize()));
+        labelTaskCount.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+        
+        c.gridx = 4;
+        c.gridy = 6;
+        c.gridwidth = 1;
+        c.gridheight = 1;
+        c.fill = GridBagConstraints.NONE;
+        c.anchor = GridBagConstraints.EAST;
+        panel.add(labelTaskCount, c);
+    }
+    
+    public JPanel getJPanel(){
+        return panel;
+    }
+    
+    public DownloadTask getSelectedTask(){
+        return listTasks.getSelectedValue();
+    }
+
+    public void addTask(DownloadTask task){
+        downloadTasks.add(task);
+        task.execute();
+        listModel.addElement(task);
+        updateTaskCount();
+        task.addPropertyChangeListener(this);
+    }
+    
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if(evt.getNewValue().equals(101)){
+            Optional taskToRemove = downloadTasks.stream().filter(t -> Long.toString(t.getEpisodeWrapper().getId()).equals(evt.getPropertyName())).findFirst();
+            if(taskToRemove.isPresent()){
+                if(listTasks.getSelectedValue() != null && taskToRemove.get().equals(listTasks.getSelectedValue())){
+                    infoPanel.clearInformation();
+                    textSelectedTask.setText("");
+                }
+                downloadTasks.remove(taskToRemove.get());
+                listModel.removeElement(taskToRemove.get());
+                updateTaskCount();
+                
+            }
+        }
+        panel.repaint();
+    }
+    
+    private void updateTaskCount(){
+        labelTaskCount.setText(Integer.toString(listTasks.getModel().getSize()));
+    }
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/DownloadTaskInformationPanel.java b/src/main/java/com/greinet/tvtotalripper/ui/DownloadTaskInformationPanel.java
new file mode 100644
index 0000000..c0f4ca2
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/DownloadTaskInformationPanel.java
@@ -0,0 +1,126 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.greinet.tvtotalripper.ui;
+
+import com.greinet.tvtotalripper.download.DownloadTask;
+import java.awt.GridLayout;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JTextField;
+import org.apache.commons.io.FileUtils;
+
+/**
+ *
+ * @author agreiner
+ */
+public class DownloadTaskInformationPanel extends JPanel{
+    
+    private JLabel labelTitle;
+    private JLabel labelShow;
+    private JLabel labelSeason;
+    private JLabel labelEpisode;
+    private JLabel labelDuration;
+    private JLabel labelProgress;
+    
+    private JTextField textTitle;
+    private JTextField textShow;
+    private JTextField textSeason;
+    private JTextField textEpisode;
+    private JTextField textDuration;
+    private JProgressBar progressBar;
+    
+    public DownloadTask selectedDownloadTask;
+    
+    public DownloadTaskInformationPanel(){
+        
+        labelTitle = new JLabel("Title");
+        labelShow = new JLabel("Show");
+        labelSeason = new JLabel("Season");
+        labelEpisode = new JLabel("Episode");
+        labelDuration = new JLabel("Duration");
+        labelProgress = new JLabel("Progress");
+        
+        textTitle = new JTextField();
+        textTitle.setEditable(false);
+        
+        textShow = new JTextField();
+        textShow.setEditable(false);
+        
+        textSeason = new JTextField();
+        textSeason.setEditable(false);
+        
+        textEpisode = new JTextField();
+        textEpisode.setEditable(false);
+        
+        textDuration = new JTextField();
+        textDuration.setEditable(false);
+        
+        progressBar = new JProgressBar(0, 100);
+        
+        setLayout(new GridLayout(6, 2));
+        
+        add(labelTitle);
+        add(textTitle);
+        add(labelShow);
+        add(textShow);
+        add(labelSeason);
+        add(textSeason);
+        add(labelEpisode);
+        add(textEpisode);
+        add(labelDuration);
+        add(textDuration);
+        add(labelProgress);
+        add(progressBar);
+    }
+    
+    public void clearInformation(){
+        this.selectedDownloadTask = null;
+        textTitle.setText("");
+        textShow.setText("");
+        textSeason.setText("");
+        textEpisode.setText("");
+        textDuration.setText("");
+        progressBar.setValue(0);
+        progressBar.setString("");
+    }
+    
+    public void setInformation(DownloadTask downloadTask){
+        textTitle.setText(downloadTask.getEpisodeWrapper().getTitle());
+        textTitle.setEditable(false);
+        textShow.setText(downloadTask.getEpisodeWrapper().getShow());
+        textShow.setEditable(false);
+        textSeason.setText(downloadTask.getEpisodeWrapper().getSeason());
+        textSeason.setEditable(false);
+        textEpisode.setText(downloadTask.getEpisodeWrapper().getEpisode());
+        textEpisode.setEditable(false);
+        textDuration.setText(downloadTask.getEpisodeWrapper().getDuration());
+        
+        if(selectedDownloadTask != null && selectedDownloadTask.equals(downloadTask)){
+            progressBar.setValue(downloadTask.percentCompleted);
+            progressBar.setStringPainted(true);
+            String done = FileUtils.byteCountToDisplaySize(downloadTask.totalBytesRead);
+            String todo = FileUtils.byteCountToDisplaySize(downloadTask.fileSize);
+            progressBar.setString(done+"/"+todo);
+        }
+        
+        downloadTask.addPropertyChangeListener((PropertyChangeEvent evt) -> {
+            
+            if(selectedDownloadTask != null && evt.getPropertyName().equals(Long.toString(selectedDownloadTask.getEpisodeWrapper().getId()))){
+                progressBar.setValue((int)evt.getNewValue());
+                progressBar.setStringPainted(true);
+                String done = FileUtils.byteCountToDisplaySize(downloadTask.totalBytesRead);
+                String todo = FileUtils.byteCountToDisplaySize(downloadTask.fileSize);
+                progressBar.setString(done+"/"+todo);
+            }
+        });
+    }
+    
+    
+    
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/EpisodesRipperPanel.java b/src/main/java/com/greinet/tvtotalripper/ui/EpisodesRipperPanel.java
new file mode 100644
index 0000000..9a3cf63
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/EpisodesRipperPanel.java
@@ -0,0 +1,124 @@
+package com.greinet.tvtotalripper.ui;
+
+import com.greinet.tvtotalripper.crawler.EpisodeWrapper;
+import java.awt.Color;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.List;
+import javax.swing.BorderFactory;
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.event.ListSelectionEvent;
+
+/**
+ *
+ * @author agreiner
+ */
+public class EpisodesRipperPanel {
+    
+    private final JPanel panel;
+    
+    private JList listPanel;
+    private DefaultListModel listModel;
+    
+    private JTextField textField;
+    private final JButton buttonDownload;
+    private final JLabel label;
+    private final List episodes;
+    
+    private final JButton buttonDownloadAll;
+    
+    public EpisodesRipperPanel(List episodes){
+        this.episodes = episodes;
+        
+        panel = new JPanel(false);
+        panel.setLayout(new GridBagLayout());
+        
+        GridBagConstraints c = new GridBagConstraints();
+        c.gridx = 0;
+        c.gridy = 0;
+        c.gridwidth = 3;
+        c.fill = GridBagConstraints.BOTH;
+        c.weightx = 0.4;
+        
+        textField = new JTextField();
+        textField.setEditable(false);
+        panel.add(textField,c);
+        
+        buttonDownload = new JButton("Download");
+        c.gridx = 3;
+        c.gridwidth = 2;
+        c.weightx = 0.1;
+        
+        panel.add(buttonDownload,c);
+
+        listModel = new DefaultListModel<>();
+        episodes.forEach(e -> listModel.addElement(e));
+        
+        listPanel = new JList<>(listModel);
+        JScrollPane scrollPane = new JScrollPane();
+        scrollPane.setViewportView(listPanel);
+        listPanel.setLayoutOrientation(JList.VERTICAL);
+        listPanel.setSelectedIndex(0);
+        
+        listPanel.addListSelectionListener((ListSelectionEvent e) -> {
+            textField.setText(listPanel.getSelectedValue().toString());
+        });
+        textField.setText(listPanel.getSelectedValue().toString());
+        
+        c.gridx = 0;
+        c.gridy = 1;
+        c.gridwidth = 5;
+        c.gridheight = 5;
+        c.weightx = 0.5;
+        
+        panel.add(scrollPane,c);
+        
+        label = new JLabel(Integer.toString(episodes.size()));
+        label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+        
+        buttonDownloadAll = new JButton("Download all episodes");
+        c.gridx = 6;
+        c.gridy = 0;
+        c.gridwidth = 1;
+        c.gridheight = 6;
+        c.weightx = 0.5;
+        panel.add(buttonDownloadAll,c);
+        
+        
+        
+        c.gridx = 4;
+        c.gridy = 6;
+        c.gridwidth = 1;
+        c.gridheight = 1;
+        c.fill = GridBagConstraints.NONE;
+        c.anchor = GridBagConstraints.EAST;
+        
+        panel.add(label, c);
+        
+        
+        
+    }
+    
+    public JPanel getJPanel(){
+        return panel;
+    }
+    
+    public EpisodeWrapper getCurrentSelected(){
+        return listPanel.getSelectedValue();
+    }
+    
+    public JButton getDownloadButton(){
+        return buttonDownload;
+    }
+    
+    public JButton getDownloadAllButton(){
+        return buttonDownloadAll;
+    }
+    
+}
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/RipperWindow.java b/src/main/java/com/greinet/tvtotalripper/ui/RipperWindow.java
new file mode 100644
index 0000000..838cf46
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/RipperWindow.java
@@ -0,0 +1,132 @@
+package com.greinet.tvtotalripper.ui;
+
+import com.greinet.tvtotalripper.crawler.CrawlerUtil;
+import com.greinet.tvtotalripper.crawler.EpisodeWrapper;
+import com.greinet.tvtotalripper.download.DownloadTask;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+/**
+ *
+ * @author agreiner
+ */
+public class RipperWindow {
+    
+    private final JFrame frame;
+    private final JTabbedPane tabbedPane;
+    
+    private JComponent panel1;
+    private JComponent panel2;
+    private JComponent panel3;
+    private JComponent panel4;
+    private JComponent panel5;
+    
+    private BaseRipperPanel showRipperPanel;
+    private SettingsRipperPanel srp;
+    private DownloadRipperPanel drp;
+    
+    public RipperWindow(){
+        frame = new JFrame("TV Total Ripper");
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        
+        tabbedPane = new JTabbedPane();
+        
+        showRipperPanel = createShowsPanel();
+        srp = new SettingsRipperPanel();
+        drp = new DownloadRipperPanel();
+        
+        panel1 = showRipperPanel.getJPanel();
+        panel2 = new JPanel();
+        panel3 = new JPanel();
+        panel4 = drp.getJPanel();
+        panel5 = srp.getPanel();
+        
+        tabbedPane.addTab("Shows", panel1);
+        tabbedPane.addTab("Seasons", panel2);
+        tabbedPane.addTab("Episodes", panel3);
+        tabbedPane.addTab("Downloads", panel4);
+        tabbedPane.addTab("Settings", panel5);
+        
+        tabbedPane.setEnabledAt(1, false);
+        tabbedPane.setEnabledAt(2, false);
+        
+        tabbedPane.setPreferredSize(new Dimension(900, 400));
+        
+        frame.add(tabbedPane);
+        
+        frame.pack();
+        frame.setAlwaysOnTop(true);
+        frame.setVisible(true);
+    }
+    
+    public static void main(String[] args) {
+        RipperWindow ripperWindow = new RipperWindow();
+    }
+    
+    private BaseRipperPanel createShowsPanel(){
+        List shows = CrawlerUtil.getShows();
+        Map namesToUrls = shows.stream().collect(Collectors.toMap(e -> e.findElement(By.xpath(".//img")).getAttribute("alt") , e -> e.getAttribute("href")));
+        
+        BaseRipperPanel brp = new BaseRipperPanel(namesToUrls);
+        
+        brp.getJButton().addActionListener((ActionEvent e) -> {
+            panel3 = new JPanel();
+            tabbedPane.setEnabledAt(2, false);
+            panel1 = createSeasonsPanel(brp.getCurrentSelected()).getJPanel();
+            tabbedPane.setComponentAt(1, panel1);
+            tabbedPane.setEnabledAt(1, true);
+            tabbedPane.setSelectedIndex(1);
+        });
+        
+        return brp;
+    }
+    
+    private BaseRipperPanel createSeasonsPanel(String url){
+        List seasons = CrawlerUtil.getSeasons(url);
+        Map elements = seasons.stream().collect(Collectors.toMap(e -> e.getText(), e -> e.getText()));
+        
+        BaseRipperPanel brp = new BaseRipperPanel(elements);
+        
+        brp.getJButton().addActionListener((ActionEvent e) -> {
+            tabbedPane.setEnabledAt(2, false);
+            panel2 = createEpisodesPanel(url,brp.getCurrentSelected()).getJPanel();
+            tabbedPane.setComponentAt(2, panel2);
+            tabbedPane.setEnabledAt(2, true);
+            tabbedPane.setSelectedIndex(2);
+        });
+        
+        return brp;
+    }
+    
+    private EpisodesRipperPanel createEpisodesPanel(String url, String seasonName){
+        List episodes = CrawlerUtil.getEpisodes(url, seasonName);
+        
+        EpisodesRipperPanel erp = new EpisodesRipperPanel(episodes);
+        
+        erp.getDownloadButton().addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                drp.addTask(new DownloadTask(erp.getCurrentSelected()));
+            }
+        });
+        
+        erp.getDownloadAllButton().addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                episodes.forEach(d -> drp.addTask(new DownloadTask(d)));
+            }
+        });
+        return erp;
+    }
+    
+}
\ No newline at end of file
diff --git a/src/main/java/com/greinet/tvtotalripper/ui/SettingsRipperPanel.java b/src/main/java/com/greinet/tvtotalripper/ui/SettingsRipperPanel.java
new file mode 100644
index 0000000..63e35da
--- /dev/null
+++ b/src/main/java/com/greinet/tvtotalripper/ui/SettingsRipperPanel.java
@@ -0,0 +1,90 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.greinet.tvtotalripper.ui;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.io.File;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+/**
+ *
+ * @author agreiner
+ */
+public class SettingsRipperPanel {
+    
+    private JPanel panel;
+    private JLabel labelDownloadFolder;
+    private JTextField textDownloadFolder;
+    private JFileChooser fileChooserDownloadFolder;
+    public static File DOWNLOADFOLDER = new File(".");
+    private JButton button;
+    
+    public SettingsRipperPanel(){
+        panel = new JPanel();
+        panel.setLayout(new GridBagLayout());
+        
+        GridBagConstraints gbc = new GridBagConstraints();
+        gbc.anchor = GridBagConstraints.NORTHWEST;
+        
+        gbc.gridx=0;
+        gbc.gridy=0;
+        gbc.gridwidth=1;
+        gbc.insets.bottom = 10;
+        labelDownloadFolder = new JLabel("Download folder");
+        labelDownloadFolder.setFont(new Font(labelDownloadFolder.getFont().getFontName(), Font.BOLD, 15));
+        panel.add(labelDownloadFolder, gbc);
+        
+        gbc.gridx=0;
+        gbc.gridy=1;
+        gbc.gridwidth=8;
+        gbc.insets.left = 20;
+        textDownloadFolder = new JTextField(DOWNLOADFOLDER.getAbsolutePath());
+        textDownloadFolder.setEditable(false);
+        textDownloadFolder.setPreferredSize(new Dimension(500, 30));
+        panel.add(textDownloadFolder,gbc);
+        
+        
+        
+        
+        
+        gbc.gridx=8;
+        gbc.gridy=1;
+        gbc.gridwidth=1;
+        gbc.insets.left = 0;
+        button = new JButton("...");
+        panel.add(button,gbc);
+        
+        fileChooserDownloadFolder = new JFileChooser(DOWNLOADFOLDER);
+        fileChooserDownloadFolder.setDialogTitle("Select download folder");
+        fileChooserDownloadFolder.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+        fileChooserDownloadFolder.setAcceptAllFileFilterUsed(false);
+        
+        
+        button.addActionListener(e -> {
+            int value = fileChooserDownloadFolder.showOpenDialog(panel);
+            
+            if (value == JFileChooser.APPROVE_OPTION){
+                DOWNLOADFOLDER = fileChooserDownloadFolder.getSelectedFile();
+                textDownloadFolder.setText(DOWNLOADFOLDER.getAbsolutePath());
+            }
+            
+        });
+    }
+
+    public JPanel getPanel() {
+        return panel;
+    }
+    
+    
+    
+}