Intro to my work:
My CPT project from last tri, and what I integrated into our data structures site, was a video uploading/sharing site where users could login, upload, and view videos through our frontend. Our videos and user information were stored in the backend as data in our DB as well as various static files in the /videos directory.
Collections
We used an SQLite DB to store our videos for our CPT integration. Each row contains: A video ID, name, thumbnail image(stored using base 64), description, video owner(denoted by user ID), genre, views, likes, and dislikes. This is a lot of data to store, so in order to be scalable using a DB was the best option. Here is the initVideos function that we use to initialize the DB with our 2 example videos: In our create() method we take in the data given and then add it to the DB, including using our static /videos directory to ensure the videos are stored.
Lists and Dictionaries
In the GET method we get the video database info and return it as JSON, by converting it to a list of dictionaries: This is just a simple Python list of all the videos and their corresponding metadata, being returned by the API to the frontend via JSON. Use of dictionary #1 – initializing DB with example video: This dictionary is created from the Video class – it adds the default videos in the site to the DB. Use of dictionary #2 – GET method uses video DB table to send video data, along with metadata like description and views to the frontend using conversion to dictionary:
APIs and JSON
Python API definitions for API and CRUD methods:
class VideoAPI:
class _CRUD(Resource): # User API operation for Create, Read. THe Update, Delete methods need to be implemeented
def put(self):
body = request.get_json()
type = int(body.get('type'))
videoID = int(body.get('videoID'))
if videoID is None:
return {'message': f'Video ID is missing'}, 400
video = Vid.query.filter_by(_videoID=videoID).first()
if video:
if type == 0:
try:
put_req = video.put()
return jsonify(video.read())
except Exception as e:
return {
"error": "Something went wrong",
"message": str(e)
}, 500
elif type == 1:
try:
put_req = video.like()
return jsonify(video.read())
except Exception as e:
return {
"error": "Something went wrong",
"message": str(e)
}, 500
elif type == 2:
try:
put_req = video.dislike()
return jsonify(video.read())
except Exception as e:
return {
"error": "Something went wrong",
"message": str(e)
}, 500
@token_required
def post(self, current_user): # Create method
''' Read data for json body '''
if request.is_json:
body = request.get_json()
''' Avoid garbage in, error checking '''
name = body.get('name')
if name is None:
return {'message': f'name is missing, or is less than 2 characters'}, 400
description = body.get('description')
if description is None:
return {'message': f'Description is missing, or is less than 2 characters'}, 400
# look for password and dob
base64 = body.get('base64')
if base64 is None:
return {'message': f'Thumbnail is missing or in the wrong format'}, 400
video = body.get('video')
if video is None:
return {'message': f'Video is missing or in the wrong format'}, 400
userID = body.get('uid')
if userID is None:
return {'message': f'userID is missing or in the wrong format'}, 400
thumb_name = body.get('thumbnail')
if thumb_name is None:
return {'message': f'Thumbnail name is missing or in the wrong format'}, 400
genre = body.get('genre')
print(genre)
if thumb_name is None:
return {'message': f'Genre is missing or in the wrong format'}, 400
''' #1: Key code block, setup USER OBJECT '''
vid = Vid(name=name, thumbnail=thumb_name,description=description,video=video,userID=userID,views=0,genre=genre)
# create video in database
videoJ = vid.create(base64)
# success returns json of video
if videoJ:
return jsonify(videoJ.read())
# failure returns error
return {'message': f'Processed {name}, either a format error or ID {id} is duplicate'}, 400
else:
video_file = request.files['video']
# Check if the file has a filename
if video_file.filename == '':
return 'No selected file', 400
# Save the video file to the 'videos' directory
video_userID = os.path.join('videos', video_file.filename)
video_file.save(video_userID)
def get(self): # Read Method
videos = Vid.query.all() # read/extract all users from database
json_ready = [video.read() for video in videos] # prepare output in json
return jsonify(json_ready) # jsonify creates Flask response object, more specific to APIs than json.dumps
class _ReadVID(Resource):
def get(self, vid):
video = Vid.query.filter_by(_videoID=vid).first()
data = video.read()
return jsonify(data)
# def put(self,)
class _Recommend(Resource):
def get(self, uid):
# Get user preferences
user = Users.query.filter_by(_uid=uid).first()
if user is None:
return jsonify({"message": "User not found"}), 404
user_preferences = user.preferences
# Get all videos
videos = Vid.query.all()
# Filter videos based on matching genres
matching_videos = []
for video in videos:
if any(pref in video.genre for pref in user_preferences):
matching_videos.append(video)
# Calculate like to dislike ratio
for video in matching_videos:
if video.dislikes != 0:
video.like_to_dislike_ratio = video.likes / video.dislikes
else:
video.like_to_dislike_ratio = video.likes
sorted_videos = sorted(matching_videos, key=lambda x: x.views, reverse=True)
# Sort matching videos based on like to dislike ratio
sorted_videos = sorted(sorted_videos, key=lambda x: x.like_to_dislike_ratio, reverse=True)
# Sort by highest to lowest views within each ratio group
# Prepare JSON response
json_ready = [video.read() for video in sorted_videos]
return jsonify(json_ready)
Defining the above as an API resource allows Flask to add each path with the methods specified in the function names, so def post()
and def get()
would add the functions as POST request and GET request handling in the Flask app, respectively.
These functions are designed to handle the methods given their name; the @token_required is used for methods that require a login(like uploading videos)
The following section of code validates the POST JSON data to make sure formatting is correct(making sure that the body has all fields necessary to complete the POST and making sure that the data given is not invalid). If not, then an error is raised and JSON is sent back to the request that describes the specific error that caused the POST to fail:
Postman
PUT: This put request adds 1 the view count of a video(telling the backend that a user has viewed this video); here I used Postman to add a view to an example video with the ID of 2. GET: This GET request recieves all of the videos currently in the video DB; in Postman here you can see the returned base64 data for the thumbnail image in the first video. POST: This POST request does a login to the backend, which returns a jwt cookie for persistent authentication on the actual frontend. This JWT cookie ensures security and allows users to stay logged in through all the pages in the frontend. 400 Error with POST request(missing userID): This POST request is missing a userID for logging in, so the backend returns a 400 Bad Request HTTP error. 404 Error with unrecognized video ID in view update PUT: When a video ID like 24(in this case, no video with ID 24 exists), the backend returns a 404 not found error, since no video was found that matches the ID of 24.
Frontend
Browser Inspect: GET: This is the inspect window showing the network traffic(packet returned from the backend) when doing a GET request by loading the main video page in the frontend. POST: The POST login returns a 200 OK response and a JWT token. PUT: The PUT request updates the views and returns the updated video data to confirm with the frontend or whoever is accessing the backend.
Formatting of Videos in view_videos.html – Videos are formatted in grid with thumbnails, titles, and view counts:
The following code:
- Fetches all the videos from the backend Flask API endpoint using a JavaScript GET Fetch
- Renders the videos in a grid format, with the proper title, thumbnail, view counts, and href redirects to access the specific videos
- Encases everything in a try/catch for error handling, in case something goes wrong
%%javascript
// Fetch all the videos using GET
try {
const apiUrl = "http://127.0.0.1:6221/api/video"
const response = await fetch(apiUrl);
const videos = await response.json();
const query = document.getElementById("query").value;
clearVideos()
renderVideos(videos, query);
} catch (error) {
console.error('Error loading videos:', error);
}
// Following JS code formats all the videos into a grid format, displaying the images as well
videos.forEach(video => {
const videoItem = document.createElement('div');
videoItem.classList.add('grid-item');
videoItem.classList.add('video-item');
const videoLink = document.createElement('a');
videoLink.href = "video.html" + "?videoID=" + video["videoID"]; // Assuming video object has a 'video' property for the URL
const videoImage = document.createElement('img');
videoImage.classList.add('video');
var encode = `${video.base64}`;
var step1 = encode.replace(/\\n/g, '');
var step2 = step1.replace(/b'/g, '');
var base64String = step2.replace(/[-:'\\]/g, '');
videoImage.src = `data:image/png;base64,${base64String}`;
const title = document.createElement('span');
title.textContent = video.name; // Assuming video object has a 'name' property for the title
const viewCount = document.createElement('span');
viewCount.textContent = `Views: ${video.views}`; // Assuming video object has a 'views' property
viewCount.classList.add('view-count'); // Add a class for styling
const lineBreak = document.createElement('br');
// Append the videoLink to the videoItem
videoItem.appendChild(videoLink);
// Append the videoImage inside the videoLink
videoLink.appendChild(videoImage);
// Append the title, view count, and line break to the videoItem
videoItem.appendChild(title);
videoItem.appendChild(lineBreak);
videoItem.appendChild(viewCount);
// Append the videoItem to the videoGrid
videoGrid.appendChild(videoItem);
});
Add view on video load: Inspect request: This is how the inspect window looks upon a successful PUT request from the frontend to the backend. Frontend update: The HTML on the frontend updates the views on the page load. Invalid PUT Request: An incorrectly formatted PUT request or malformed packet will return an error from the backend.
Code:
- Reloads view count and loads the video on-screen in the webpage
- Uses a fetch statement to send a PUT request to the proper Flask API endpoint, with the correct videoID
- Uses a try/catch on the fetch statement for error handling
%%javascript
// Display views on success
views.innerText = data["views"] + " views";
source.src = data["video"];
video.load();
// Actual fetch request
try {
const url = "http://127.0.0.1:6221/api/video/"
fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ videoID: videoID, type: '0' }) //JSON data
})
} catch (error) {
// Log error message for reference
console.error('Error:', error);
}