Sugestão aleatória de filmes e séries para assistir por streaming
Publicado por Pedro Fernandes (última atualização em 30/04/2023)
[ Hits: 2.206 ]
Homepage: https://github.com/PedroF37
Projeto Python para sugerir aleatoriamente filmes/séries para assistir em streaming, usando a API (não oficial) do Justwatch.
Aplicativo sugere filme/série para assistir, de todos os serviços de streaming, todos os gêneros e todas as classificações to IMDb, ou, dos streamings, gêneros e classificações especificadas.
Aplicativo sugere baseado nesses parâmetros aleatoriamente e tenta garantir que não sugere títulos repetidos, salvando os títulos sugeridos em arquivo e verificando se o título escolhido se encontra ou não no arquivo.
Se quiser adicionar mais serviços de streaming, adiciona dentro do script, no dicionário "PROVIDERS_DICT". É algo como: Disney-Plus: dnp (dnp é o nome que aparece na API do justwatch e na própria URL do justwatch).
Se na documentação da API não mostrar o streaming em questão, basta entrar no site do justwatch e selecionar o streaming, que irá aparecer as siglas na URL.
Original:
https://github.com/PedroF37/Streaming
# -------------------------------------------------------------------------- #
# IMPORTAÇÕES
# tkinter
from tkinter import Tk, Frame, Label, Button
from tkinter import PhotoImage, Listbox, Variable
from tkinter.messagebox import showerror, showinfo
from tkinter.ttk import Style, Combobox
# JustWatch
from justwatch import JustWatch
# random
from random import choice
# requests
from requests import get
from requests.exceptions import HTTPError, ConnectionError
# os
from os import remove
# pillow
from PIL import Image, ImageTk
# webbrowser
from webbrowser import open as op
# atexit
from atexit import register
# sys
from sys import exit
# -------------------------------------------------------------------------- #
# CONSTANTES
# Cores
COLOR1 = '#292929' # Fundo
COLOR2 = '#c7c5c5' # Janela
COLOR3 = '#fffbef' # Letra
# Caminho do poster para ser deletado no final
POSTER_PATH = './poster.jpg'
# Arquivo com as sugestões dadas
SUGESTIONS_FILE = './sugetions.txt'
# URLs
BASE_URL = 'https://www.justwatch.com'
BASE_IMAGE_URL = 'https://images.justwatch.com'
# Dicionário mapeia os streamings para os códigos
# deles na url do JustWatch
PROVIDERS_DICT = {
'Disney-Plus': 'dnp', 'GloboPlay': 'gop',
'HBO-MAX': 'hbm', 'Netflix': 'nfx',
'Prime-Video': 'prv', 'Star-Plus': 'srp'
}
# Mapeia série e filme (português) para o que vai na url
CONTENT_TYPES_DICT = {'Série': 'show', 'Filme': 'movie'}
# Mapeia os géneros
GENRES_DICT = {
'Ação & Aventura': 'act', 'Comédia': 'cmy', 'Documentário': 'doc',
'Fantasia': 'fnt', 'Terror': 'hrr', 'Música & Musical': 'msc',
'Romance': 'rma', 'Esportes & Fitness': 'spt', 'Western': 'wsn',
'Animação': 'ani', 'Crime': 'crm',
'Drama': 'drm', 'História': 'hst', 'Família': 'fml',
'Mistério & Thriller': 'trl', 'Ficção Científica': 'scf',
'Guerra & Militar': 'war', 'Reality TV': 'rly'
}
# Streamings, filme/série e géneros
# para os Combobox e Listbox
stream_list = [key for key in PROVIDERS_DICT]
content_type_list = [key for key in CONTENT_TYPES_DICT]
genre_list = [key for key in GENRES_DICT]
[
item.insert(0, 'TODOS')
for item in
(stream_list, content_type_list, genre_list)
]
# Lista de Rating do imdb
rating_list = [
0.0, 1.0, 2.0, 3.0, 4.0, 5.0,
6.0, 7.0, 8.0, 9.0, 10.0
]
rating_list.insert(0, 'QUALQUER')
global poster_img, button_img
global thumbs_up_img, cool_img, imdb_img
# -------------------------------------------------------------------------- #
# FUNÇÕES
def remove_poster():
"""Cuida de remover o poster baixado."""
try:
remove(POSTER_PATH)
except OSError:
pass
def delete_sugestions():
"""Cuida de remover arquivo com sugestões passadas."""
try:
remove(SUGESTIONS_FILE)
showinfo('Sucesso', 'Sugestões deletadas com sucesso')
except OSError:
showinfo('', 'Não possui sugestões gravadas ainda')
def mount_query(streaming_listbox, genres_select, items):
"""Cuida de montar a requisição."""
# Aqui, destroi para não acontecer de os labels ficarem colados
# ao apertar botão novamente.
[widget.destroy() for widget in output_frame.winfo_children()]
# Acho que não é necessario, o poster é sobsecrito aqui
# mas acho que não faz mal também kkk!
remove_poster()
# Cria listas de streammings escolhidos e géneros escolhidos.
streamings_list = [
streaming_listbox.get(i)
for i in streaming_listbox.curselection()
]
genres_list = [
genres_select.get(i)
for i in genres_listbox.curselection()
]
if 'TODOS' in streamings_list or not streamings_list:
# Lista
streaming = list(PROVIDERS_DICT.values())
else:
# Itens enviados como lista
streaming = [PROVIDERS_DICT[i] for i in streamings_list]
if 'TODOS' in genres_list or not genres_list:
# Lista
genres = list(GENRES_DICT.values())
else:
# Itens enviados como lista
genres = [GENRES_DICT[i] for i in genres_list]
if items[0] == 'TODOS' or items[0] == '':
# Lista
types = list(CONTENT_TYPES_DICT.values())
else:
# Item enviado como lista
types = [CONTENT_TYPES_DICT[items[0]]]
# Aqui, pega de qualquer rating, ou do
# rating especificado até ao rating de 10.0
if items[1] == 'QUALQUER' or items[1] == '':
rating = {
"imdb:score": {
"min_scoring_value": 0.0,
"max_scoring_value": 10.0
}
}
else:
rating = {
"imdb:score": {
"min_scoring_value": float(items[1]),
"max_scoring_value": 10.0
}
}
query(streaming, types, genres, rating)
def query(streaming, types, genres, rating):
"""Cuida de fazer a consulta"""
try:
just_watch = JustWatch(country='BR')
results = just_watch.search_for_item(
providers=streaming,
content_types=types,
genres=genres,
scoring_filter_types=rating,
page_size=100
)
except ConnectionError:
showerror('', 'Verifique sua conexão e tente mais tarde')
exit()
except HTTPError:
showerror('', 'Erro inesperado na requisição. Tente mais tarde')
exit()
else:
parse_results(results)
def parse_results(results):
"""Cuida de pegar os dados."""
'''
Aqui, dependendo da combinação (género, filme/serie etc), pode
não achar algum item. Exemplo, se colocar: GloboPlay, Documentário,
Série e classificação a partir de 6.0, dá erro de KeyError, porque
(neste caso) não tem poster. Mas quem garante que o globoplay tem
documentários? kk.. Então, para não usar um try/except para cada item,
fiz assim kk.
'''
try:
# Pega os títulos
titles = [item['title'] for item in results['items']]
# Pega os links para o filme ou seŕie
links = [
f"{BASE_URL}{item['full_path']}"
for item in results['items']
]
# Pega o link para o poster do tamanho especificado.
# Aqui tamanho é s332
posters = [
f"{BASE_IMAGE_URL}{item['poster'].replace('{profile}', 's332')}"
for item in results['items']
]
'''
Aqui, quero pegar o rating do imdb. Mas o json é muito complicado kk.
Tem dicionário dentro de lista com lista dentro de dicionário.
Para piorar, o dicionário onde está o rating do imdb é
{'provider_type': 'imdb:score', 'value': 8.5}, e este dicionário está
em posições diferentes para cada lista kk. E não posso pegar só pelo
provider_type, pois tem outros dicionários com provider_type cujos
valores não são imdb:score.
Então, score_list, pega a lista de listas com os dicionários.
Depois imdb_list, pega da lista de listas o rating (na chave value),
se o valor da chave provider_type for imdb:score
'''
scorelist = [
results['items'][score]['scoring']
for score in range(len(results['items']))
]
imdb_scores = list()
for item in range(len(scorelist)):
for provider in scorelist[item]:
if provider['provider_type'] == 'imdb:score':
imdb_scores.append(provider['value'])
except KeyError:
showinfo(
'',
'Não foram emcontrados itens com a combinação' \
' de parâmetros especificados'
)
return
else:
# Dicionario que mapeia os titulos ao link.
# Ex: 'Tulsa King': 'https://....serie/tulsa-king'
title_link_dict = {
key: value
for (key, value)
in zip(titles, links)
}
# Dicionario que mapeia titulos para o link do poster
title_poster_dict = {
key: value
for (key, value)
in zip(titles, posters)
}
# Dicionário que mapeia títulos ao rating do imdb
title_score_dict = {
key: value
for (key, value)
in zip(titles, imdb_scores)
}
pick_sugestion(
title_link_dict,
title_poster_dict,
title_score_dict
)
def was_sugested(title):
"""
Cuida de varrer o arquivo
para vêr se título já foi sugerido.
"""
'''
Retornos
1 - Arquivo existe e título já foi seugerido.
2 - Arquivo existe e título ainda não foi sugerido.
3 - Arquivo não existe ainda. Será criado e título será escrito.
'''
try:
with open(SUGESTIONS_FILE, 'r', encoding='utf-8') as f:
found = False
for line in f:
if title in line:
found = True
if found:
return 1
else:
return 2
except IOError:
# Aqui, arquivo estará vazio ainda.
with open(SUGESTIONS_FILE, 'w', encoding='utf-8') as f:
f.write(f'{title}\n')
return 3
def pick_sugestion(
title_link_dict,
title_poster_dict,
title_score_dict
):
"""Escolhe o título para sugerir"""
title, link = choice(list(title_link_dict.items()))
exists = was_sugested(title)
count = 0
while exists == 1:
count += 1
if count == 5:
showinfo(
'',
'Estamos com problemas em sugerir algo novo.' \
' Experimente deletar sugeridos, ou alterar os parâmetros' \
' para a sugestão. Iremos terminar o programa.'
)
exit()
title, link = choice(list(title_link_dict.items()))
exists = was_sugested(title)
if exists == 2:
# Arquivo existe, mas título não foi sugerido ainda.
# Será escrito aqui no arquivo.
with open(SUGESTIONS_FILE, 'a', encoding='utf-8') as f:
f.write(f'{title}\n')
poster = get(title_poster_dict[title])
'''
Aqui, conto com que se não deu erro na função query, não será
aqui meros segundos depois que a conexão falhará. Por isso não
uso aqui try/except. Mas concerteza que pode acontecer né? kkk!!
'''
with open(POSTER_PATH, 'wb') as f:
[f.write(chunk) for chunk in poster]
show_output(title, link, title_poster_dict, title_score_dict)
def show_output(title, link, title_link_dict, title_score_dict):
"""Mostra o resultado."""
global poster_img, button_img
global thumbs_up_img, cool_img, imdb_img
thumbs_up_img = Image.open('icones/thumbs-up.png')
thumbs_up_img = thumbs_up_img.resize((25, 25))
thumbs_up_img = ImageTk.PhotoImage(thumbs_up_img)
label = Label(
output_frame, text='Vamos Assistir:....',
font=('Roboto 10 bold'), anchor='center',
bg=COLOR1, fg=COLOR3
)
label.place(x=150, y=0)
title_label = Label(
output_frame, text=f' {title}?',
font=('Roboto 10 bold'), anchor='nw', image=thumbs_up_img,
bg=COLOR1, fg=COLOR3, compound='left'
)
title_label.place(x=80, y=25)
imdb_img = Image.open('icones/imdb.png')
imdb_img = imdb_img.resize((15, 15))
imdb_img = ImageTk.PhotoImage(imdb_img)
imdb_rating_label = Label(
output_frame, image=imdb_img, compound='left',
text=f' Classificação: {title_score_dict[title]:.1f}',
font=('Roboto 8 bold'), anchor='nw',
bg=COLOR1, fg=COLOR3
)
imdb_rating_label.place(x=150, y=55)
poster_img = Image.open(POSTER_PATH)
poster_img = ImageTk.PhotoImage(poster_img)
poster_img_label = Label(
output_frame, text='',
image=poster_img
)
poster_img_label.place(x=50, y=80)
cool_img = Image.open('icones/cool.png')
cool_img = cool_img.resize((25, 25))
cool_img = ImageTk.PhotoImage(cool_img)
link_label = Label(
output_frame, text=' Veja mais informações! ',
font=('Roboto 10 bold'), fg=COLOR3,
bg=COLOR1, image=cool_img, compound='left'
)
link_label.place(x=120, y=560)
button_img = Image.open('icones/button.png')
button_img = button_img.resize((45, 45))
button_img = ImageTk.PhotoImage(button_img)
link_label_button = Label(
output_frame, text='',
image=button_img,
cursor='hand2',
bg=COLOR1
)
link_label_button.place(x=180, y=610)
link_label_button.bind(
'<Button-1>',
lambda open_link: callback(link)
)
def callback(url):
"""Cuida de abrir o link"""
op(url)
# -------------------------------------------------------------------------- #
# JANELA
window = Tk()
window.title('')
window.geometry('1000x720')
window.resizable(0, 0)
window.configure(bg=COLOR2)
style = Style(window)
style.theme_use('clam')
# -------------------------------------------------------------------------- #
# FRAMES
title_frame = Frame(
window, width=1000,
height=48, bg=COLOR1
)
title_frame.grid(
row=0, column=0,
sticky='nsew'
)
input_frame = Frame(
window, width=448,
height=670, bg=COLOR1
)
input_frame.place(x=0, y=50)
output_frame = Frame(
window, width=552,
height=670, bg=COLOR1
)
output_frame.place(x=450, y=50)
# -------------------------------------------------------------------------- #
# CONFIGURANDO TITLE_FRAME
glasses_img = PhotoImage(file='icones/glasses.png')
sad_img = PhotoImage(file='icones/sad.png')
title_label = Label(
title_frame, text=' Assistir o quê?? SOCORRO!!!',
image=glasses_img, compound='left',
font=('Roboto 20 bold'),
anchor='nw', bg=COLOR1,
fg=COLOR3
)
title_label.place(x=10, y=10)
sad_label = Label(
title_frame, text='',
image=sad_img, compound='right',
bg=COLOR1
)
sad_label.place(x=420, y=10)
# -------------------------------------------------------------------------- #
# CONFIGURANDO INPUT_FRAME
# Streamings
streamings_label = Label(
input_frame, text='Streaming',
font=('Roboto 12 bold'), anchor='nw',
bg=COLOR1, fg=COLOR3
)
streamings_label.place(x=30, y=20)
stream = Variable(value=stream_list)
streaming_listbox = Listbox(
input_frame,
font=('Roboto 10'),
listvariable=stream,
selectmode='multiple',
exportselection=0
)
streaming_listbox.place(x=30, y=60)
# Tipos (filme/serie)
types_label = Label(
input_frame, text='Filme/Série',
font=('Roboto 12 bold'), anchor='nw',
bg=COLOR1, fg=COLOR3
)
types_label.place(x=30, y=270)
types_combobox = Combobox(
input_frame, width=17,
font=('Roboto 10'),
values=content_type_list,
state='readonly'
)
types_combobox.place(x=30, y=310)
# Género (ação/terror etc)
genres_label = Label(
input_frame, text='Género',
font=('Roboto 12 bold'), anchor='nw',
bg=COLOR1, fg=COLOR3
)
genres_label.place(x=200, y=20)
genre = Variable(value=genre_list)
genres_listbox = Listbox(
input_frame,
font=('Roboto 10'),
listvariable=genre,
selectmode='multiple',
exportselection=0
)
genres_listbox.place(x=200, y=60)
# Rating Imdb
rating_label = Label(
input_frame, text='Rating (A partir de)',
font=('Roboto 12 bold'), anchor='nw',
bg=COLOR1, fg=COLOR3
)
rating_label.place(x=200, y=270)
rating_combobox = Combobox(
input_frame, width=17,
font=('Roboto 10'),
values=rating_list,
state='readonly'
)
rating_combobox.place(x=200, y=310)
# Botão para pegar o item
find_img = PhotoImage(file='icones/find.png')
find_button = Button(
input_frame, text='SUGERIR', font=('Roboto 8 bold'),
relief='ridge', overrelief='sunken',
bg=COLOR1, fg=COLOR3, activebackground=COLOR1,
activeforeground=COLOR3, image=find_img, cursor='hand2',
compound='left', anchor='nw', command=lambda: mount_query(
streaming_listbox, genres_listbox,
[types_combobox.get(), rating_combobox.get()]
)
)
find_button.place(x=145, y=400)
# Botão para deletar arquivo
# com sugestões passadas
trash_img = Image.open('icones/trash.png')
trash_img = trash_img.resize((15, 15))
trash_img = ImageTk.PhotoImage(trash_img)
delete_sugested_button = Button(
input_frame, text=' DELETAR SUGERIDOS', font=('Roboto 8 bold'),
relief='ridge', overrelief='sunken',
bg=COLOR1, fg=COLOR3, activebackground=COLOR1,
activeforeground=COLOR3, image=trash_img,
compound='left', anchor='nw', command=delete_sugestions
)
delete_sugested_button.place(x=110, y=450)
# -------------------------------------------------------------------------- #
# LOOP
# Remove o poster baixado quando sair do app
register(remove_poster)
window.mainloop()
# -------------------------------------------------------------------------- #
Calculadora de area de poligonos e circulos
Adicione a opção Redimensionar e rotacionar imagens ao Nautilus
Correios - Rastreador de encomendas
Modo Simples de Baixar e Usar o bash-completion
Monitorando o Preço do Bitcoin ou sua Cripto Favorita em Tempo Real com um Widget Flutuante
Instalando partes faltantes do Plasma 6
Adicionar botão "mostrar área de trabalho" no Zorin OS
Como montar um servidor de backup no linux
Estou tentando ser legalista, mas tá complicado! (9)
espelhar monitores nao funciona (2)
SQLITE não quer funcionar no LINUX LMDE6 64 com Lazaruz 4.2 64bit (n... (1)









