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: 1.742 ]

Homepage: https://github.com/PedroF37

Download Streaming.7z




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

  



Esconder código-fonte

# -------------------------------------------------------------------------- #
# 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()


# -------------------------------------------------------------------------- #

Scripts recomendados

Calculadora para números complexos

Random Google Images - v1.0b

Just Do It - XML Generic Editor

Calculadora do IMC em Tkinter

Gerador de números para Mega-Sena


  

Comentários
[1] Comentário enviado por maurixnovatrento em 14/05/2023 - 20:01h


Interessante.
___________________________________________________________
Conhecimento não se Leva para o Túmulo.
https://github.com/mxnt10

[2] Comentário enviado por sabe nada em 17/05/2023 - 17:08h


[1] Comentário enviado por mauricio123 em 14/05/2023 - 20:01h


Interessante.
___________________________________________________________
Conhecimento não se Leva para o Túmulo.
https://github.com/mxnt10



Obrigado. Sim, eu assino milhentos serviços de streaming, fico sempre na indecisão sobre o que assistir kk


Contribuir com comentário




Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts