package chatty.util;
import chatty.Helper;
import chatty.util.api.StreamInfo;
import chatty.util.api.TwitchApi;
import chatty.util.settings.SettingChangeListener;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Writes stream info to a file. Does not request stream info itself (except
* when a setting changes, for testing), but instead can get stream infos that
* are requested anyway (so basicially you have to have the channel already open
* or followed).
*
* Setting in the format (one file per line):
* streamname filename content
*
* Example:
* joshimuz title %title
* joshimuz game %game
* joshimuz status %title %game
*
*
* Since info is written as it comes in (streamStatus()), having more than one
* stream written to the same file doesn't really make much sense, because you
* can't guarantee a precedence. Maybe polling info would work better for that
* case, but then info may not be written immediately as it comes in. But
* usually one stream -> one file should be enough anyway. For that reason, for
* now only one file/[online/offline] combination is allowed.
*
* @author tduva
*/
public class StreamStatusWriter implements SettingChangeListener {
private static final Logger LOGGER = Logger.getLogger(StreamStatusWriter.class.getName());
private static final Charset CHARSET = Charset.forName("UTF-8");
private static final Pattern p = Pattern.compile("(\\w+) ([0-9_A-Za-z\\.]+)( online| offline)?( [^\\n]+)?");
private boolean enabled;
/**
* Using Set so only one file/[online/offline] combination is allowed.
*/
private final Set<Item> items = new HashSet<>();
/**
* Save list of streams used in the items, so they can be pulled for
* testing.
*/
private final Set<String> streams = new HashSet<>();
/**
* The base path for writing the files.
*/
private final String path;
private final TwitchApi api;
/**
* Creates a new instance.
*
* @param path The path to use as base for the file to write the info to
* @param api A reference to the TwitchApi to request info from for testing
*/
public StreamStatusWriter(String path, TwitchApi api) {
this.path = path;
this.api = api;
}
/**
* Enable or disable writing the stream info altogether.
*
* @param enabled
*/
public synchronized void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Sets the current setting to a new one, parsing the information and
* turning it into easier to use Item objects.
*
* @param setting
*/
public synchronized void setSetting(String setting) {
items.clear();
streams.clear();
Matcher m = p.matcher(setting);
while (m.find()) {
String stream = StringUtil.toLowerCase(m.group(1));
String file = m.group(2);
String online = StringUtil.trim(m.group(3));
boolean forOnline = true;
if ("offline".equals(online)) {
forOnline = false;
}
String content = StringUtil.trim(m.group(4));
content = content == null ? "" : content;
items.add(new Item(stream, file, content, forOnline));
streams.add(stream);
}
//System.out.println(items);
}
/**
* If stream info writing is enabled altogether, request every stream info
* once to test the info writing.
*/
public synchronized void test() {
if (enabled) {
for (String stream : streams) {
streamStatus(api.getStreamInfo(stream, null));
}
}
}
/**
* Writes the given stream info if it is online and an Item exists for it.
*
* @param info
*/
public synchronized void streamStatus(StreamInfo info) {
if (enabled && info.isValid()) {
for (Item item : items) {
checkItemAndWrite(info, item);
}
}
}
/**
* Check if the given StreamInfo matches the Item's requirements, and if so,
* then write it.
*
* @param info The StreamInfo to get the info from
* @param item The Item to check if the StreamInfo should be used
* @return true if a file was written, false if the requirements didn't
* match
*/
private boolean checkItemAndWrite(StreamInfo info, Item item) {
if (item.stream.equalsIgnoreCase(info.getStream())) {
if ((item.forOnline && info.getOnline())
|| !item.forOnline && !info.getOnline()) {
write(item.file, makeContent(info, item.content));
return true;
}
}
return false;
}
/**
* Makes the text that will be written to the file.
*
* @param info The StreamInfo to replace the parameters with
* @param content The content, which can contain parameters to be replaced
* @return The content, with the parameter replaced
*/
private String makeContent(StreamInfo info, String content) {
content = content.replace("%title", info.getTitle());
content = content.replace("%game", info.getGame());
content = content.replace("%viewersf", Helper.formatViewerCount(info.getViewers()));
content = content.replace("%followersf", Helper.formatViewerCount(info.getFollowerCount()));
content = content.replace("%subscribersf", Helper.formatViewerCount(info.getSubscriberCount()));
content = content.replace("%viewers", String.valueOf(info.getViewers()));
content = content.replace("%followers", String.valueOf(info.getFollowerCount()));
content = content.replace("%subscribers", String.valueOf(info.getSubscriberCount()));
return content;
}
/**
* Write the given content to a file.
*
* @param fileName
* @param content
*/
private void write(String fileName, String content) {
Path path = Paths.get(this.path, "exported");
Path file = path.resolve(fileName);
try {
Files.createDirectories(path);
try (BufferedWriter writer = Files.newBufferedWriter(file, CHARSET)) {
writer.write(content);
}
} catch (IOException ex) {
LOGGER.warning("Error writing status: " + ex);
}
}
/**
* Listen to setting changes and update accordingly.
*
* @param setting
* @param type
* @param value
*/
@Override
public void settingChanged(String setting, int type, Object value) {
if (setting.equals("statusWriter")) {
setSetting((String) value);
test();
} else if (setting.equals("enableStatusWriter")) {
enabled = (Boolean)value;
test();
}
}
private static class Item {
public final String stream;
public final String file;
public final String content;
public final boolean forOnline;
public Item(String stream, String file, String content, boolean forOnline) {
this.stream = stream;
this.file = file;
this.content = content;
this.forOnline = forOnline;
}
@Override
public String toString() {
return stream+"/"+file+"/"+forOnline+"/"+content;
}
@Override
public int hashCode() {
int hash = 5;
hash = 17 * hash + Objects.hashCode(this.file);
hash = 17 * hash + (this.forOnline ? 1 : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Item other = (Item) obj;
if (!Objects.equals(this.file, other.file)) {
return false;
}
if (this.forOnline != other.forOnline) {
return false;
}
return true;
}
}
}