Skip to content

Instantly share code, notes, and snippets.

@DavidBouw
Created July 11, 2021 01:08
Show Gist options
  • Save DavidBouw/8c23453e2cda0ead714335cab68d6e14 to your computer and use it in GitHub Desktop.
Save DavidBouw/8c23453e2cda0ead714335cab68d6e14 to your computer and use it in GitHub Desktop.
Automatically create an IMDB Top 250 or Letterboxd Top 250 collection in Plex using an existing movie library
tt6751668 tt0091251 tt0068646 tt0056058 tt0071562 tt0047478 tt0050083 tt0271383 tt0055233 tt0245429 tt0057565 tt0101985 tt0111161 tt0244316 tt8613070 tt4633694 tt0053114 tt0468569 tt0099685 tt0140888 tt0108052 tt0080684 tt0044741 tt0060196 tt0079944 tt0111341 tt0469494 tt0019254 tt0060827 tt0317248 tt0167260 tt0043014 tt0060107 tt0058625 tt0169858 tt0046438 tt0078788 tt0089881 tt0118694 tt0110912 tt4016934 tt0050825 tt0053115 tt0095327 tt0113247 tt0097216 tt2582802 tt0062622 tt0100234 tt0047396 tt0083922 tt0052572 tt0087884 tt0054407 tt0119698 tt0054215 tt0102926 tt6725014 tt0056322 tt0095765 tt0072417 tt0077711 tt0053604 tt0089603 tt0047445 tt0064116 tt0156887 tt0048473 tt0081505 tt0120737 tt0112471 tt0048452 tt0078748 tt0156794 tt0084787 tt0364569 tt2396224 tt0054042 tt0381681 tt0038650 tt0072443 tt0040725 tt0021749 tt0073486 tt0167261 tt0072684 tt0249241 tt0353969 tt0050634 tt0075404 tt0050976 tt0050783 tt0041154 tt0477348 tt0056801 tt0022100 tt0361748 tt0058946 tt0045152 tt0015324 tt0063794 tt1128075 tt0058604 tt0053198 tt0040522 tt0114369 tt0049902 tt0137523 tt1255953 tt0064040 tt0042192 tt0109424 tt0346336 tt0056172 tt0075314 tt0034583 tt0057012 tt3742378 tt1832382 tt3612616 tt0050986 tt0027977 tt0037674 tt0111495 tt0055630 tt0073198 tt0018192 tt0076759 tt0091670 tt0096288 tt10272386 tt0048424 tt0033467 tt0110357 tt0058888 tt0076085 tt0046268 tt0116282 tt0032553 tt0053779 tt0037558 tt0064068 tt1087578 tt0092048 tt0347149 tt0088763 tt0054248 tt0071315 tt0062873 tt0048956 tt0041959 tt0253474 tt0057277 tt0052357 tt0206634 tt0035446 tt0093191 tt0338013 tt0075793 tt0103064 tt0036112 tt0093342 tt4975722 tt0072890 tt0086879 tt0087843 tt0054130 tt8020896 tt0081398 tt0017136 tt5311514 tt0018455 tt2380307 tt0056444 tt8075192 tt0166924 tt0065234 tt0101640 tt0070510 tt0046478 tt0042804 tt0114709 tt0086022 tt0056512 tt0051036 tt0029192 tt0038733 tt0082971 tt0096283 tt0063278 tt0094625 tt0407887 tt0069293 tt2278388 tt0042876 tt0036775 tt0118799 tt0050613 tt0040897 tt0079672 tt0051201 tt0078754 tt10633456 tt0043313 tt1375666 tt3281548 tt0073363 tt0045274 tt2106476 tt0408664 tt0048021 tt1392190 tt0117214 tt0050371 tt0062229 tt1827487 tt4468740 tt0074958 tt0105888 tt0066921 tt0043338 tt0118749 tt0120815 tt0057358 tt0129167 tt0063522 tt0119217 tt0051093 tt5052448 tt0120265 tt0055852 tt0144782 tt0061847 tt0028950 tt1954470 tt0457430 tt0042593 tt0114787 tt3153634 tt0097223 tt0061184 tt0059527 tt0061065 tt0082912 tt0053291 tt0113277 tt0243889 tt6390668 tt0066993 tt8413338
Parasite Come and See The Godfather Harakiri The Godfather: Part II Seven Samurai 12 Angry Men A Dog's Will The Human Condition III: A Soldier's Prayer Spirited Away High and Low A Brighter Summer Day The Shawshank Redemption Yi Yi Portrait of a Lady on Fire Spider-Man: Into the Spider-Verse The Human Condition I: No Greater Love The Dark Knight GoodFellas Central Station Schindler's List The Empire Strikes Back Ikiru The Good, the Bad and the Ugly Stalker Satantango There Will Be Blood The Passion of Joan of Arc Persona City of God The Lord of the Rings: The Return of the King Sunset Boulevard Andrei Rublev Woman in the Dunes Neon Genesis Evangelion: The End of Evangelion Tokyo Story Apocalypse Now Ran In the Mood for Love Pulp Fiction The Handmaiden Paths of Glory The Human Condition II: Road to Eternity Grave of the Fireflies La Haine Do the Right Thing Whiplash 2001: A Space Odyssey Close-Up Rear Window Fanny and Alexander The World of Apu Paris, Texas Le Trou Princess Mononoke Psycho The Silence of the Lambs Scenes from a Marriage The Given Word Cinema Paradiso A Woman Under the Influence Autumn Sonata The Apartment Mishima: A Life in Four Chapters Sansho the Bailiff Once Upon a Time in the West Perfect Blue Pather Panchali The Shining The Lord of the Rings: The Fellowship of the Ring Before Sunrise Ordet Alien Eternity and a Day The Thing Oldboy It's Such a Beautiful Day Macario Before Sunset It's a Wonderful Life Mirror The Red Shoes City Lights One Flew Over the Cuckoo's Nest The Lord of the Rings: The Two Towers Barry Lyndon Werckmeister Harmonies Memories of Murder The Cranes Are Flying The Ascent The Seventh Seal Nights of Cabiria Late Spring No Country for Old Men M Inglourious Basterds The Battle of Algiers Singin' in the Rain Sherlock, Jr. War and Peace Love Exposure I Am Cuba The 400 Blows Bicycle Thieves Se7en A Man Escaped Fight Club Incendies Army of Shadows All About Eve Chungking Express The Best of Youth Lawrence of Arabia Taxi Driver Casablanca Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb The Second Mother A Separation Mommy Wild Strawberries Modern Times Children of Paradise Three Colors: Red Yojimbo Jeanne Dielman, 23, Quai du Commerce 1080 Bruxelles Napoleon Star Wars The Sacrifice Landscape in the Mist The Father The Night of the Hunter Citizen Kane The Lion King Red Beard A Special Day The Wages of Fear Fargo The Great Dictator La Dolce Vita Brief Encounter Funeral Parade of Roses Still Walking Tampopo Howl's Moving Castle Back to the Future Rocco and His Brothers Chinatown The Young Girls of Rochefort Aparajito The Third Man The Pianist The Big City Vertigo Children of Men To Be or Not to Be Wings of Desire Eternal Sunshine of the Spotless Mind We All Loved Each Other So Much Terminator 2: Judgment Day The Life and Death of Colonel Blimp Where Is My Friend's House? Moonlight Dog Day Afternoon Amadeus Once Upon a Time in America La Notte An Elephant Sitting Still Raging Bull Metropolis Your Name. Sunrise: A Song of Two Humans Coco An Autumn Afternoon Shoplifters Mulholland Drive Z Raise the Red Lantern Paper Moon Ugetsu The Young and the Damned Toy Story Nostalgia Il Sorpasso Sweet Smell of Success Make Way for Tomorrow A Matter of Life and Death Raiders of the Lost Ark My Neighbor Totoro Marketa Lazarová Akira The Departed Solaris The Grand Budapest Hotel Rashomon Double Indemnity Life Is Beautiful Throne of Blood The Treasure of the Sierra Madre Opening Night Witness for the Prosecution All That Jazz Minari Early Summer Inception Little Women Manila in the Claws of Light Umberto D. The Hunt Nobody Knows Rififi Mad Max: Fury Road A Moment of Innocence A Face in the Crowd Le Samouraï Once Upon a Time in Anatolia Paddington 2 Network Life, and Nothing More... A Clockwork Orange Ace in the Hole Boogie Nights Saving Private Ryan Winter Light The Iron Giant Rosemary's Baby Good Will Hunting Tokyo Twilight Get Out Taste of Cherry Cléo from 5 to 7 The Red Light Bandit Samurai Rebellion Grand Illusion Gangs of Wasseypur - Part 2 Pan's Labyrinth In a Lonely Place Underground Hope Time of the Gypsies Who's Afraid of Virginia Woolf? The Shop on Main Street The Face of Another Pixote Some Like It Hot Heat Eureka Invisible Life The Devils Kumbalangi Nights
2019 1985 1972 1962 1974 1954 1957 2000 1961 2001 1963 1991 1994 2000 2019 2018 1959 2008 1990 1998 1993 1980 1952 1966 1979 1994 2007 1928 1966 2002 2003 1950 1966 1964 1997 1953 1979 1985 2000 1994 2016 1957 1959 1988 1995 1989 2014 1968 1990 1954 1982 1959 1984 1960 1997 1960 1991 1974 1962 1988 1974 1978 1960 1985 1954 1968 1997 1955 1980 2001 1995 1955 1979 1998 1982 2003 2012 1960 2004 1946 1975 1948 1931 1975 2002 1975 2000 2003 1957 1977 1957 1957 1949 2007 1963 1931 2009 1966 1952 1924 1966 2008 1964 1959 1948 1995 1956 1999 2010 1969 1950 1994 2003 1962 1976 1942 1964 2015 2011 2014 1957 1936 1945 1994 1961 1975 1927 1977 1986 1988 2020 1955 1941 1994 1965 1977 1953 1996 1940 1960 1945 1969 2008 1985 2004 1985 1960 1974 1967 1956 1949 2002 1963 1958 2006 1942 1987 2004 1974 1991 1943 1987 2016 1975 1984 1984 1961 2018 1980 1927 2016 1927 2017 1962 2018 2001 1969 1991 1973 1953 1950 1995 1983 1962 1957 1937 1946 1981 1988 1967 1988 2006 1972 2014 1950 1944 1997 1957 1948 1977 1957 1979 2020 1951 2010 2019 1975 1952 2012 2004 1955 2015 1996 1957 1967 2011 2017 1976 1992 1971 1951 1997 1998 1963 1999 1968 1997 1957 2017 1997 1962 1968 1967 1937 2012 2006 1950 1995 2013 1988 1966 1965 1966 1981 1959 1995 2000 2019 1971 2019
#------------------------------------------------------------------------------
#
# Automated IMDB Top 250 Plex collection script inspired by /u/SwiftPanda16
# Set Secure Connections to Preferred or this will not work
# *** Use at your own risk! ***
# *** I am not responsible for damages to your Plex server or libraries. ***
#
#------------------------------------------------------------------------------
import json
import requests
import time
import shutil
import sys
import csv
import os
from lxml import html
from plexapi.server import PlexServer
### Plex server details ###
# Make sure that under Network settings you have Secure connections set to Preferred or Disabled
PLEX_URL = 'http://localhost:32400'
PLEX_TOKEN = 'xxxxxxxxxx'
### Existing movie library details ###
MOVIE_LIBRARY_NAME = 'Movies'
### New IMDB Top 250 library details ###
IMDB_CHART_URL = 'http://www.imdb.com/chart/top'
LETTERBOXD_CHART_URL = 'https://letterboxd.com/dave/list/official-top-250-narrative-feature-films/'
LETTERBOXD_URL = 'https://letterboxd.com'
IMDB_COLLECTION_NAME = 'IMDB Top 250'
LETTERBOXD_COLLECTION_NAME = 'Letterboxd Official Top 250 Narrative Feature Films'
### The Movie Database details ###
# Enter your TMDb API key if your movie library is using "The Movie Database" agent.
# This will be used to convert the TMDb IDs to IMDB IDs.
# You can leave this blank '' if your movie library is using the "Plex Movie" agent.
TMDB_API_KEY = ''
##### CODE BELOW #####
TMDB_REQUEST_COUNT = 0 # DO NOT CHANGE
def add_collection(library_key, rating_key, collection_name):
headers = {"X-Plex-Token": PLEX_TOKEN}
params = {"type": 1,
"id": rating_key,
"collection[0].tag.tag": collection_name,
"collection.locked": 1
}
url = "{base_url}/library/sections/{library}/all".format(base_url=PLEX_URL, library=library_key)
r = requests.put(url, headers=headers, params=params)
def remove_collection(library_key, rating_key, collection_name):
headers = {"X-Plex-Token": PLEX_TOKEN}
params = {"type": 1,
"id": rating_key,
"collection[].tag.tag-": collection_name
}
url = "{base_url}/library/sections/{library}/all".format(base_url=PLEX_URL, library=library_key)
r = requests.put(url, headers=headers, params=params)
def get_imdb_id_from_tmdb(tmdb_id):
global TMDB_REQUEST_COUNT
if not TMDB_API_KEY:
return None
# Wait 10 seconds for the TMDb rate limit
if TMDB_REQUEST_COUNT >= 40:
time.sleep(10)
TMDB_REQUEST_COUNT = 0
params = {"api_key": TMDB_API_KEY}
url = "https://api.themoviedb.org/3/movie/{tmdb_id}".format(tmdb_id=tmdb_id)
r = requests.get(url, params=params)
TMDB_REQUEST_COUNT += 1
if r.status_code == 200:
movie = json.loads(r.text)
return movie['imdb_id']
else:
return None
def get_imdb_id_from_letterboxd(url):
### Retrieve IMDB data from Letterboxd movie page ###
r = requests.get(url)
tree = html.fromstring(r.content)
movie_title = tree.xpath("//div[contains(@class, 'react-component film-poster')]/@data-film-name")[0]
movie_year = tree.xpath("//div[contains(@class, 'react-component film-poster')]/@data-film-release-year")[0]
try:
imdb_url = tree.xpath("//p[@class='text-link text-footer']//a[@data-track-action='IMDb']/@href")
imdb_id = imdb_url[0].rsplit('/', 2)[-2]
except:
# tmdb_url = tree.xpath("//p[@class='text-link text-footer']//a[@data-track-action='TMDb']/@href")
# tmdb_id = tmdb_url[0].rsplit('/', 2)[-2]
# Gangs of Wasseypur is one combined film on IMDB but is two separate on Letterboxd
if movie_title == "Gangs of Wasseypur - Part 2":
imdb_id = "tt1954470"
else:
imdb_id = 0
return movie_title,movie_year,imdb_id
def letterboxd_top_250(library_language):
### Get the Letterboxd Top 250 List ###
print("Retrieving the Letterboxd Official Top 250 Narrative Feature Films list...")
os.chdir(os.path.dirname(os.path.abspath(__file__)))
top_250_titles,top_250_years,top_250_ids = [],[],[]
if os.path.isfile('top_250.csv'):
refresh = input("Stored Letterboxd Top 250 exists. Do you want to force a refresh? (y/n): ")
if refresh == 'n':
csv_read_data = {}
with open('top_250.csv', 'r') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
for i, row in enumerate(csv_reader):
csv_read_data[i] = row
top_250_ids = csv_read_data[0]
top_250_titles = csv_read_data[1]
top_250_years = csv_read_data[2]
if refresh == 'y' or not os.path.isfile('top_250.csv'):
print("Please be patient. This will take longer than creating the IMDB Top 250 list.")
print()
page_number = 1
indicator = True
while indicator:
r = requests.get(LETTERBOXD_CHART_URL + 'page/{}'.format(page_number), headers={'Accept-Language': library_language})
tree = html.fromstring(r.content)
# http://stackoverflow.com/questions/35101944/empty-list-is-returned-from-imdb-using-python-lxml
# https://www.w3schools.com/html/default.asp
# https://www.w3schools.com/xml/xpath_syntax.asp
# https://stackoverflow.com/questions/4531995/getting-attribute-using-xpath
top_250_letterboxd_url = tree.xpath("//ul[contains(@class, 'poster-list -p125 -grid film-list')]//div[@class='poster film-poster really-lazy-load']/@data-target-link")
if top_250_letterboxd_url:
for url in top_250_letterboxd_url:
# print(url)
title,year,imdb_id = get_imdb_id_from_letterboxd(LETTERBOXD_URL + url)
top_250_titles.append(title)
top_250_years.append(year)
top_250_ids.append(imdb_id)
page_number += 1
else:
indicator = False
with open('top_250.csv', 'w') as csv_file:
csv_writer = csv.writer(csv_file, delimiter=',')
csv_writer.writerow(top_250_ids)
csv_writer.writerow(top_250_titles)
csv_writer.writerow(top_250_years)
return top_250_ids, top_250_titles, top_250_years
def imdb_top_250(library_language):
### Get the IMDB Top 250 List ###
print("Retrieving the IMDB Top 250 list...")
r = requests.get(IMDB_CHART_URL, headers={'Accept-Language': library_language})
tree = html.fromstring(r.content)
# http://stackoverflow.com/questions/35101944/empty-list-is-returned-from-imdb-using-python-lxml
top_250_titles = tree.xpath("//table[contains(@class, 'chart')]//td[@class='titleColumn']/a/text()")
top_250_years = tree.xpath("//table[contains(@class, 'chart')]//td[@class='titleColumn']/span/text()")
top_250_ids = tree.xpath("//table[contains(@class, 'chart')]//td[@class='ratingColumn']/div//@data-titleid")
return top_250_ids, top_250_titles, top_250_years
def plex_movie_list():
### Create PlexServer object and list of movies ###
try:
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
except Exception as e:
# print(e)
print("No Plex server found at: {base_url}".format(base_url=PLEX_URL))
print("Exiting script.")
sys.exit()
print("Retrieving a list of movies from the '{library}' library in Plex...".format(library=MOVIE_LIBRARY_NAME))
try:
movie_library = plex.library.section(MOVIE_LIBRARY_NAME)
movie_library_key = movie_library.key
library_language = movie_library.language
all_movies = movie_library.all()
return movie_library, movie_library_key, library_language, all_movies
except:
print("The '{library}' library does not exist in Plex.".format(library=MOVIE_LIBRARY_NAME))
print("Exiting script.")
sys.exit()
def imdb_id_mapping(all_movies):
### Create a dictionary consisting of {imdb_id: movie} pairs for the Plex library. ###
plex_library_imdb_ids = []
imdb_map = {}
for m in all_movies:
if 'imdb://' in m.guid:
imdb_id = m.guid.split('imdb://')[1].split('?')[0]
elif 'themoviedb://' in m.guid:
tmdb_id = m.guid.split('themoviedb://')[1].split('?')[0]
imdb_id = get_imdb_id_from_tmdb(tmdb_id)
else:
imdb_id = None
if imdb_id:
plex_library_imdb_ids.append(imdb_id)
imdb_map[imdb_id] = m
return plex_library_imdb_ids, imdb_map
def set_collection(top_250_ids, imdb_map, movie_library_key, collection_name):
### Create the plex collection ###
print("Setting the collection for the '{}' library...".format(MOVIE_LIBRARY_NAME))
in_library_idx = []
for i, imdb_id in enumerate(top_250_ids):
movie = imdb_map.pop(imdb_id, None)
if movie:
add_collection(movie_library_key, movie.ratingKey, collection_name)
in_library_idx.append(i)
for movie in imdb_map.values():
remove_collection(movie_library_key, movie.ratingKey, collection_name)
return in_library_idx
def get_found_missing_list(top_250_ids, top_250_titles, top_250_years, in_library_idx):
# Get list of missing IMDB Top 250 movies
missing_top_250 = [(idx, imdb) for idx, imdb in enumerate(zip(top_250_ids, top_250_titles, top_250_years))
if idx not in in_library_idx]
# Get list of found IMDB Top 250 movies
found_top_250 = [(idx, imdb) for idx, imdb in enumerate(zip(top_250_ids, top_250_titles, top_250_years))
if idx in in_library_idx]
return missing_top_250, found_top_250, len(top_250_ids)
def print_results(missing_top_250, found_top_250, list_count):
columns, _ = shutil.get_terminal_size(fallback=(80, 24))
print("="*columns)
print("\nNumber of Top 250 movies in the library: {count}".format(count=list_count-len(missing_top_250)))
print("Number of missing Top 250 movies: {count}".format(count=len(missing_top_250)))
print("\nList of found Top 250 movies:\n")
for idx, (imdb_id, title, year) in found_top_250:
print("{idx}\t{imdb_id}\t{title} {year}".format(idx=idx+1, imdb_id=imdb_id, title=title, year=year))
print("\nList of missing Top 250 movies:\n")
for idx, (imdb_id, title, year) in missing_top_250:
print("{idx}\t{imdb_id}\t{title} {year}".format(idx=idx+1, imdb_id=imdb_id, title=title, year=year))
# def metacritic_score(imdb_id, session):
# r = session.get("https://www.imdb.com/title/{}/criticreviews".format(imdb_id))
# tree = html.fromstring(r.content)
# metacritic_score = tree.xpath("//div[@class = 'metascore_block']//span[@itemprop = 'ratingValue']/text()")
# if metacritic_score:
# metacritic_score = int(metacritic_score[0])
# else:
# metacritic_score = "None"
# return metacritic_score
# def run_metacritic_score():
# cutoff_threshold = int(input("What score ceiling do you want? (Hint: 101 will include all scores) "))
# plex_library_metacritic_scores = {}
# movie_library, movie_library_key, library_language, all_movies = plex_movie_list()
# plex_library_imdb_ids, imdb_map = imdb_id_mapping(all_movies)
# session = requests.Session()
# for imdb_id in plex_library_imdb_ids:
# plex_library_metacritic_scores[imdb_id] = metacritic_score(imdb_id, session)
# if plex_library_metacritic_scores[imdb_id] != "None" and plex_library_metacritic_scores[imdb_id] <= cutoff_threshold:
# print(imdb_map[imdb_id].title, plex_library_metacritic_scores[imdb_id])
def run_imdb_top_250():
movie_library, movie_library_key, library_language, all_movies = plex_movie_list()
top_250_ids, top_250_titles, top_250_years = imdb_top_250(library_language)
plex_library_imdb_ids, imdb_map = imdb_id_mapping(all_movies)
in_library_idx = set_collection(top_250_ids, imdb_map, movie_library_key, IMDB_COLLECTION_NAME)
missing_top_250, found_top_250, list_count = get_found_missing_list(top_250_ids, top_250_titles, top_250_years, in_library_idx)
print_results(missing_top_250, found_top_250, list_count)
def run_letterboxd_top_250():
movie_library, movie_library_key, library_language, all_movies = plex_movie_list()
top_250_ids, top_250_titles, top_250_years = letterboxd_top_250(library_language)
plex_library_imdb_ids, imdb_map = imdb_id_mapping(all_movies)
in_library_idx = set_collection(top_250_ids, imdb_map, movie_library_key, LETTERBOXD_COLLECTION_NAME)
missing_top_250, found_top_250, list_count = get_found_missing_list(top_250_ids, top_250_titles, top_250_years, in_library_idx)
print_results(missing_top_250, found_top_250, list_count)
def run_crossover_top_250():
movie_library, movie_library_key, library_language, all_movies = plex_movie_list()
top_250_ids, top_250_titles, top_250_years = imdb_top_250(library_language)
top_250_ids_two, top_250_titles_two, top_250_years_two = letterboxd_top_250(library_language)
top_250_ids_crossover = set(top_250_ids) & set(top_250_ids_two)
plex_library_imdb_ids, imdb_map = imdb_id_mapping(all_movies)
in_library_idx = set_collection(top_250_ids_crossover, imdb_map, movie_library_key, "IMDB & Letterboxd Top 250 Crossover")
if __name__ == "__main__":
columns, rows = shutil.get_terminal_size(fallback=(80, 24))
print("="*columns)
print("Automated Plex collection script".center(columns))
print("="*columns)
menu = {}
menu['1'] = ". IMDB Top 250"
menu['2'] = ". Letterboxd Top 250"
menu['3'] = ". IMDB Top 250 & Letterboxd Top 250"
menu['4'] = ". Quit"
options = menu.keys()
for entry in options:
print("{}{}".format(entry,menu[entry]))
selection = input("Please select an option: ")
if selection == '1':
print()
run_imdb_top_250()
elif selection == '2':
print()
run_letterboxd_top_250()
elif selection == '3':
print()
run_crossover_top_250()
else:
print()
print("Unknown Option Selected!")
print("="*columns)
print("Done".center(columns))
print("="*columns)
input("Press Enter to finish...")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment