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