package com.jadn.cc.services; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import javax.xml.parsers.SAXParserFactory; import android.util.Log; import android.widget.TextView; import com.jadn.cc.core.CarCastApplication; import com.jadn.cc.core.Config; import com.jadn.cc.core.OrderingPreference; import com.jadn.cc.core.Sayer; import com.jadn.cc.core.Subscription; import com.jadn.cc.core.Util; public class DownloadHelper implements Sayer { private String currentSubscription = " "; private String currentTitle = " "; private int globalMax; private int podcastsCurrentBytes; private int podcastsDownloaded; private int podcastsTotalBytes; private int sitesScanned; private int totalPodcasts; private int totalSites; private TextView tv; private boolean isRunning = true; StringBuilder sb = new StringBuilder("Getting ready to start downloads\n"); public DownloadHelper(int globalMax) { this.globalMax = globalMax; } public boolean isRunning(){ return isRunning; } SimpleDateFormat sdf = new SimpleDateFormat("MMM-dd hh:mma"); private String getLocalFileExtFromMimetype(String mimetype) { if ("audio/mp3".equals(mimetype)) { return ".mp3"; } if ("audio/ogg".equals(mimetype)) { return ".ogg"; } return ".bin"; } protected void downloadNewPodCasts(ContentService contentService) { try { DownloadHistory history = new DownloadHistory(contentService); say("Starting find/download new podcasts. CarCast ver " + CarCastApplication.getVersion()); say("Problems? please use Menu / Email Download Report - THANKS!"); List<Subscription> sites = contentService.getSubscriptions(); say("\nSearching " + sites.size() + " subscriptions. " + sdf.format(new Date())); totalSites = sites.size(); say("History of downloads contains " + history.size() + " podcasts."); List<MetaNet> enclosures = new ArrayList<MetaNet>(); SAXParserFactory spf = SAXParserFactory.newInstance(); for (Subscription sub : sites) { EnclosureHandler encloseureHandler = new EnclosureHandler(history, sub.priority); if (sub.enabled) { try { say("\nScanning subscription/feed: " + sub.url); URL url = new URL(sub.url); int foundStart = encloseureHandler.metaNets.size(); if (sub.maxDownloads == Subscription.GLOBAL) encloseureHandler.setMax(globalMax); else encloseureHandler.setMax(sub.maxDownloads); String name = sub.name; encloseureHandler.setFeedName(name); Util.findAvailablePodcasts(sub.url, encloseureHandler); String message = sitesScanned + "/" + sites.size() + ": " + name + ", " + (encloseureHandler.metaNets.size() - foundStart) + " new"; say(message); contentService.updateNotification(message); } catch (Throwable e) { /* Display any Error to the GUI. */ say("Error ex:" + e.getMessage()); Log.e("BAH", "bad", e); } } else { say("\nSkipping subscription/feed: " + sub.url + " because it is not enabled."); } sitesScanned++; if (sub.orderingPreference == OrderingPreference.LIFO) Collections.reverse(encloseureHandler.metaNets); enclosures.addAll(encloseureHandler.metaNets); } // endforeach say("\nTotal enclosures " + enclosures.size()); List<MetaNet> newPodcasts = new ArrayList<MetaNet>(); for (MetaNet metaNet : enclosures) { if (history.contains(metaNet)) continue; newPodcasts.add(metaNet); } say(newPodcasts.size() + " podcasts will be downloaded."); contentService.updateNotification(newPodcasts.size() + " podcasts will be downloaded."); totalPodcasts = newPodcasts.size(); for (MetaNet metaNet : newPodcasts) { podcastsTotalBytes += metaNet.getSize(); } System.setProperty("http.maxRedirects", "50"); say("\n"); byte[] buf = new byte[16383]; int got = 0; for (int i = 0; i < newPodcasts.size(); i++) { String shortName = newPodcasts.get(i).getTitle(); String localFileExt = getLocalFileExtFromMimetype(newPodcasts.get(i).getMimetype()); say((i + 1) + "/" + newPodcasts.size() + " " + shortName); contentService.updateNotification((i + 1) + "/" + newPodcasts.size() + " " + shortName); podcastsDownloaded = i + 1; try { Config config = new Config(contentService); String prefix = ""; /* * Non-priority podcast files are named XXXX.mp3, where XXXX is the millisecond timestamp at * the time the podcast was downloaded. * * Priority podcast files are named YYYY:00:XXXX.mp3, where XXXX is as above, and YYYY is the timestamp of * file currently in the player. * * Notes: * ":" is chosen as the separator because it sorts after the "." of the file name suffix. * The "00" is incuded to make it possible to have multiple priority levels in the future. * * IMPORTANT: * The naming scheme used here *must* match MetaHolder.isPriority(). */ if (newPodcasts.get(i).getPriority()) if (contentService.currentMeta() != null) prefix = contentService.currentMeta().getBaseFilename() + ":00:"; String castFileName = prefix + System.currentTimeMillis() + localFileExt; File castFile = config.getPodcastRootPath(castFileName); Log.d("CarCast", "New podcast file: " + castFileName); currentSubscription = newPodcasts.get(i).getSubscription(); currentTitle = newPodcasts.get(i).getTitle(); File tempFile = config.getPodcastRootPath("tempFile"); say("Subscription: " + currentSubscription); say("Title: " + currentTitle); say("enclosure url: " + new URL(newPodcasts.get(i).getUrl())); InputStream is = getInputStream(new URL(newPodcasts.get(i).getUrl())); FileOutputStream fos = new FileOutputStream(tempFile); int amt = 0; int expectedSizeKilo = newPodcasts.get(i).getSize() / 1024; String preDownload = sb.toString(); int totalForThisPodcast = 0; say(String.format("%dk/%dk 0", totalForThisPodcast / 1024, expectedSizeKilo) + "%\n"); while ((amt = is.read(buf)) >= 0) { fos.write(buf, 0, amt); podcastsCurrentBytes += amt; totalForThisPodcast += amt; sb = new StringBuilder(preDownload + String.format("%dk/%dk %d", totalForThisPodcast / 1024, expectedSizeKilo, (int) ((totalForThisPodcast / 10.24) / expectedSizeKilo)) + "%\n"); } say("download finished."); fos.close(); is.close(); // add before rename, so if rename fails, we remember // that we tried this file and skip it next time. history.add(newPodcasts.get(i)); tempFile.renameTo(castFile); new MetaFile(newPodcasts.get(i), castFile).save(); got++; if (totalForThisPodcast != newPodcasts.get(i).getSize()) { say("Note: reported size (in feed) doesnt match actual size (downloaded file)"); // subtract out wrong value podcastsTotalBytes -= newPodcasts.get(i).getSize(); // add in correct value podcastsTotalBytes += totalForThisPodcast; } say("-"); // update progress for player contentService.newContentAdded(); } catch (Throwable e) { say("Problem downloading " + newPodcasts.get(i).getUrl() + " e:" + e); } } say("Finished. Downloaded " + got + " new podcasts. " + sdf.format(new Date())); contentService.doDownloadCompletedNotification(got); } finally { isRunning = false; } } // Deal with servers with "location" instead of "Location" in redirect // headers private InputStream getInputStream(URL url) throws IOException { int redirectLimit = 15; while (redirectLimit-- > 0) { HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setInstanceFollowRedirects(false); con.setConnectTimeout(120 * 1000); con.setReadTimeout(120 * 1000); con.connect(); if (con.getResponseCode() == 200) { return con.getInputStream(); } if (con.getResponseCode() == 404 && url.getPath().contains(" ")){ String newURL = url.getProtocol()+"://"+url.getHost()+(url.getPort()==-1?"":(":"+url.getPort()))+ url.getPath().replaceAll(" ", "%20"); return getInputStream(new URL(newURL)); } if (con.getResponseCode() > 300 && con.getResponseCode() > 399) { say(url + " gave resposneCode " + con.getResponseCode()); throw new IOException(); } url = null; for (int i = 0; i < 50; i++) { if (con.getHeaderFieldKey(i) == null) continue; if (con.getHeaderFieldKey(i).toLowerCase().equals("location")) { url = new URL(con.getHeaderField(i)); } } if (url == null) { say("Got 302 without Location"); } } throw new IOException(CarCastApplication.getAppTitle() + " redirect limit reached"); } public String getStatus() { if (sitesScanned != totalSites) return "Scanning Sites " + sitesScanned + "/" + totalSites; return "Fetching " + podcastsDownloaded + "/" + totalPodcasts + "\n" + (podcastsCurrentBytes / 1024) + "k/" + (podcastsTotalBytes / 1024) + "k"; } public String getEncodedStatus(){ String status = (isRunning() ? "running" : "done") + "," + sitesScanned + "," + totalSites + "," + podcastsDownloaded + "," + totalPodcasts + "," + podcastsCurrentBytes + "," + podcastsTotalBytes + "," + currentSubscription + "," + currentTitle; return status; } @Override public void say(String text) { sb.append(text); sb.append('\n'); Log.i("CarCast/Download", text); } }