Tracking the Worst Sci-Fi Movies With Angular and Slash GraphQL

Selecting Dgraph’s Slash GraphQL

  • Dgraph Slash GraphQL to house the data
  • Dgraph Slash GraphQL to provide a GraphQL API for accessing the data
  • Angular CLI to create an application for presenting the data

Getting Data From IMDb

  • genre must include “Sci-Fi”
  • limited to movie types (exclude shorts, made-for-TV movies, series, etc.)
  • excludes titles with fewer than 500 ratings

IMDb Datasets

  • title.basics.tsv.gz — contains basic information for IMDb titles
  • title.ratings.tsv.gz — contains the ratings and votes for IMDB titles

Filtering the TSV Files Using Java

  • read each line of the title.basics.tsv file
  • determine if the line contains the “Sci-Fi” genre
  • if so, capture the title ID attribute as the key to a Map<String, String> and place the entire line as the value of the map
  • if any match is found, process the title.ratings.tsv file:
  • read each line of the ratings file and capture the title ID
  • if the title ID attribute exists in the Map<String, String>, add the rating and votes data to the value of the map entry
  • create a new TSV file which contains the Sci-Fi title information, plus the average user rating and number of votes
public class Application {
private static final String DEFAULT_GENRE = "Sci-Fi";
private static final String USER_HOME = "user.home";
private static final String DELIMITER = "\t";
private static final String TITLE_BASICS_TSV_FILE_LOCATION = "/downloads/title.basics.tsv";private static final String TITLE_RATINGS_FILE_LOCATION = "/downloads/title.ratings.tsv";private static final String DESTINATION_FILE = "/downloads/filtered.tsv";public static void main(String[] args) throws IOException {
String genre = DEFAULT_GENRE;
if (args != null && args.length > 0) {
genre = args[0];
}
Collection<String> data = filterData(TITLE_BASICS_TSV_FILE_LOCATION, genre);if (CollectionUtils.isNotEmpty(data)) {
writeFile(data, DESTINATION_FILE);
}
}
...
}
private static Collection<String> filterData(String fileName, String genre) throws IOException {
Map<String, String> data = new HashMap<>();
try (BufferedReader br = new BufferedReader(new FileReader(System.getProperty(USER_HOME) + fileName))) {
String string;
long lineNumber = 0;
while ((string = br.readLine()) != null) {
if (lineNumber > 0 && StringUtils.contains(string, genre)) {
String firstItem = StringUtils.substringBefore(string, DELIMITER);
data.put(firstItem, string);
}
logResults(lineNumber, fileName);
lineNumber++;
}
if (MapUtils.isNotEmpty(data)) {
appendUserRatings(data, TITLE_RATINGS_FILE_LOCATION);
}
}
return data.values();
}
private static void appendUserRatings(Map<String, String> data, String fileName) throws IOException {try (BufferedReader br = new BufferedReader(new FileReader(System.getProperty(USER_HOME) + fileName))) {
String string;
long lineNumber = 0;
while ((string = br.readLine()) != null) {
if (lineNumber > 0) {
String firstItem = StringUtils.substringBefore(string, DELIMITER);
if (data.containsKey(firstItem)) {
data.put(firstItem, data.get(firstItem) + DELIMITER + StringUtils.substringAfter(string, DELIMITER));
}
}
logResults(lineNumber, fileName);
lineNumber++;
}
}
}
private static void writeFile(Collection<String> data, String fileName) throws IOException {try (BufferedWriter bw = new BufferedWriter(new FileWriter(System.getProperty(USER_HOME) + fileName))) {
for (String str : data) {
bw.write(str);
bw.newLine();
}
}
}
private static void logResults(long lineNumber, String fileName) {
if (lineNumber % 10000 == 0) {
System.out.println("Completed " + lineNumber + " " + fileName + " records");
}
}

Locating the Bottom 125

  • id
  • titleType
  • primaryTitle
  • originalTitle
  • isAdult
  • startYear
  • endYear
  • runtimeMinutes
  • genres
  • averageRating
  • numVotes
  • only “movie” value for the titleType column
  • remove any values where isAdult is greater than zero
  • only items which have a value greater than or equal to 500 in the numVotes column
  • id
  • primaryTitle (which will become title)
  • startYear (which will become releaseYear)
  • runtimeMinutes
  • genres (which will become genre)
  • averageRating
  • numVotes (which will become votes)
{id:"tt5311054", title:"Browncoats: Independence War", releaseYear:2015,runtimeMinutes:98,genre:"Action,Sci-Fi,War",averageRating:1.1,votes:717},

Using Slash GraphQL

type Movie {
id: String! @id @search(by: [hash])
title: String! @search(by: [fulltext])
releaseYear: Int! @search
runtimeMinutes: Int!
genre: String! @search(by: [fulltext])
averageRating: Float! @search
votes: Int! @search
seen: User
}
type User {
username: String! @id @search(by: [hash])
movies: [Movie] @hasInverse(field: seen)
}
mutation AddMovies {
addMovie(input: [
{id:"tt5311054", title:"Browncoats: Independence War", releaseYear:2015,runtimeMinutes:98,genre:"Action,Sci-Fi,War",averageRating:1.1,votes:717},
{id:"tt2205589", title:"Rise of the Black Bat", releaseYear:2012,runtimeMinutes:80,genre:"Action,Sci-Fi",averageRating:1.2,votes:690},
{id:"tt1854506", title:"Aliens vs. Avatars", releaseYear:2011,runtimeMinutes:80,genre:"Horror,Sci-Fi",averageRating:1.5,votes:1584},
... more JSON data here ...
{id:"tt0068313", title:"Brain of Blood", releaseYear:1971,runtimeMinutes:87,genre:"Horror,Sci-Fi",averageRating:2.9,votes:727},
{id:"tt1754438", title:"Robotropolis", releaseYear:2011,runtimeMinutes:85,genre:"Action,Adventure,Sci-Fi",averageRating:2.9,votes:1180}
])
}
mutation AddUser {
addUser(input:
[
{
username: "johnjvester",
movies: [
{id: "tt0052286"},
{id: "tt0077834"},
{id: "tt0145529"},
{id: "tt0053464"},
{id: "tt0060074"},
{id: "tt0075343"},
{id: "tt0089280"},
{id: "tt0059464"},
{id: "tt0055562"}
]
}
]) {
numUids
}
}

Adding the Movie Poster

  • Visit http://www.omdbapi.com/apikey.aspx to request an API key.
  • Select the FREE option and provide an email address.
  • Single-click the Submit button and follow any required follow-up steps.
  • Note the “Here is your key” value provided via email from The OMDb API.

Creating the Angular Application

@Injectable({
providedIn: 'root'
})
export class GraphQLService {
allMovies:string = '{queryMovie(filter: {}) {votes, title, runtimeMinutes, releaseYear, id, genre, averageRating}}';
singleUserPrefix:string = '{getUser(username:"';
singleUserSuffix:string = '"){username,movies{title,id}}}';
constructor(private http: HttpClient) { }
baseUrl: string = environment.api;
getMovies() {
return this.http.get<QueryMovieResponse>(this.baseUrl + '?query=' + this.allMovies).pipe(
tap(),
catchError(err => { return ErrorUtils.errorHandler(err)
}));
}
getUser(username:string) {
return this.http.get<GetUserResponse>(this.baseUrl + '?query=' + this.singleUserPrefix + username + this.singleUserSuffix).pipe(
tap(),
catchError(err => { return ErrorUtils.errorHandler(err)
}));
}
}
@Injectable({
providedIn: 'root'
})
export class OmdbService {
constructor(private http: HttpClient) {
baseUrl: string = environment.omdbApi + environment.omdbKey;
getMoviePoster(id:string) {
return this.http.get<any>(this.baseUrl + '&i=' + id).pipe(
tap(),
catchError(err => { return ErrorUtils.errorHandler(err)
}));
}
}
export const environment = {
production: false,
api: 'https://some-host-instance.us-west-2.aws.cloud.dgraph.io/graphql',
omdbApi: 'http://www.omdbapi.com/?apikey=',
omdbKey: 'omdbApiKeyGoesHere'
};
ngOnInit() {
this.graphQlService.getMovies()
.subscribe(data => {
if (data) {
let queryMovieResponse: QueryMovieResponse = data;
this.movies = queryMovieResponse.data.queryMovie;
this.movies.sort((a, b) => (a.title > b.title) ? 1 : -1)
}
}, (error) => {
console.error('error', error);
}).add(() => {
});
}
ngOnInit() {
if (this.movie && this.movie.id) {
this.omdbService.getMoviePoster(this.movie.id)
.subscribe(data => {
if (data && data.Poster) {
this.posterUrl = data.Poster;
this.graphQlService.getUser(this.username)
.subscribe(getUserResponse => {
if (getUserResponse && getUserResponse.data && getUserResponse.data.getUser) {
this.user = getUserResponse.data.getUser;
this.hasSeenThisMovie();
}
}, (error) => {
console.error('error', error);
}).add(() => {
});
}
}, (error) => {
console.error('error', error);
}).add(() => {
});
}
}
  • @ng-bootstrap/ng-bootstrap: ^5.3.1
  • angular-star-rating: ^4.0.0-beta.3
  • bootstrap: ^4.5.2
  • css-star-rating: ^1.2.4

Using the Angular Application

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store