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;
+ }
+
+
+
+}