Twitch is a leading streaming service that specializes in broadcasting real-time gaming. Millions of people every month are streaming on and watching Twitch. One of the best things about Twitch is that you can interact with others in real-time gaming sessions and be part of the community. However, if you are an occasional gamer like me, you may also find the amount of content and pop-out lives on the Twitch homepage overwhelming and distracting. Can we filter the unnecessary content and make it more personal?
With data retrieved using Twitch API, this minimal-looking web application allows all users to browse and search top game lists and specific Twitch content using keywords in three categories: streams, videos, and clips. For registered users, they will be able to create and save their favorite lists in each category and get personalized recommendations.
- Java
- Java Servlet
- Twitch API
- SQL
- React
- Ant Design 3
- Amazon Web Services
- RESTful API using Java servlets.
- Retrieve real time data through Twitch API and store in MySQL [Twitch API]
- Display popular games retrived from Twitch website for all users.
- Support three search functionality: by top games, by game name, and through favorited collections. [Search Methods]
- Registered user can save and collect favorite clips/streams/videos. [Favorite Feature]
- Content-based reommendation system. [Recommendation System]
- Minimal, content-focused, and clutter-free frontEnd design.
- Optimization on favorite list deletion and update.
- Twitch API is a RESTFUL API that lets developers build creative integrations for the broader Twitch community
- For all users, myTwitchHub offers top game display and will allow client to search content by game name, which will fetch data by calling two Twitch APIs: GetTopGames and getGames
public class TwitchClient {
// Returns the top x streams based on game ID.
private List<Item> searchStreams(String gameId, int limit) throws TwitchException {
List<Item> streams = getItemList(searchTwitch(buildSearchURL(STREAM_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : streams) {
item.setType(ItemType.STREAM);
item.setUrl(TWITCH_BASE_URL + item.getBroadcasterName());
}
return streams;
}
// Returns the top x clips based on game ID.
private List<Item> searchClips(String gameId, int limit) throws TwitchException {
List<Item> clips = getItemList(searchTwitch(buildSearchURL(CLIP_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : clips) {
item.setType(ItemType.CLIP);
}
return clips;
}
// Returns the top x videos based on game ID.
private List<Item> searchVideos(String gameId, int limit) throws TwitchException {
List<Item> videos = getItemList(searchTwitch(buildSearchURL(VIDEO_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : videos) {
item.setType(ItemType.VIDEO);
}
return videos;
}
public List<Item> searchByType(String gameId, ItemType type, int limit) throws TwitchException {
List<Item> items = Collections.emptyList();
switch (type) {
case STREAM:
items = searchStreams(gameId, limit);
break;
case VIDEO:
items = searchVideos(gameId, limit);
break;
case CLIP:
items = searchClips(gameId, limit);
break;
}
// Update gameId for all items. GameId is used by recommendation function
for (Item item : items) {
item.setGameId(gameId);
}
return items;
}
public Map<String, List<Item>> searchItems(String gameId) throws TwitchException {
Map<String, List<Item>> itemMap = new HashMap<>();
for (ItemType type : ItemType.values()) {
itemMap.put(type.toString(), searchByType(gameId, type, DEFAULT_SEARCH_LIMIT));
}
return itemMap;
}
public Game searchGame(String gameName) throws TwitchException {
List<Game> gameList = getGameList(searchTwitch(buildGameURL(GAME_SEARCH_URL_TEMPLATE, gameName, 0)));
if (gameList.size() != 0) {
return gameList.get(0);
}
return null;
}
// Similar to getGameList, convert the json data returned from Twitch to a list of Item objects.
private List<Item> getItemList(String data) throws TwitchException {
ObjectMapper mapper = new ObjectMapper();
try {
return Arrays.asList(mapper.readValue(data, Item[].class));
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new TwitchException("Failed to parse item data from Twitch API");
}
}
// Returns the top x streams based on game ID.
private List<Item> searchStreams(String gameId, int limit) throws TwitchException {
List<Item> streams = getItemList(searchTwitch(buildSearchURL(STREAM_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : streams) {
item.setType(ItemType.STREAM);
item.setUrl(TWITCH_BASE_URL + item.getBroadcasterName());
}
return streams;
}
// Returns the top x clips based on game ID.
private List<Item> searchClips(String gameId, int limit) throws TwitchException {
List<Item> clips = getItemList(searchTwitch(buildSearchURL(CLIP_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : clips) {
item.setType(ItemType.CLIP);
}
return clips;
}
// Returns the top x videos based on game ID.
private List<Item> searchVideos(String gameId, int limit) throws TwitchException {
List<Item> videos = getItemList(searchTwitch(buildSearchURL(VIDEO_SEARCH_URL_TEMPLATE, gameId, limit)));
for (Item item : videos) {
item.setType(ItemType.VIDEO);
}
return videos;
}
public List<Item> searchByType(String gameId, ItemType type, int limit) throws TwitchException {
List<Item> items = Collections.emptyList();
switch (type) {
case STREAM:
items = searchStreams(gameId, limit);
break;
case VIDEO:
items = searchVideos(gameId, limit);
break;
case CLIP:
items = searchClips(gameId, limit);
break;
}
// Update gameId for all items. GameId is used by recommendation function
for (Item item : items) {
item.setGameId(gameId);
}
return items;
}
public Map<String, List<Item>> searchItems(String gameId) throws TwitchException {
Map<String, List<Item>> itemMap = new HashMap<>();
for (ItemType type : ItemType.values()) {
itemMap.put(type.toString(), searchByType(gameId, type, DEFAULT_SEARCH_LIMIT));
}
return itemMap;
}
@WebServlet(name = "FavoriteServlet", urlPatterns = {"/favorite"})
public class FavoriteServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Check if the session is still valid, which means the user has been logged in successfully.
HttpSession session = request.getSession(false);
if (session == null) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
String userId = (String) session.getAttribute("user_id");
// Get favorite item information from request body
FavoriteRequestBody body = ServletUtil.readRequestBody(FavoriteRequestBody.class, request);
if (body == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
MySQLConnection connection = null;
try {
// Save the favorite item to the database
connection = new MySQLConnection();
connection.setFavoriteItem(userId, body.getFavoriteItem());
} catch (MySQLException e) {
throw new ServletException(e);
} finally {
if (connection != null) {
connection.close();
}
}
}
// Insert a favorite record to the database
public void setFavoriteItem(String userId, Item item) throws MySQLException {
if (conn == null) {
System.err.println("DB connection failed");
throw new MySQLException("Failed to connect to Database");
}
// Need to make sure item is added to the database first because the foreign key restriction on item_id(favorite_records) -> id(items)γ
saveItem(item);
// Using ? and preparedStatement to prevent SQL injection
String sql = "INSERT IGNORE INTO favorite_records (user_id, item_id) VALUES (?, ?)";
try {
PreparedStatement statement = conn.prepareStatement(sql);
statement.setString(1, userId);
statement.setString(2, item.getId());
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw new MySQLException("Failed to save favorite item to Database");
}
}
...
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Check if the session is still valid, which means the user has been logged in successfully.
HttpSession session = request.getSession(false);
if (session == null) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
String userId = (String) session.getAttribute("user_id");
Map<String, List<Item>> itemMap;
MySQLConnection connection = null;
try {
// Read the favorite items from the database
connection = new MySQLConnection();
itemMap = connection.getFavoriteItems(userId);
ServletUtil.writeItemMap(response, itemMap);
} catch (MySQLException e) {
throw new ServletException(e);
} finally {
if (connection != null) {
connection.close();
}
}
}
...
// Get favorite items for the given user. The returned map includes three entries like {"Video": [item1, item2, item3], "Stream": [item4, item5, item6], "Clip": [item7, item8, ...]}
public Map<String, List<Item>> getFavoriteItems(String userId) throws MySQLException {
if (conn == null) {
System.err.println("DB connection failed");
throw new MySQLException("Failed to connect to Database");
}
Map<String, List<Item>> itemMap = new HashMap<>();
for (ItemType type : ItemType.values()) {
itemMap.put(type.toString(), new ArrayList<>());
}
Set<String> favoriteItemIds = getFavoriteItemIds(userId);
String sql = "SELECT * FROM items WHERE id = ?";
try {
PreparedStatement statement = conn.prepareStatement(sql);
for (String itemId : favoriteItemIds) {
statement.setString(1, itemId);
ResultSet rs = statement.executeQuery();
if (rs.next()) {
ItemType itemType = ItemType.valueOf(rs.getString("type"));
Item item = new Item.Builder().id(rs.getString("id")).title(rs.getString("title"))
.url(rs.getString("url")).thumbnailUrl(rs.getString("thumbnail_url"))
.broadcasterName(rs.getString("broadcaster_name")).gameId(rs.getString("game_id")).type(itemType).build();
itemMap.get(rs.getString("type")).add(item);
}
}
} catch (SQLException e) {
e.printStackTrace();
throw new MySQLException("Failed to get favorite items from Database");
}
return itemMap;
}
}
public class ItemRecommender {
private static final int DEFAULT_GAME_LIMIT = 3;
private static final int DEFAULT_PER_GAME_RECOMMENDATION_LIMIT = 10;
private static final int DEFAULT_TOTAL_RECOMMENDATION_LIMIT = 20;
private List<Item> recommendByTopGames(ItemType type, List<Game> topGames) throws RecommendationException {
List<Item> recommendedItems = new ArrayList<>();
TwitchClient client = new TwitchClient();
outerloop:
for (Game game : topGames) {
List<Item> items;
try {
items = client.searchByType(game.getId(), type, DEFAULT_PER_GAME_RECOMMENDATION_LIMIT);
} catch (TwitchException e) {
throw new RecommendationException("Failed to get recommendation result");
}
for (Item item : items) {
if (recommendedItems.size() == DEFAULT_TOTAL_RECOMMENDATION_LIMIT) {
break outerloop;
}
recommendedItems.add(item);
}
}
return recommendedItems;
}
// Return a list of Item objects for the given type. Types are one of [Stream, Video, Clip]. All items are related to the items previously favorited by the user. E.g., if a user favorited some videos about game "Just Chatting", then it will return some other videos about the same game.
private List<Item> recommendByFavoriteHistory(
Set<String> favoritedItemIds, List<String> favoriteGameIds, ItemType type) throws RecommendationException {
// Count the favorite game IDs from the database for the given user. E.g. if the favorited game ID list is ["1234", "2345", "2345", "3456"], the returned Map is {"1234": 1, "2345": 2, "3456": 1}
Map<String, Long> favoriteGameIdByCount = favoriteGameIds.parallelStream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Sort the game Id by count. E.g. if the input is {"1234": 1, "2345": 2, "3456": 1}, the returned Map is {"2345": 2, "1234": 1, "3456": 1}
List<Map.Entry<String, Long>> sortedFavoriteGameIdListByCount = new ArrayList<>(
favoriteGameIdByCount.entrySet());
sortedFavoriteGameIdListByCount.sort((Map.Entry<String, Long> e1, Map.Entry<String, Long> e2) -> Long
.compare(e2.getValue(), e1.getValue()));
if (sortedFavoriteGameIdListByCount.size() > DEFAULT_GAME_LIMIT) {
sortedFavoriteGameIdListByCount = sortedFavoriteGameIdListByCount.subList(0, DEFAULT_GAME_LIMIT);
}
List<Item> recommendedItems = new ArrayList<>();
TwitchClient client = new TwitchClient();
// Search Twitch based on the favorite game IDs returned in the last step.
outerloop:
for (Map.Entry<String, Long> favoriteGame : sortedFavoriteGameIdListByCount) {
List<Item> items;
try {
items = client.searchByType(favoriteGame.getKey(), type, DEFAULT_PER_GAME_RECOMMENDATION_LIMIT);
} catch (TwitchException e) {
throw new RecommendationException("Failed to get recommendation result");
}
for (Item item : items) {
if (recommendedItems.size() == DEFAULT_TOTAL_RECOMMENDATION_LIMIT) {
break outerloop;
}
if (!favoritedItemIds.contains(item.getId())) {
recommendedItems.add(item);
}
}
}
return recommendedItems;
}
// Return a map of Item objects as the recommendation result. Keys of the may are [Stream, Video, Clip]. Each key is corresponding to a list of Items objects, each item object is a recommended item based on the previous favorite records by the user.
public Map<String, List<Item>> recommendItemsByUser(String userId) throws RecommendationException {
Map<String, List<Item>> recommendedItemMap = new HashMap<>();
Set<String> favoriteItemIds;
Map<String, List<String>> favoriteGameIds;
MySQLConnection connection = null;
try {
connection = new MySQLConnection();
favoriteItemIds = connection.getFavoriteItemIds(userId);
favoriteGameIds = connection.getFavoriteGameIds(favoriteItemIds);
} catch (MySQLException e) {
throw new RecommendationException("Failed to get user favorite history for recommendation");
} finally {
connection.close();
}
for (Map.Entry<String, List<String>> entry : favoriteGameIds.entrySet()) {
if (entry.getValue().size() == 0) {
TwitchClient client = new TwitchClient();
List<Game> topGames;
try {
topGames = client.topGames(DEFAULT_GAME_LIMIT);
} catch (TwitchException e) {
throw new RecommendationException("Failed to get game data for recommendation");
}
recommendedItemMap.put(entry.getKey(), recommendByTopGames(ItemType.valueOf(entry.getKey()), topGames));
} else {
recommendedItemMap.put(entry.getKey(), recommendByFavoriteHistory(favoriteItemIds, entry.getValue(), ItemType.valueOf(entry.getKey())));
}
}
return recommendedItemMap;
}
// Return a map of Item objects as the recommendation result. Keys of the may are [Stream, Video, Clip]. Each key is corresponding to a list of Items objects, each item object is a recommended item based on the top games currently on Twitch.
public Map<String, List<Item>> recommendItemsByDefault() throws RecommendationException {
Map<String, List<Item>> recommendedItemMap = new HashMap<>();
TwitchClient client = new TwitchClient();
List<Game> topGames;
try {
topGames = client.topGames(DEFAULT_GAME_LIMIT);
} catch (TwitchException e) {
throw new RecommendationException("Failed to get game data for recommendation");
}
for (ItemType type : ItemType.values()) {
recommendedItemMap.put(type.toString(), recommendByTopGames(type, topGames));
}
return recommendedItemMap;
}