4
\$\begingroup\$

All my code is below. It uses an API to search for a show and then outputs the providers (like breaking bad is available on Netflix).

The main aim of this application was to practice using APIs and object oriented programming as I have been struggling and not very confident in my skill of using Object Oriented Principles. So any advice on what I did right or wrong would be helpful.

'''
An app to find the providers of a certain movie or TV show by searching its name.
'''

import requests

def get_API_key():
    # Gets the API key
    return "<<an API key>>"

def searchshow(key, search_term, page=1):
    # Takes a user's search term and returns the appropriate page of results.
    response = requests.get("https://api.themoviedb.org/3/search/tv?api_key="+key+"&query="+search_term+"&page="+str(page))
    return response.json()

def searchresults(j, total_results):
    # Looks at whether the results are multiple or if there's no results. Quits the program if there are no results or displays the results with a new function.
    if total_results==0:
        print("No search results, try again.")
        quit()
    elif total_results>1:
        display_results(j, total_results)

def display_results(j, total_results):
    # Displays all the results
    for i in range(total_results):
        print(str(i) + ". " + j["results"][i]["name"])
        print(j["results"][i]["overview"])
        print()

def choose_show(j, total_results):
    # Let's the user pick a show if there are multiple choices else it chooses the only show possible if there's only one choice.
    if total_results==1:
        num = 0
    else:
        num = int(input("Please choose a show: "))
    show_name = j["results"][num]["name"]
    show_id = str(j["results"][num]["id"])
    show_overview = j["results"][num]["overview"]
    show_details = [show_name, show_id, show_overview]
    print(show_name)
    print(show_id)
    print(show_overview)
    return show_details

def get_all_providers(show_id, api_key):
    response = requests.get("https://api.themoviedb.org/3/tv/"+show_id+"/watch/providers?api_key="+api_key)
    return response.json()

def types_of_providers(j, country):
    # Provides a list of the types of providers
    providers = []
    for i in j["results"][country]:
        providers.append(i)
    providers.pop(0)
    return providers

def display_providers(j, country, type_of_provider):
    # Displays the providers of each type of provider
    print("For " + type_of_provider + ":")
    for i in range(len(j["results"][country][type_of_provider])):
        print(j["results"][country][type_of_provider][i]["provider_name"])

def main():
    api_key = get_API_key()
    search_term = input("Enter the TV show or movie you'd like to see the providers for: ")
    j = searchshow(api_key, search_term)
    total_results = j["total_results"]
    searchresults(j, total_results)
    show_details = choose_show(j, total_results)
    j = get_all_providers(show_details[1], api_key)
    country = input("Enter your country code: ")
    providers = types_of_providers(j, country)
    print()
    print("Provided on")
    for i in providers:
        display_providers(j, country, i)

if __name__ == "__main__":
    main()
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

PEP-8

Your code is mostly compliant with the Style Guide for Python Code. A few deviations should be corrected.

  1. Function names should be in snake_case. So searchshow should be search_show and searchresults should be search_results.
  2. The variable name j is too short. It should be named with something descriptive, like json or json_results.
  3. Operators should have a space before and afterwards. if total_results==0: should be if total_results == 0:, and elif total_results>1: should be elif total_results > 1:

Bug?

What if total_results == 1? It seems odd (suspicious) that display_results()` is not called for 1 result!

Quit

NEVER call quit(). This terminates the Python interpreter. If you attempt to write unit tests in Python, but call a function that quits the interpreter, your unit testing will suddenly stop working.

Avoid looping over ranges

If you write for index in range(len(container)): and then use container[index] in the loop body, you are being inefficient and not "Pythonic". Loop over the container elements directly:

def display_providers(j, country, type_of_provider):
    print("For " + type_of_provider + ":")
    for provider in j["results"][country][type_of_provider]:
        print(provider["provider_name"])

Reducing complexity

search_results() calls display_results() when there is more than one show, and later main() calls choose_show() which might (if there is more than one show) request a choice from the previously displayed menu. This should be one not be split this way. choose_show() should display the menu if it is asking for the user to choose a show.

def choose_show(shows):
    if shows['total_results'] == 1:
        num = 0
    else:
        for index, show in enumerate(shows['results']):
            print(f"{index}. {show['name'}")
            print(show['overview'])
            print()

        num = int(input("Please choose a show: "))

    show = shows['results'][num]

    show_name = show['name']
    show_id = str(show['id'])
    show_overview = show['overview']

    return [show_name, show_id, show_overview]

The above shows usage of the enumerate() function. It also demonstrates using a local variable to avoid indexing into json['results'][num] repeatedly.

With the above function (and a few new ones not shown here), your main() might change like:

def main():
    api_key = get_API_key()
    search_term = input("Enter the TV show or movie you'd like to see the providers for: ")
    shows = search_show(api_key, search_term)
    if shows['total_results'] > 0:
        show_details = choose_show(shows)
        display_show_details(show_details)
        all_providers = get_all_providers(show_details[1], api_key)
        country = input("Enter your country code: ")
        display_providers_for_country(all_providers, country)
    else:
        print("No results")

Note: My personal style has strings that are displayed to the user (and therefor might require translation to another language) in double quotes, and other strings (dictionary keys, URLs, SQL statements) in single quotes. Since you are asking for country codes, you might be supporting internationalization and/or localization in the future -- and being quickly able to identify the user facing strings can be useful. Python does not treat single and double quoted strings differently; internally, a string is just a string.


Note: I would not say this program is demonstrating any object-oriented principles. You are not creating any objects, using any data encapsulation, nor asking objects to behave in any polymorphic fashions. It is just a simply procedural program.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks a lot! The reason I had them in seperate function was that I learned about the Single Resposibility Principle recently and thought that it would be good practice to separate the displaying of the menu to the if statement but it seems that I was going about the principle in the wrong way. \$\endgroup\$
    – Twam
    Commented Mar 23, 2022 at 9:27
  • \$\begingroup\$ The Single Responsibility Principle is good; a function should do one thing and do it well. Displaying a menu is one thing - and can be moved into its own function. Asking a question is one thing - with two parts (displaying the question & getting the result); the two sub parts can be done in their own functions called by the "asking a question" function. Your main function does more than just "ask a question", so it shouldn't be the one calling the "question & answer" subparts of "asking a question"; it needs an intermediary (possibly more than one). Hope that helps. \$\endgroup\$
    – AJNeufeld
    Commented Mar 23, 2022 at 14:42
1
\$\begingroup\$

Overlapping at least partly with AJNeufeld's good advice:

Your aim of OOP is a little mystifying given that you've defined no objects. In my demonstration code I will show a couple.

Add PEP484 type hints.

Add a depagination routine so that you aren't only fetching the first page.

Always check whether your request succeeded, the easiest way being response.raise_for_status.

I would combine searchresults and display_results, don't quit, and don't pass around total_results: just pass around a sequence of results and apply len().

Use f-strings rather than concatenation in situations like

"https://api.themoviedb.org/3/tv/"+show_id+"/watch/providers"

but don't also append your query parameters. Pass those to the params kwarg of requests.

Don't hard-code your API key. One easy way to pass this in is an environment variable.

Use a session.

Suggested

"""
An app to find the providers of a certain movie or TV show by searching its name.
"""
from datetime import date
from itertools import count
from os import environ
from textwrap import wrap
from typing import Any, NamedTuple, Iterator, Sequence, Optional, Literal

from requests import Session


class SearchResult(NamedTuple):
    backdrop_path: Optional[str]
    genre_ids: list[int]
    id: int
    name: str
    origin_country: list[str]
    original_language: str
    original_name: str
    overview: str
    popularity: float
    poster_path: Optional[str]
    vote_average: float
    vote_count: int
    first_air_date: Optional[date] = None

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> 'SearchResult':
        aired = d.get('first_air_date')
        if aired:
            d['first_air_date'] = date.fromisoformat(aired)
        return cls(**d)

    def summary(self, index: int) -> str:
        return (
            f'{index}. {self.name}'
            '\n'
            + '\n'.join(wrap(self.overview, width=80))
            + '\n'
        )


Country = str
Disposition = Literal['flatrate', 'buy', 'ads']


class Provider(NamedTuple):
    disposition: Disposition
    display_priority: int
    logo_path: str
    provider_id: int
    provider_name: str


class CountryProviders(NamedTuple):
    country: Country
    link: str
    providers: dict[Disposition, list[Provider]]

    @classmethod
    def from_dict(cls, d: dict[str, Any], country: str) -> 'CountryProviders':
        link = d.pop('link')

        dispositions = {
            disposition: [
                Provider(disposition, **provider)
                for provider in providers
            ]
            for disposition, providers in d.items()
        }

        return cls(country=country, link=link, providers=dispositions)


def get_api_key() -> str:
    # Gets the API key (v3 auth)
    return environ['API_KEY']


def search_show_page(
    session: Session, key: str, search_term: str, page: int = 1,
) -> dict[str, Any]:
    with session.get(
        url='https://api.themoviedb.org/3/search/tv',
        params={
            'api_key': key,
            'query': search_term,
            'page': page,
        },
        headers={'Accept': 'application/json'},
    ) as resp:
        resp.raise_for_status()
        return resp.json()


def search_show(
    session: Session, key: str, search_term: str,
) -> Iterator[SearchResult]:
    for page in count(1):
        doc = search_show_page(session, key, search_term, page)
        for result in doc['results']:
            yield SearchResult.from_dict(result)
        if page >= doc['total_pages']:
            break


def display_results(results: Sequence[SearchResult]) -> None:
    if len(results) == 0:
        print('No search results, try again.')
    else:
        for i, result in enumerate(results, 1):
            print(result.summary(i))


def choose_show(results: Sequence[SearchResult]) -> SearchResult:
    # Lets the user pick a show if there are multiple choices
    # else it chooses the only show possible if there's only one choice.
    if len(results) == 1:
        return results[0]

    num = int(input('Please choose a show by its index: '))
    return results[num - 1]


def get_all_providers(session: Session, api_key: str, show_id: int) -> dict[str, Any]:
    with session.get(
        url=f'https://api.themoviedb.org/3/tv/{show_id}/watch/providers',
        params={'api_key': api_key},
        headers={'Accept': 'application/json'},
    ) as resp:
        resp.raise_for_status()
        return resp.json()


def types_of_providers(doc: dict[str, Any], country: Country) -> CountryProviders:
    providers = doc['results'].get(country)

    return providers and CountryProviders.from_dict(providers, country)


def display_providers(providers: Optional[CountryProviders]) -> None:
    if providers is None:
        print('There are no providers in your country')
    else:
        print('\nProvided on:')
        for disposition, disp_providers in providers.providers.items():
            print(
                f'For {disposition}:',
                ', '.join(p.provider_name for p in disp_providers)
            )


def main() -> None:
    api_key = get_api_key()
    search_term = input("Enter the TV show or movie you'd like to see the providers for: ")

    with Session() as session:
        results = tuple(search_show(session, api_key, search_term))
        display_results(results)
        if len(results) == 0:
            return

        show_details = choose_show(results)
        country = input('Enter your country code: ')
        providers = types_of_providers(
            get_all_providers(session, api_key, show_details.id),
            country,
        )
        display_providers(providers)


if __name__ == '__main__':
    main()

Output

Enter the TV show or movie you'd like to see the providers for: teletub
1. Teletubbies
Pre-school fun, fantasy and education with colourful rotund characters Tinky
Winky, Dipsy, Laa-Laa and Po in a magical land called Teletubbyland.

Enter your country code: US

Provided on:
For ads: The Roku Channel
For buy: Amazon Video
For flatrate: Hoopla
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.