Skip to content

A full-stack web application for users to search Twitch resources in three categories: stream, videos, and clips, and get recommendations

Notifications You must be signed in to change notification settings

Tianyao-Ma/MyGameHub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

40 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MyGameHub

-----------------------------------------------------

🎬 About the project

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.

-----------------------------------------------------

🎞️ Project Demo

πŸ€– Tech Stack

  • Java
  • Java Servlet
  • Twitch API
  • SQL
  • React
  • Ant Design 3
  • Amazon Web Services

πŸ“ Design Doc

πŸ”₯ Key Features

  • 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.

🌱 For Furture Improvement

  • Optimization on favorite list deletion and update.

-----------------------------------------------------

πŸ—’οΈ Sample Code

Twitch API

Retrieve real-time data from Twitch using Twitch API

  • 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;
    }

Search Methods

Search by game name, search by game type

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;
    }
    

Set and Unset Favorite Items

Support registered users to save and unsave favorite clips/videos/streams with SQL

@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;
    }
}

Content Based Recommendation

Implement Content-based Recommendation for Twitch Items
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;
  }

About

A full-stack web application for users to search Twitch resources in three categories: stream, videos, and clips, and get recommendations

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published