7 Cinema Pre-show Generator¶
Hi everyone! In this chapter, we will learn how to create a cinema pre-show generator. What exactly is a cinema pre-show? Have you ever observed the advertisements, public service messages, and movie trailers which run before the actual movie in a cinema? Well, all of that comes under pre-show.
I came up with the idea for this project during a movie night with a group of my friends. We love watching movies in our dorm and we love talking about upcoming movies. The only problem is that we have to actively go out and search for new movie trailers. If we go to a cinema, we skip that part because the cinema automatically shows us trailers for upcoming movies. I wanted to replicate the same environment during our cozy movie nights. What if before the start of a movie during our private screening we can play trailers for upcoming movies that have the same genre as the movie we are currently starting?
Perfect, time to work on a delicious new project and improve our programming skills!
Normally, cinema folks use video editing software to stitch together multiple videos/trailers to generate that pre-show. But we are programmers! Surely we can do better than that?
Our project will be able to generate an automatic pre-show consisting of 3 (or more) trailers for upcoming flicks related to the current one we are going to watch. It will also add in the “put your phones on silent mode” message (have you been bothered by phones ringing during a movie? Me too…) and the pre-movie countdown timer (the timer ticks give me goosebumps).
The script side of the final product of this chapter will look something like Fig. 7.1.
Through this project, you will learn how to use moviepy, tmdbsimple, make automated Google searches, and automate video downloads from Apple trailers. So without any further ado, let’s get started!
7.1 Setting up the project¶
We will be using the following libraries:
moviepy
tmdbsimple
google
Start by creating a “pre_show” folder for this project. Then, create a virtual environment:
$ python -m venv env
$ source env/bin/activate
Now let’s install the required libraries using pip
:
$ touch requirements.txt
$ echo "tmdbsimple\ngoogle\nmoviepy" > requirements.txt
$ pip install -r requirements.txt
moviepy
has extra dependencies which you might need to install as well (if the PIP installation fails). You can find the installation instructions for moviepy
here.
Now create an app.py
file inside the pre_show
folder and import the following modules:
from moviepy.editor import (VideoFileClip,
concatenate_videoclips)
import tmdbsimple as tmdb
from googlesearch import search
7.2 Sourcing video resources¶
Now we need to source our videos from somewhere. We will be downloading the movie trailers automatically but we still need to download the rest of the videos manually. The rest of the videos include the countdown and the “put your phones on silent” video. I will be using this free countdown video and this free “turn your cell phones off” video. Download both of these videos before you move on. The download instructions are in the video descriptions.
If for some reason these videos aren’t available, any other video will work fine as well. Just make sure that you update the script later on to reference these new videos.
7.3 Getting movie information¶
The next step is to figure out the genre of the movie that we are planning on watching. This way we can play only those upcoming movie trailers which belong to the same genre. We will be using The Movie DB for this. Before we move on, please go to tmdb, create an account and signup for an API key. It’s completely free, so don’t worry about spending a dime on this. TMDB change their website frequently so chances are that you might need to follow slightly different steps to get an API key than the steps I show below. This should not be a huge issue as the new navigation would still be fairly intuitive.
Now we can search for a movie on tmdb by using the following Python code (Replace "YOUR API KEY"
with your actual API key):
tmdb.API_KEY = "YOUR API KEY"
search = tmdb.Search()
response = search.movie(query="The Prestige")
print(search.results[0]['title'])
Note
Save this code in an app.py
file. We will be making changes to this file throughout this tutorial.
Now run this code with this command:
$ python app.py
This code simply creates a tmdb.Search()
object and searches for a movie by using the movie()
method of the tmdb.Search()
object. The result is a list containing Python dictionaries. We extract the first element (movie dictionary) from the list and print value associated with the key title
.
tmdb
also makes it super easy to search for upcoming movies:
upcoming = tmdb.Movies()
response = upcoming.upcoming()
for movie in response['results']:
print(movie['title'])
This code is also similar to the previous one. It creates a tmdb.Movies()
object and then prints the titles of the upcoming movies. When I am personally working with a JSON API, I love exploring the full response of an API call. My favourite way to do that is to copy the complete JSON output of a function call and pasting that on JSBeautifier Fig. 7.5. The auto-indentation makes it super easy to get a general feel of the data one is working with.
Almost every movie has multiple genres. “The Prestige” has three:
18: drama
9648: mystery
53: thriller
The numbers before the genre names are just internal IDs TMDB uses for each genre.
Let’s filter these upcoming movies based on genres. As we already know that most movies have multiple genres, we need to decide which genre we will be using to filter out the upcoming movies. It is a bit rare for two movies to share the exact same list of genres so we can not simply compare this whole list in its entirety. I personally decided to compare only the first returned genre which in this case is “drama”. This is how we can filter the upcoming movies list:
for movie in response['results']:
if search.results[0]['genre_ids'][0] in movie['genre_ids']:
print(movie['title'])
The above code produced the following output for me:
Sicario: Day of the Soldado
Terminal
Hereditary
Beirut
Loving Pablo
12 Strong
Marrowbone
Skyscraper
The Yellow Birds
Mary Shelley
We can also make the genre selection more interesting by randomly choosing a genre:
from random import choice
for movie in response['results']:
if choice(search.results[0]['genre_ids']) in movie['genre_ids']:
print(movie['title'])
choice(list) randomly picks a value from a list.
Cool! now we can search for the trailers for the first three movies.
7.4 Downloading the trailers¶
Apple stores high definition trailers for all upcoming movies but does not provide an API to programmatically query its database and download the trailers. We need a creative solution. I searched around and found out that all of the trailers are stored on the trailers.apple.com
domain. Can we somehow use this information to search for trailers on that domain? The answer is a resounding yes! We need to reach out to our friend Google and use something called Google Dorks.
According to WhatIs.com:
A Google dork query, sometimes just referred to as a dork, is a search string that uses advanced search operators to find information that is not readily available on a website.
In plain words, a Google Dork allows us to limit our search based on specific parameters. For instance, we can use Google to search for some query string on a specific website. This will make sure Google does not return results containing any other website which might contain that string.
The dork which we will be using today is site:trailers.apple.com <movie name>
(replace movie name with actual movie name).
Try doing a search for The Incredibles on Google with that dork. The output should be similar to Fig. 7.6.
Congrats! We are one step closer to our final goal. Now we need to figure out two things. First, how to automate Google searches, and second, how to download trailers from Apple. The first problem can be solved by this library and the second problem can be solved by this one. Aren’t you glad that Python has a library for almost everything?
We have already installed the google
library but we haven’t installed the apple_trailer_downloader
because we can’t install it using pip
. What we have to do is that we have to save this file in our current app.py
folder.
Now, let’s run the same Google dork using googlesearch
:
from googlesearch import search
for url in search('site:trailers.apple.com The Incredibles 2', stop=10):
print(url)
The current code is going to give us 10 results. You can change the number of results returned by changing the stop
argument. The output should resemble this:
https://trailers.apple.com/trailers/disney/incredibles-2/
https://trailers.apple.com/trailers/disney/the_incredibles/
https://trailers.apple.com/ca/disney/incredibles-2/
https://trailers.apple.com/trailers/disney/the_incredibles/trailer2_small.html
https://trailers.apple.com/trailers/genres/family/
https://trailers.apple.com/
https://trailers.apple.com/ca/disney/?sort=title_1
https://trailers.apple.com/trailers/disney/
https://trailers.apple.com/ca/genres/family/
https://trailers.apple.com/trailers/genres/family/index_abc5.html
https://trailers.apple.com/ca/
https://trailers.apple.com/trailers/genres/comedy/?page=2
Amazing! The first URL is exactly the one we are looking for. At this point, I ran this command with a bunch of different movie names just to confirm that the first result is always the one we are looking for.
Now, let’s use apple_trailer_downloader
to download the trailer from that first URL. Instead of getting the URL from the search
method, I am going to hardcode a URL and use that as a basis to work on the download feature. This is super helpful because you reduce the dynamic nature of your code. If the download part isn’t working fine you don’t have to go back and test the search part as well.
Once we are fairly confident that the download part is working as expected, we can integrate both of these parts together. Let’s go ahead and write down the download part of the code and test it:
import os
from download_trailers import (get_trailer_file_urls,
download_trailer_file,
get_trailer_filename)
page_url = "https://trailers.apple.com/trailers/disney/incredibles-2/"
destdir = os.getcwd()
trailer_url = get_trailer_file_urls(page_url, "720", "single_trailer", [])[0]
trailer_file_name = get_trailer_filename(
trailer_url['title'],
trailer_url['type'],
trailer_url['res']
)
if not os.path.exists(trailer_file_name):
download_trailer_file(trailer_url['url'], destdir, trailer_file_name)
If everything is correctly set-up, this should download the trailer for “Incredibles 2” in your current project folder with the name of Incredibles 2.Trailer.720p.mov
.
The important parts in this code are os.getcwd()
and os.path.exists(trailer_file_name)
.
os.getcwd()
stands for “get current working directory”. It is the directory from which you are running the code. If you are running the code from your project folder, it will return the path of your project folder.
os.path.exists(trailer_file_name)
checks if there is a path that exists on the system or not. This essentially helps us check if there is a trailer file with the same name downloaded before or not. If there is a file with the same name downloaded in the current directory, it will skip the download. Hence, running the code second time should not do anything.
Now let’s take a look at merging these videos/trailers using moviepy
.
7.5 Merging trailers together¶
Moviepy
makes merging videos extremely easy. The code required to merge two trailers with the names: Incredibles 2.Trailer.720p.mov
and Woman Walks Ahead.Trailer.720p.mov
is:
from moviepy.editor import (VideoFileClip,
concatenate_videoclips)
clip1 = VideoFileClip('Woman Walks Ahead.Trailer.720p.mov')
clip2 = VideoFileClip('Incredibles 2.Trailer.720p.mov')
final_clip = concatenate_videoclips([clip1, clip2])
final_clip.write_videofile("combined trailers.mp4")
Firstly, we create VideoFileClip()
objects for each video file. Then we use the concatenate_videoclips()
function to merge the two clips and finally we use the write_videofile()
method to save the merged clip in a combined trailers.mp4
file. This will also convert the file type from mov
to mp4
.
At this point your project folder should have the following files:
$ ls pre_show
Woman Walks Ahead.Trailer.720p.mov
Incredibles 2.Trailer.720p.mov
turn-off.mkv
countdown.mp4
venv
requirements.txt
app.py
Let’s go ahead and also merge in our “turn your cell phones off” video and the countdown video:
from moviepy.editor import (
VideoFileClip,
concatenate_videoclips
)
clip1 = VideoFileClip('Woman Walks Ahead.Trailer.720p.mov')
clip2 = VideoFileClip('Incredibles 2.Trailer.720p.mov')
clip3 = VideoFileClip('turn-off.mkv')
clip4 = VideoFileClip('countdown.mp4')
final_clip = concatenate_videoclips([clip1, clip2, clip3, clip4])
final_clip.write_videofile("combined trailers.mp4")
The output from the generated video will look something like Fig. 7.7.
This certainly doesn’t seem right. None of our sources contained a grainy video like this. The output .mp4
file was also corrupted from near the end.
This issue had me pulling out my hair for a whole day. I searched around almost everywhere but couldn’t find any solution. Finally, I found a forum post somewhere where someone else was having the same problem. His issue was resolved by passing in the method='compose'
keyword argument to the concatenate_videoclips
function. It was such a simple fix that I felt super stupid and wanted to bang my head against the wall one more time.
This argument is required in this case because our separate video files are of different dimensions. The trailers are 1280x544
whereas the countdown video and the “turn your cell phones off” video is 1920x1080
. Official docs have a proper explanation of this argument:
``method=”compose”``: if the clips do not have the same resolution, the final resolution will be such that no clip has to be resized. As a consequence, the final clip has the height of the highest clip and the width of the widest clip of the list. All the clips with smaller dimensions will appear centered. The border will be transparent if mask=True, else it will be of the color specified by
bg_color
.
We can use it like this:
final_clip = concatenate_videoclips([clip1, clip2, clip3, clip4],
method="compose")
However, we can not use this argument as-it-is because that will result in trailers taking less space on screen (Fig. 7.8) and the countdown timer taking up more space (Fig. 7.9).
This is because moviepy by default tries to preserve the biggest width and height from all the clips. What I ended up doing was that I reduced the size of my two bigger clips by 40% and then I merged all of the videos together. It resulted in something similar to Fig. 7.10 and Fig. 7.11:
The code required for doing that is this:
from moviepy.editor import (VideoFileClip,
concatenate_videoclips)
clip1 = VideoFileClip('Woman Walks Ahead.Trailer.720p.mov')
clip2 = VideoFileClip('Incredibles 2.Trailer.720p.mov')
clip3 = VideoFileClip('turn-off.mkv').resize(0.60)
clip4 = VideoFileClip('countdown.mp4').resize(0.60)
final_clip = concatenate_videoclips([clip1, clip2, clip3, clip4],
method="compose")
final_clip.write_videofile("combined trailers.mp4")
I later found out that we can pass in a video URL to VideoFileClip
as well. This way we will not have to download videos using download_trailers.py
file and moviepy
will take care of downloading automatically. It is done like this:
clip1 = VideoFileClip(trailer['url'])
Before we move on we should combine our code uptil now.
7.6 Final Code¶
This is what we have so far:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import os import sys from moviepy.editor import (VideoFileClip, concatenate_videoclips) import tmdbsimple as tmdb from googlesearch import search as googlesearch from download_trailers import get_trailer_file_urls tmdb.API_KEY = "YOUR API KEY" query = sys.argv[-1] print("[Pre-show Generator] Movie:", query) search = tmdb.Search() response = search.movie(query=query) upcoming = tmdb.Movies() response = upcoming.upcoming() similar_movies = [] for movie in response['results']: if search.results[0]['genre_ids'][0] in movie['genre_ids']: similar_movies.append(movie) print('[Pre-show Generator] Which movies seem interesting?\ Type the indexes like this: 3,4,6 \n') for c, movie in enumerate(similar_movies): print(c+1, ".", movie['title']) select_movies = input('[Pre-show Generator] Ans: ') select_movies = [int(index)-1 for index in select_movies.split(',')] final_movie_list = [similar_movies[index] for index in select_movies] print('[Pre-show Generator] Searching trailers') trailer_urls = [] for movie in final_movie_list: for url in googlesearch('site:trailers.apple.com ' + movie['title'], stop=10): break trailer = get_trailer_file_urls(url, "720", "single_trailer", [])[0] trailer_urls.append(trailer['url']) print('[Pre-show Generator] Combining trailers') trailer_clips = [VideoFileClip(url) for url in trailer_urls] trailer_clips.append(VideoFileClip('turn-off.mp4').resize(0.60)) trailer_clips.append(VideoFileClip('countdown.mp4').resize(0.60)) final_clip = concatenate_videoclips(trailer_clips, method="compose") final_clip.write_videofile("combined trailers.mp4") |
I made the code a bit more user friendly by adding in helpful print
statements. The user is also given the choice to select the movies they want to download the trailers for. You can make it completely autonomous but I felt that some degree of user control would be great. The user provides the indexes like this: 1,3,4
which I then split by using the split
method.
The user-provided indexes are not the same indexes for the movies in similar_movies
list so I convert the user-supplied index into an integer and subtract one from it. Then I extract the selected movies and put them in the final_movie_list
.
The rest of the code is pretty straightforward. I made excessive use of list comprehensions as well. For instance:
trailer_clips = [VideoFileClip(url) for url in trailer_urls]
List comprehensions should be second nature for you by now. Just in case, these are nothing more than a compact way to write for
loops and store the result in a list. The above code can also be written like this:
trailer_clips = []
for url in trailer_urls:
trailer_clips.append(VideoFileClip(url))
Note
Get into the habit of using list comprehensions. They are Pythonic and make your code more readable in most while reducing the code size at the same time.
Warning
Don’t use too deeply nested list comprehensions, because that would just make your code ugly. Any piece of code is usually read more times than it is written. Sacrificing some screen space for more readability is a useful tradeoff in the long-term.
Save this code in the app.py
file and run it like this:
$ python app.py "The Prestige"
Replace “The Prestige” with any other movie name (The "
is important). The output should be similar to this:
[Pre-show Generator] Movie: The Prestige
[Pre-show Generator] Which movies seem interesting? Type the indexes like this: 3,4,6
1 . Sicario: Day of the Soldado
2 . Terminal
3 . Hereditary
4 . Beirut
5 . Loving Pablo
6 . 12 Strong
7 . Skyscraper
[Pre-show Generator] Ans:
At this point you need to pass in the index of movies which you are interested in:
[Pre-show Generator] Ans: 1,4
The rest of the output should be similar to Fig. 7.12.
7.7 Troubleshoot¶
You may have a couple of scenarios which will require some creativity to solve. You could end up in a situation where the download_trailers
library doesn’t work anymore. In that case either go ahead and figure out a way to scrape the .mov links from the trailers website or search for a new libray on GitHub which does work. You can also look for new sources for sourcing the trailers.
Another possibility would be to end up in a situation that googlesearch stops working. Chances are that either you ran the script too much that Google thinks you are a bot and has started returning captchas or that Google has tweaked their website slightly which requires some update to googlesearch
. In case its the former scenario, you can use some other search engine and figure out how to do targeted searching. For the latter, search GitHub for a google search related library which has recently been updated.
You can also encounter some bugs in moviepy. For instance, when I was editing this chapter I ran my script again and got this error:
AttributeError: 'NoneType' object has no attribute 'stdout'
This was resolved by downgrading my moviepy version. I found the solution by searching on Google and reading this issue on GitHub.
7.8 Next Steps¶
If you have the same output your script is working perfectly. Now you can extend this script in multiple ways.
Currently, I am downloading the trailers in 720p quality. Try downloading them in the 1080p quality. You might have to modify the input to the resize()
method.
You can also use vlc.py
and pyautogui
such that your Python file will automatically run the combined trailer using vlc
and once the trailer is finished it will press Alt + Tab
(win/linux) or Command + Tab
(Mac) using pyautogui
to switch to the second “movie” window (I am thinking about Netflix running in a browser window) and start playing the movie automatically.
One other improvement can be to make use of click and automate the whole process by passing in all the arguments at run time. This way you will not have to wait for the app to return the movie names for you to choose from. It will automatically choose the indexes which you specify during run-time. This will also introduce the element of surprise because you will have no idea which trailers will be downloaded!
I am thinking of something like this:
$ python app.py "The Prestige" --indexes 3,4,7
This will search for similar movies to prestige and automatically download the movies which have the index of 3, 4, and 7.
Go ahead and try out these modifications. If you get stuck just shoot me an email and I would be more than happy to help.
See you in the next chapter!