diff --git a/lib/jgat-custom.jar b/lib/jgat-custom.jar new file mode 100644 index 00000000..77b13610 Binary files /dev/null and b/lib/jgat-custom.jar differ diff --git a/source/net/sourceforge/filebot/Analytics.java b/source/net/sourceforge/filebot/Analytics.java new file mode 100644 index 00000000..03fb5823 --- /dev/null +++ b/source/net/sourceforge/filebot/Analytics.java @@ -0,0 +1,209 @@ + +package net.sourceforge.filebot; + + +import static com.dmurph.tracking.JGoogleAnalyticsTracker.GoogleAnalyticsVersion.*; +import static net.sourceforge.filebot.Settings.*; + +import java.awt.DisplayMode; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.HeadlessException; +import java.util.Map; +import java.util.logging.Logger; + +import com.dmurph.tracking.AnalyticsConfigData; +import com.dmurph.tracking.JGoogleAnalyticsTracker; +import com.dmurph.tracking.VisitorData; +import com.sun.jna.Platform; + + +public class Analytics { + + private static final Map persistentData = Settings.forPackage(Analytics.class).node("analytics").asMap(); + private static final String VISITOR_ID = "visitorId"; + private static final String TIMESTAMP_FIRST = "timestampFirst"; + private static final String TIMESTAMP_LAST = "timestampLast"; + private static final String VISITS = "visits"; + + private static final VisitorData visitorData = restoreVisitorData(); + private static final JGoogleAnalyticsTracker tracker = new JGoogleAnalyticsTracker(getConfig(getApplicationProperty("analytics.WebPropertyID"), visitorData), V_4_7_2); + + + public static void trackView(Class view, String title) { + trackView(view.getName().replace(',', '/'), title); + } + + + public static synchronized void trackView(String view, String title) { + if (!tracker.isEnabled()) + return; + + tracker.trackPageViewFromSearch(view, title, "filebot.sourceforge.net", getJavaVersionIdentifier(), getDeploymentMethod()); + } + + + public static void trackEvent(String category, String action, String label) { + trackEvent(category, action, label, null); + } + + + public static synchronized void trackEvent(String category, String action, String label, Integer value) { + if (!tracker.isEnabled()) + return; + + tracker.trackEvent(category, action, label, value); + } + + + public static void setEnabled(boolean enabled) { + tracker.setEnabled(enabled); + } + + + private static String getDeploymentMethod() { + return getApplicationDeployment() == null ? "fatjar" : getApplicationDeployment(); + } + + + private static String getJavaVersionIdentifier() { + return System.getProperty("java.runtime.name") + " " + System.getProperty("java.version"); + } + + + private static AnalyticsConfigData getConfig(String webPropertyID, VisitorData visitorData) { + AnalyticsConfigData config = new AnalyticsConfigData(webPropertyID, visitorData); + + config.setUserAgent(getUserAgent()); + config.setEncoding(System.getProperty("file.encoding")); + config.setUserLanguage(getUserLanguage()); + + try { + GraphicsDevice[] display = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); + config.setScreenResolution(getScreenResolution(display)); + config.setColorDepth(getColorDepth(display)); + } catch (HeadlessException e) { + Logger.getLogger(Analytics.class.getName()).warning(e.getMessage()); + config.setScreenResolution("80x25"); + config.setColorDepth("1"); + } + + return config; + } + + + private static String getUserAgent() { + String wm = null; + String os = null; + + if (Platform.isWindows()) { + wm = "Windows"; + os = "Windows NT " + System.getProperty("os.version"); + } else if (Platform.isX11()) { + wm = "X11"; + if (Platform.isLinux()) + os = "Linux " + System.getProperty("os.arch"); + else if (Platform.isSolaris()) + os = "SunOS " + System.getProperty("os.version"); + else if (Platform.isFreeBSD()) + os = "FreeBSD"; + else if (Platform.isOpenBSD()) + os = "OpenBSD"; + } else if (Platform.isMac()) { + wm = "Macintosh"; + os = System.getProperty("os.name"); + } + + return String.format("%s/%s (%s; U; %s; JRE %s)", getApplicationName(), getApplicationVersion(), wm, os, System.getProperty("java.version")); + } + + + private static String getUserLanguage() { + String region = System.getProperty("user.region"); + if (region == null) + region = System.getProperty("user.country"); + + return System.getProperty("user.language") + "-" + region; + } + + + private static String getScreenResolution(GraphicsDevice[] display) { + int screenHeight = 0; + int screenWidth = 0; + + // get size of each screen + for (int i = 0; i < display.length; i++) { + DisplayMode dm = display[i].getDisplayMode(); + screenWidth += dm.getWidth(); + screenHeight += dm.getHeight(); + } + + if (screenHeight <= 0 && screenWidth <= 0) + throw new HeadlessException("Illegal screen size"); + + return screenWidth + "x" + screenHeight; + } + + + private static String getColorDepth(GraphicsDevice[] display) { + if (display[0] == null) + return null; + + String colorDepth = display[0].getDisplayMode().getBitDepth() + ""; + for (int i = 1; i < display.length; i++) { + colorDepth += ", " + display[i].getDisplayMode().getBitDepth(); + } + + return colorDepth; + } + + + private static VisitorData restoreVisitorData() { + try { + // try to restore visitor + int visitorId = Integer.parseInt(persistentData.get(VISITOR_ID)); + long timestampFirst = Long.parseLong(persistentData.get(TIMESTAMP_FIRST)); + long timestampLast = Long.parseLong(persistentData.get(TIMESTAMP_LAST)); + int visits = Integer.parseInt(persistentData.get(VISITS)); + + return VisitorData.newSession(visitorId, timestampFirst, timestampLast, visits); + } catch (Exception e) { + // new visitor + return VisitorData.newVisitor(); + } + } + + + private static void storeVisitorData(VisitorData visitor) { + persistentData.put(VISITOR_ID, String.valueOf(visitor.getVisitorId())); + persistentData.put(TIMESTAMP_FIRST, String.valueOf(visitor.getTimestampFirst())); + persistentData.put(TIMESTAMP_LAST, String.valueOf(visitor.getTimestampPrevious())); + persistentData.put(VISITS, String.valueOf(visitor.getVisits())); + } + + + public static void completeTracking(long timeout) { + storeVisitorData(visitorData); + JGoogleAnalyticsTracker.completeBackgroundTasks(timeout); + } + + + static { + Runtime.getRuntime().addShutdownHook(new Thread("AnalyticsShutdownHook") { + + @Override + public void run() { + completeTracking(2000); + } + }); + } + + + /** + * Dummy constructor to prevent instantiation. + */ + private Analytics() { + throw new UnsupportedOperationException(); + } + +} diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index fcfbc6ab..16624d1c 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -71,12 +71,16 @@ public class Main { CacheManager.getInstance().clearAll(); } + // initialize analytics + Analytics.setEnabled(!argumentBean.disableAnalytics); + + // run command-line interface and then exit if (argumentBean.runCLI()) { - // run cmdline interface and then exit - System.exit(new ArgumentProcessor().process(argumentBean)); + int status = new ArgumentProcessor().process(argumentBean); + System.exit(status); } - // Start user interface + // start user interface SwingUtilities.invokeLater(new Runnable() { @Override diff --git a/source/net/sourceforge/filebot/Settings.java b/source/net/sourceforge/filebot/Settings.java index 58a7a38b..415fec97 100644 --- a/source/net/sourceforge/filebot/Settings.java +++ b/source/net/sourceforge/filebot/Settings.java @@ -32,9 +32,21 @@ public final class Settings { } + public static String getApplicationDeployment() { + String deployment = System.getProperty("application.deployment"); + if (deployment != null) + return deployment; + + if (System.getProperty("javawebstart.version") != null) + return "webstart"; + + return null; + } + + public static File getApplicationFolder() { // special handling for web start - if (System.getProperty("application.deployment") != null || System.getProperty("javawebstart.version") != null) { + if (getApplicationDeployment() != null) { // can't use working directory for web start applications File folder = new File(System.getProperty("user.home"), ".filebot"); diff --git a/source/net/sourceforge/filebot/Settings.properties b/source/net/sourceforge/filebot/Settings.properties index 83f3044a..24d89631 100644 --- a/source/net/sourceforge/filebot/Settings.properties +++ b/source/net/sourceforge/filebot/Settings.properties @@ -2,6 +2,10 @@ application.name: FileBot application.version: 2.0 +# google analytics +analytics.WebPropertyID: UA-25379256-2 + +# database api keys thetvdb.apikey: 58B4AA94C59AD656 themoviedb.apikey: 5a6edae568130bf10617b6d45be99f13 sublight.apikey: afa9ecb2-a3ee-42b1-9225-000b4038bc85 diff --git a/source/net/sourceforge/filebot/cli/ArgumentBean.java b/source/net/sourceforge/filebot/cli/ArgumentBean.java index 419f2e0f..a14a7ad8 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentBean.java +++ b/source/net/sourceforge/filebot/cli/ArgumentBean.java @@ -57,11 +57,14 @@ public class ArgumentBean { @Option(name = "--log", usage = "Log level", metaVar = "[all, config, info, warning]") public String log = "all"; + @Option(name = "-open", usage = "Open file in GUI", metaVar = "file") + public boolean open = false; + @Option(name = "-clear", usage = "Clear cache and application settings") public boolean clear = false; - @Option(name = "-open", usage = "Open file in GUI", metaVar = "file") - public boolean open = false; + @Option(name = "-no-analytics", usage = "Disable analytics") + public boolean disableAnalytics = false; @Option(name = "-help", usage = "Print this help message") public boolean help = false; diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 20078920..78940d53 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -28,10 +28,11 @@ import java.util.TreeSet; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Map.Entry; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.WebServices; -import net.sourceforge.filebot.format.MediaBindingBean; import net.sourceforge.filebot.format.ExpressionFormat; +import net.sourceforge.filebot.format.MediaBindingBean; import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.hash.VerificationFileReader; import net.sourceforge.filebot.hash.VerificationFileWriter; @@ -60,8 +61,10 @@ import net.sourceforge.filebot.web.VideoHashSubtitleService; public class ArgumentProcessor { public int process(ArgumentBean args) throws Exception { + Analytics.trackView(ArgumentProcessor.class, "FileBot CLI"); + CLILogger.setLevel(args.getLogLevel()); + try { - CLILogger.setLevel(args.getLogLevel()); Set files = new LinkedHashSet(args.getFiles(true)); if (args.getSubtitles) { @@ -185,6 +188,7 @@ public class ArgumentProcessor { } // rename episodes + Analytics.trackEvent("CLI", "Rename", "Episode", renameMap.size()); return renameAll(renameMap); } @@ -243,7 +247,8 @@ public class ArgumentProcessor { } } - // rename episodes + // rename movies + Analytics.trackEvent("CLI", "Rename", "Movie", renameMap.size()); return renameAll(renameMap); } @@ -279,6 +284,7 @@ public class ArgumentProcessor { if (it.getValue() != null && it.getValue().size() > 0) { // auto-select first element if there are multiple hash matches for the same video files File subtitle = fetchSubtitle(it.getValue().get(0), it.getKey(), outputFormat, outputEncoding); + Analytics.trackEvent(service.getName(), "DownloadSubtitle", it.getValue().get(0).getLanguageName(), 1); // download complete, cross this video off the list remainingVideos.remove(it.getKey()); @@ -305,6 +311,7 @@ public class ArgumentProcessor { for (SubtitleDescriptor descriptor : subtitles) { if (isDerived(descriptor.getName(), video)) { File subtitle = fetchSubtitle(descriptor, video, outputFormat, outputEncoding); + Analytics.trackEvent(service.getName(), "DownloadSubtitle", descriptor.getLanguageName(), 1); // download complete, cross this video off the list remainingVideos.remove(video); @@ -324,6 +331,7 @@ public class ArgumentProcessor { CLILogger.warning("No matching subtitles found: " + video); } + Analytics.trackEvent("CLI", "Download", "Subtitle", downloadedSubtitles.size()); return downloadedSubtitles; } diff --git a/source/net/sourceforge/filebot/ui/MainFrame.java b/source/net/sourceforge/filebot/ui/MainFrame.java index 8c786c91..c8c81f54 100644 --- a/source/net/sourceforge/filebot/ui/MainFrame.java +++ b/source/net/sourceforge/filebot/ui/MainFrame.java @@ -27,6 +27,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanelBuilder; @@ -131,6 +132,7 @@ public class MainFrame extends JFrame { contentPane.add(panel); } + Analytics.trackView(panel.getClass(), selectedBuilder.getName()); headerPanel.setTitle(selectedBuilder.getName()); panel.setVisible(true); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeListMatcher.java b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeListMatcher.java index 8c424340..0121e448 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeListMatcher.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeListMatcher.java @@ -29,6 +29,7 @@ import java.util.concurrent.RunnableFuture; import javax.swing.Action; import javax.swing.SwingUtilities; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Matcher; import net.sourceforge.filebot.similarity.NameSimilarityMetric; @@ -122,6 +123,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { SearchResult selectedSearchResult = selectSearchResult(query, results); if (selectedSearchResult != null) { + Analytics.trackEvent(provider.getName(), "FetchEpisodeList", selectedSearchResult.getName()); return provider.getEpisodeList(selectedSearchResult, locale); } } @@ -194,6 +196,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } }); + Analytics.trackEvent(provider.getName(), "Match", "Episode", matches.size()); return matches; } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java b/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java index d9bfd9d5..44eff64a 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java @@ -30,6 +30,7 @@ import java.util.concurrent.RunnableFuture; import javax.swing.Action; import javax.swing.SwingUtilities; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.web.MovieDescriptor; @@ -65,6 +66,10 @@ class MovieHashMatcher implements AutoCompleteMatcher { // unknown hash, try via imdb id from nfo file if (movie == null || !autodetect) { movie = grabMovieName(movieFiles[i], locale, autodetect, movie); + + if (movie != null) { + Analytics.trackEvent(service.getName(), "SearchMovie", movie.getName()); + } } // check if we managed to lookup the movie descriptor @@ -81,6 +86,8 @@ class MovieHashMatcher implements AutoCompleteMatcher { } } + Analytics.trackEvent(service.getName(), "HashLookup", "Movie", filesByMovie.size()); // number of positive hash lookups + // collect all File/MoviePart matches List> matches = new ArrayList>(); @@ -125,6 +132,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { } }); + Analytics.trackEvent(service.getName(), "Match", "Movie", matches.size()); return matches; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index 5440c9a2..846da50d 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -12,6 +12,7 @@ import java.awt.event.ActionEvent; import java.io.File; import java.util.AbstractList; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -19,6 +20,7 @@ import java.util.Map.Entry; import javax.swing.AbstractAction; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; @@ -74,10 +76,14 @@ class RenameAction extends AbstractAction { } } + // collect renamed types + List types = new ArrayList(); + // remove renamed matches for (Entry entry : renameLog) { // find index of source file int index = model.files().indexOf(entry.getKey()); + types.add(model.values().get(index).getClass()); // remove complete match model.matches().remove(index); @@ -86,6 +92,10 @@ class RenameAction extends AbstractAction { // update history if (renameLog.size() > 0) { HistorySpooler.getInstance().append(renameLog); + + for (Class it : new HashSet(types)) { + Analytics.trackEvent("GUI", "Rename", it.getSimpleName(), frequency(types, it)); + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java index 16503a5c..56abd6e5 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java @@ -52,6 +52,7 @@ import ca.odell.glazedlists.swing.EventSelectionModel; import ca.odell.glazedlists.swing.TextComponentMatcherEditor; import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.subtitle.SubtitleFormat; import net.sourceforge.filebot.ui.panel.subtitle.SubtitlePackage.Download.Phase; @@ -239,7 +240,9 @@ class SubtitleDownloadComponent extends JComponent { public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() == Phase.DONE) { try { - files.addAll(subtitle.getDownload().get()); + List subtitles = subtitle.getDownload().get(); + Analytics.trackEvent(subtitle.getProvider().getName(), "DownloadSubtitle", subtitle.getLanguage().getName(), subtitles.size()); + files.addAll(subtitles); } catch (CancellationException e) { // ignore cancellation } catch (Exception e) { diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java index ba55fa0e..ecc440ba 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java @@ -24,19 +24,20 @@ import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.vfs.ArchiveType; import net.sourceforge.filebot.vfs.MemoryFile; import net.sourceforge.filebot.web.SubtitleDescriptor; +import net.sourceforge.filebot.web.SubtitleProvider; import net.sourceforge.tuned.FileUtilities; public class SubtitlePackage { + private final SubtitleProvider provider; private final SubtitleDescriptor subtitle; - private final Language language; - private Download download; - public SubtitlePackage(SubtitleDescriptor subtitle) { + public SubtitlePackage(SubtitleProvider provider, SubtitleDescriptor subtitle) { + this.provider = provider; this.subtitle = subtitle; // resolve language name @@ -58,6 +59,11 @@ public class SubtitlePackage { } + public SubtitleProvider getProvider() { + return provider; + } + + public String getName() { return subtitle.getName(); } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index ef24be6c..c7b0130b 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -214,7 +214,7 @@ public class SubtitlePanel extends AbstractSearchPanel packages = new ArrayList(); for (SubtitleDescriptor subtitle : request.getProvider().getSubtitleList(getSearchResult(), request.getLanguageName())) { - packages.add(new SubtitlePackage(subtitle)); + packages.add(new SubtitlePackage(request.getProvider(), subtitle)); } return packages; diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java b/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java index d8692f9f..3c4ba404 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java @@ -52,6 +52,7 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.web.SubtitleDescriptor; import net.sourceforge.filebot.web.VideoHashSubtitleService; @@ -161,7 +162,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { // query services sequentially queryService = Executors.newFixedThreadPool(1); - for (VideoHashSubtitleServiceBean service : services) { + for (final VideoHashSubtitleServiceBean service : services) { QueryTask task = new QueryTask(service, mappingModel.getVideoFiles(), languageName) { @Override @@ -179,6 +180,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { } // make subtitle column visible + Analytics.trackEvent(service.getName(), "HashLookup", "Movie", subtitles.size()); // number of positive hash lookups mappingModel.setOptionColumnVisible(true); } catch (Exception e) { Logger.getLogger(VideoHashSubtitleDownloadDialog.class.getName()).log(Level.WARNING, e.getMessage()); @@ -584,7 +586,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { private static class SubtitleDescriptorBean extends AbstractBean { private final SubtitleDescriptor subtitle; - private final VideoHashSubtitleServiceBean source; + private final VideoHashSubtitleServiceBean service; private StateValue state; private Exception error; @@ -592,7 +594,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { public SubtitleDescriptorBean(SubtitleDescriptor subtitle, VideoHashSubtitleServiceBean source) { this.subtitle = subtitle; - this.source = source; + this.service = source; } @@ -602,7 +604,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { public Icon getIcon() { - return source.getIcon(); + return service.getIcon(); } @@ -615,6 +617,7 @@ class VideoHashSubtitleDownloadDialog extends JDialog { setState(StateValue.STARTED); try { + Analytics.trackEvent(service.getName(), "DownloadSubtitle", subtitle.getLanguageName(), 1); return subtitle.fetch(); } catch (Exception e) { // remember exception