tv.phantombot.cache.TwitchCache.java Source code

Java tutorial

Introduction

Here is the source code for tv.phantombot.cache.TwitchCache.java

Source

/* astyle --style=java --indent=spaces=4 --mode=java */

/*
 * Copyright (C) 2016-2018 phantombot.tv
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * @author illusionaryone
 */

package tv.phantombot.cache;

import com.google.common.collect.Maps;

import java.lang.Math;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.List;
import java.util.TimeZone;

import org.json.JSONArray;
import org.json.JSONObject;

import com.gmt2001.TwitchAPIv5;
import com.illusionaryone.ImgDownload;
import java.io.File;
import org.apache.commons.io.FileUtils;

import tv.phantombot.PhantomBot;
import tv.phantombot.event.EventBus;
import tv.phantombot.event.twitch.online.TwitchOnlineEvent;
import tv.phantombot.event.twitch.offline.TwitchOfflineEvent;
import tv.phantombot.event.twitch.gamechange.TwitchGameChangeEvent;
import tv.phantombot.event.twitch.clip.TwitchClipEvent;
import tv.phantombot.event.twitch.titlechange.TwitchTitleChangeEvent;

/*
 * TwitchCache Class
 *
 * This class keeps track of certain Twitch information such as if the channel is online or not
 * and sends events to the JS side to indicate when the channel has gone off or online.
 */
public class TwitchCache implements Runnable {

    private static final Map<String, TwitchCache> instances = Maps.newHashMap();
    private final String channel;
    private final Thread updateThread;
    private boolean killed = false;

    /* Cached data */
    private Boolean isOnline = false;
    private Boolean forcedGameTitleUpdate = false;
    private Boolean forcedStreamTitleUpdate = false;
    private String streamCreatedAt = "";
    private String gameTitle = "Some Game";
    private String streamTitle = "Some Title";
    private String previewLink = "https://www.twitch.tv/p/assets/uploads/glitch_solo_750x422.png";
    private String logoLink = "https://www.twitch.tv/p/assets/uploads/glitch_solo_750x422.png";
    private String[] communities = new String[3];
    private long streamUptimeSeconds = 0L;
    private int viewerCount = 0;
    private int views = 0;

    /*
     * Creates an instance for a channel.
     *
     * @param   channel      Name of the Twitch Channel for which this instance is created.
     * @return  TwitchCache  The new TwitchCache instance object.
     */
    public static TwitchCache instance(String channel) {
        TwitchCache instance = instances.get(channel);
        if (instance == null) {
            instance = new TwitchCache(channel);
            instances.put(channel, instance);
            return instance;
        }
        return instance;
    }

    /*
     * Constructor for TwitchCache object.
     *
     * @param  channel  Name of the Twitch Channel for which this object is created.
     */
    @SuppressWarnings("CallToThreadStartDuringObjectConstruction")
    private TwitchCache(String channel) {
        if (channel.startsWith("#")) {
            channel = channel.substring(1);
        }

        this.channel = channel;
        this.updateThread = new Thread(this, "tv.phantombot.cache.TwitchCache");

        Thread.setDefaultUncaughtExceptionHandler(com.gmt2001.UncaughtExceptionHandler.instance());
        this.updateThread.setUncaughtExceptionHandler(com.gmt2001.UncaughtExceptionHandler.instance());

        updateThread.start();
    }

    /*
     * Thread run instance.  This is the main loop for the thread that is created to manage
     * retrieving data from the Twitch API.  This loop runs every 30 seconds, querying data
     * from Twitch.
     */
    @Override
    @SuppressWarnings("SleepWhileInLoop")
    public void run() {
        // If this cache starts before the database, we need to wait.
        while (PhantomBot.instance() == null || PhantomBot.instance().getDataStore() == null) {
            com.gmt2001.Console.debug.println("TwitchCache::run::failed:database:null");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                com.gmt2001.Console.err.printStackTrace(ex);
            }
        }

        /* Update the clips every other loop. */
        boolean doUpdateClips = false;

        /* Check the DB for a previous Game and Stream Title */
        String gameTitle = getDBString("game");
        String streamTitle = getDBString("title");

        if (gameTitle != null) {
            this.gameTitle = gameTitle;
        }
        if (streamTitle != null) {
            this.streamTitle = streamTitle;
        }

        while (!killed) {
            try {
                try {
                    this.updateCache();
                } catch (Exception ex) {
                    com.gmt2001.Console.debug.println("TwitchCache::run: " + ex.getMessage());
                }
            } catch (Exception ex) {
                com.gmt2001.Console.err.println("TwitchCache::run: " + ex.getMessage());
            }

            if (doUpdateClips) {
                doUpdateClips = false;
                updateClips();
            } else {
                doUpdateClips = true;
            }

            try {
                Thread.sleep(30 * 1000);
            } catch (InterruptedException ex) {
                com.gmt2001.Console.err.println(
                        "TwitchCache::run: Failed to execute sleep [InterruptedException]: " + ex.getMessage());
            }
        }
    }

    /*
     * Polls the Clips endppint, trying to find the most recent clip.  Note that because Twitch
     * reports by the viewcount, and has a limit of 100 clips, it is possible to miss the most
     * recent clip until it has views.
     *
     * We do not throw an exception because this is not a critical function unlike the gathering
     * of data via the updateCache() method.
     */
    private void updateClips() {
        String doCheckClips = PhantomBot.instance().getDataStore().GetString("clipsSettings", "", "toggle");
        String discordDoClipsCheck = PhantomBot.instance().getDataStore().GetString("discordSettings", "",
                "clipsToggle");
        if ((doCheckClips == null || doCheckClips.equals("false"))
                && (discordDoClipsCheck == null || discordDoClipsCheck.equals("false"))) {
            return;
        }

        JSONObject clipsObj = TwitchAPIv5.instance().getClipsToday(this.channel);

        String createdAt = "";
        String clipURL = "";
        String creator = "";
        String title = "";
        JSONObject thumbnailObj = new JSONObject();
        int largestTrackingId = 0;

        if (clipsObj.has("clips")) {
            JSONArray clipsData = clipsObj.getJSONArray("clips");
            if (clipsData.length() > 0) {
                setDBString("most_viewed_clip_url",
                        "https://clips.twitch.tv/" + clipsData.getJSONObject(0).getString("slug"));
                String lastTrackingIdStr = getDBString("last_clips_tracking_id");
                int lastTrackingId = (lastTrackingIdStr == null ? 0 : Integer.parseInt(lastTrackingIdStr));
                largestTrackingId = lastTrackingId;
                for (int i = 0; i < clipsData.length(); i++) {
                    JSONObject clipData = clipsData.getJSONObject(i);
                    int trackingId = Integer.parseInt(clipData.getString("tracking_id"));
                    if (trackingId > largestTrackingId) {
                        largestTrackingId = trackingId;
                        createdAt = clipData.getString("created_at");
                        clipURL = "https://clips.twitch.tv/" + clipData.getString("slug");
                        creator = clipData.getJSONObject("curator").getString("display_name");
                        thumbnailObj = clipData.getJSONObject("thumbnails");
                        title = clipData.getString("title");
                    }
                }
            }
        }

        if (clipURL.length() > 0) {
            setDBString("last_clips_tracking_id", String.valueOf(largestTrackingId));
            setDBString("last_clip_url", clipURL);
            EventBus.instance().postAsync(new TwitchClipEvent(clipURL, creator, title, thumbnailObj));
        }
    }

    /*
     * Polls the Twitch API and updates the database cache with information.  This method also
     * sends events when appropriate.
     */
    private void updateCache() throws Exception {
        Boolean success = true;
        Boolean isOnline = false;
        Boolean sentTwitchOnlineEvent = false;
        String gameTitle = "";
        String streamTitle = "";
        String previewLink = "";
        String logoLink = "";
        String[] communities = new String[3];
        Date streamCreatedDate = new Date();
        Date currentDate = new Date();
        long streamUptimeSeconds = 0L;

        com.gmt2001.Console.debug.println("TwitchCache::updateCache");

        /* Retrieve Stream Information */
        try {
            JSONObject streamObj = TwitchAPIv5.instance().GetStream(this.channel);

            if (streamObj.getBoolean("_success") && streamObj.getInt("_http") == 200) {

                /* Determine if the stream is online or not */
                isOnline = !streamObj.isNull("stream");

                if (!this.isOnline && isOnline) {
                    this.isOnline = true;
                    EventBus.instance().postAsync(new TwitchOnlineEvent());
                    sentTwitchOnlineEvent = true;
                } else if (this.isOnline && !isOnline) {
                    this.isOnline = false;
                    EventBus.instance().postAsync(new TwitchOfflineEvent());
                }

                if (isOnline) {
                    /* Calculate the stream uptime in seconds. */
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
                    dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
                    try {
                        streamCreatedDate = dateFormat
                                .parse(streamObj.getJSONObject("stream").getString("created_at"));
                        streamUptimeSeconds = (long) (Math
                                .floor(currentDate.getTime() - streamCreatedDate.getTime()) / 1000);
                        this.streamUptimeSeconds = streamUptimeSeconds;
                        this.streamCreatedAt = streamObj.getJSONObject("stream").getString("created_at");
                    } catch (Exception ex) {
                        success = false;
                        com.gmt2001.Console.err.println(
                                "TwitchCache::updateCache: Bad date from Twitch, cannot convert for stream uptime ("
                                        + streamObj.getJSONObject("stream").getString("created_at") + ")");
                    }

                    /* Determine the preview link. */
                    previewLink = streamObj.getJSONObject("stream").getJSONObject("preview").getString("template")
                            .replace("{width}", "1920").replace("{height}", "1080");
                    this.previewLink = previewLink;

                    /* Get the viewer count. */
                    viewerCount = streamObj.getJSONObject("stream").getInt("viewers");
                    this.viewerCount = viewerCount;
                } else {
                    streamUptimeSeconds = 0L;
                    this.streamUptimeSeconds = streamUptimeSeconds;
                    this.previewLink = "https://www.twitch.tv/p/assets/uploads/glitch_solo_750x422.png";
                    this.streamCreatedAt = "";
                    this.viewerCount = 0;
                }
            } else {
                success = false;
                if (streamObj.has("message")) {
                    com.gmt2001.Console.err.println("TwitchCache::updateCache: " + streamObj.getString("message"));
                } else {
                    com.gmt2001.Console.debug.println("TwitchCache::updateCache: Failed to update.");
                }
            }
        } catch (Exception ex) {
            com.gmt2001.Console.err.println("TwitchCache::updateCache: " + ex.getMessage());
            success = false;
        }

        // Wait a bit here.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            com.gmt2001.Console.debug.println(ex);
        }

        try {
            JSONObject streamObj = TwitchAPIv5.instance().GetChannel(this.channel);

            if (streamObj.getBoolean("_success") && streamObj.getInt("_http") == 200) {

                /* Get the game being streamed. */
                if (streamObj.has("game")) {
                    if (!streamObj.isNull("game")) {
                        gameTitle = streamObj.getString("game");
                        if (!forcedGameTitleUpdate && !this.gameTitle.equals(gameTitle)) {
                            setDBString("game", gameTitle);
                            /* Send an event if we did not just send a TwitchOnlineEvent. */
                            if (!sentTwitchOnlineEvent) {
                                this.gameTitle = gameTitle;
                                EventBus.instance().postAsync(new TwitchGameChangeEvent(gameTitle));
                            }
                            this.gameTitle = gameTitle;
                        }

                        if (forcedGameTitleUpdate && this.gameTitle.equals(gameTitle)) {
                            forcedGameTitleUpdate = false;
                        }
                    }
                } else {
                    success = false;
                }

                if (streamObj.has("views")) {
                    /* Get the view count. */
                    views = streamObj.getInt("views");
                    this.views = views;
                }

                /* Get the logo */
                if (streamObj.has("logo") && !streamObj.isNull("logo")) {
                    logoLink = streamObj.getString("logo");
                    this.logoLink = logoLink;
                    if (PhantomBot.betap) {
                        ImgDownload.downloadHTTPTo(logoLink, "./web/beta-panel/img/logo.png");
                    }
                }

                // Get the display name.
                if (PhantomBot.betap && streamObj.has("display_name") && !streamObj.isNull("display_name")) {
                    File file = new File("./web/beta-panel/js/utils/panelConfig.js");
                    if (file.exists()) {
                        // Read the file.
                        String fileContent = FileUtils.readFileToString(file, "utf-8");
                        // Replace the name.
                        fileContent = fileContent.replace("@DISPLAY_NAME@", streamObj.getString("display_name"));
                        // Write the new stuff.
                        FileUtils.writeStringToFile(file, fileContent, "utf-8");
                    }
                }

                /* Get the title. */
                if (streamObj.has("status")) {
                    if (!streamObj.isNull("status")) {
                        streamTitle = streamObj.getString("status");

                        if (!forcedStreamTitleUpdate && !this.streamTitle.equals(streamTitle)) {
                            setDBString("title", streamTitle);
                            this.streamTitle = streamTitle;
                            /* Send an event if we did not just send a TwitchOnlineEvent. */
                            if (!sentTwitchOnlineEvent) {
                                this.streamTitle = streamTitle;
                                EventBus.instance().postAsync(new TwitchTitleChangeEvent(streamTitle));
                            }
                            this.streamTitle = streamTitle;
                        }
                        if (forcedStreamTitleUpdate && this.streamTitle.equals(streamTitle)) {
                            forcedStreamTitleUpdate = false;
                        }
                    }
                } else {
                    success = false;
                }
            } else {
                success = false;
                if (streamObj.has("message")) {
                    com.gmt2001.Console.err.println("TwitchCache::updateCache: " + streamObj.getString("message"));
                } else {
                    com.gmt2001.Console.debug.println("TwitchCache::updateCache: Failed to update.");
                }
            }
        } catch (Exception ex) {
            com.gmt2001.Console.err.println("TwitchCache::updateCache: " + ex.getMessage());
            success = false;
        }

        // Wait a bit here.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            com.gmt2001.Console.debug.println(ex);
        }

        /* Update communities */
        try {
            JSONObject object = TwitchAPIv5.instance().GetCommunities(this.channel);
            if (object.has("communities") && object.getJSONArray("communities").length() > 0) {
                JSONArray array = object.getJSONArray("communities");
                for (int i = 0; i < array.length(); i++) {
                    communities[i] = array.getJSONObject(i).getString("name");
                }
            }
            this.communities = communities;
        } catch (Exception ex) {
            com.gmt2001.Console.err
                    .println("TwitchCache::updateCache: Failed to get communities: " + ex.getMessage());
        }

        if (PhantomBot.twitchCacheReady.equals("false") && success) {
            com.gmt2001.Console.debug.println("TwitchCache::setTwitchCacheReady(true)");
            PhantomBot.instance().setTwitchCacheReady("true");
        }
    }

    /*
     * Returns if the channel is online or not.
     */
    public Boolean isStreamOnline() {
        return this.isOnline;
    }

    /*
     * Returns a String representation of true/false to indicate if the stream is online or not.
     */
    public String isStreamOnlineString() {
        if (this.isOnline) {
            return new String("true");
        }
        return new String("false");
    }

    /*
     * Returns the uptime of the channel in seconds.
     */
    public long getStreamUptimeSeconds() {
        return this.streamUptimeSeconds;
    }

    /*
     * Returns the stream created_at date from Twitch.
     */
    public String getStreamCreatedAt() {
        return this.streamCreatedAt;
    }

    /*
     * Returns the name of the game being played in the channel.
     */
    public String getGameTitle() {
        return this.gameTitle;
    }

    /*
     * Sets the game title.  Useful for when !game is used.
     */
    public void setGameTitle(String gameTitle) {
        forcedGameTitleUpdate = true;
        this.gameTitle = gameTitle;
        EventBus.instance().postAsync(new TwitchGameChangeEvent(gameTitle));
    }

    /*
     * Returns the title (status) of the stream.
     */
    public String getStreamStatus() {
        return this.streamTitle;
    }

    /*
     * Sets the title (status) of the stream.  Useful for when !title is used.
     */
    public void setStreamStatus(String streamTitle) {
        forcedStreamTitleUpdate = true;
        this.streamTitle = streamTitle;
        EventBus.instance().postAsync(new TwitchTitleChangeEvent(streamTitle));
    }

    /*
     * Returns the preview link.
     */
    public String getPreviewLink() {
        return this.previewLink;
    }

    /*
     * Returns the logo link.
     */
    public String getLogoLink() {
        return this.logoLink;
    }

    /*
     * Returns the viewer count.
     */
    public int getViewerCount() {
        return this.viewerCount;
    }

    /*
     * Returns the views count.
     */
    public int getViews() {
        return this.views;
    }

    /*
     * Set the communities
     */
    public void setCommunities(String[] communities) {
        this.communities = communities;
    }

    /*
     * Returns an array of communities if set.
     */
    public String[] getCommunities() {
        return this.communities;
    }

    /*
     * Destroys the current instance of the TwitchCache object.
     */
    public void kill() {
        killed = true;
    }

    /*
     * Destroys all instances of the TwitchCache object.
     */
    public static void killall() {
        for (Entry<String, TwitchCache> instance : instances.entrySet()) {
            instance.getValue().kill();
        }
    }

    /*
     * Gets a string from the database. Simply a wrapper around the PhantomBot instance.
     *
     * @param   String  The database key to search for in the streamInfo table.
     * @return  String  Returns the found value or null.
     */
    private String getDBString(String dbKey) {
        return PhantomBot.instance().getDataStore().GetString("streamInfo", "", dbKey);
    }

    /*
     * Sets a string into the database.  Simply a wrapper around the PhantomBot instance.
     *
     * @param   String  The database key to use for inserting the value into the streamInfo table.
     * @param   String  The value to insert.
     * @return  String  Returns the found value or null.
     */
    private void setDBString(String dbKey, String dbValue) {
        PhantomBot.instance().getDataStore().SetString("streamInfo", "", dbKey, dbValue);
    }
}