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.171 ]
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()
# -------------------------------------------------------------------------- #
Organizador de Arquivos em Python + tkinter
Visualizar a data e hora de um servidor SNTP e atualizar na BIOS do sistema
Criador de instalador USB Linux bootável com Python
Unescape de caracteres especiais ISO-8859-1
Monitorando o Preço do Bitcoin ou sua Cripto Favorita em Tempo Real com um Widget Flutuante
IA Turbina o Desktop Linux enquanto distros renovam forças
Como extrair chaves TOTP 2FA a partir de QRCODE (Google Authenticator)
Como realizar um ataque de força bruta para desobrir senhas?
Como usar Gpaste no ambiente Cinnamon
Atualizando o Fedora 42 para 43
Pergunta: Meu teclado não está respondendo direito como e consertar? (2)
Secure boot, artigo interessante, nada técnico. (6)
SQLITE não quer funcionar no LINUX LMDE6 64 com Lazaruz 4.2 64bit (n... (0)









