diff --git a/backend/app/server.py b/backend/app/server.py index f2677932bfaaf14f201ac9159d0b0f5be9797289..1fa789f4c6cedbc56a8d985a938a50f41537cf03 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -6,11 +6,13 @@ from app.routes.auth import auth from app.routes.user import user from flask_login import LoginManager -from flask import Flask, jsonify +from flask import Flask, jsonify, request from dotenv import load_dotenv from datetime import timedelta from flask_cors import CORS import os +import requests + load_dotenv() @@ -66,6 +68,70 @@ def healthCheck(): } }) +@app.route('/where') +def wheretowatch(): + movie_name = request.args.get("movie_name") + country_code = os.getenv('WHERE_TO_WATCH_COUNTRY') + api_key = os.getenv('WHERE_TO_WATCH') + + if not api_key: + return jsonify({"error": "API key not found in environment variables."}), 500 + + # Search for the movie to get the movie ID + search_url = f'https://api.themoviedb.org/3/search/movie?api_key={api_key}&query={movie_name}' + response = requests.get(search_url) + + if response.status_code == 200: + search_data = response.json() + + if 'results' in search_data and len(search_data['results']) > 0: + # Assume we want the first movie result + movie_id = search_data['results'][0].get('id') + print(f"Found movie ID: {movie_id} for {movie_name}") + + # Get the watch providers for the movie + watch_url = f'https://api.themoviedb.org/3/movie/{movie_id}/watch/providers?api_key={api_key}' + watch_response = requests.get(watch_url) + + if watch_response.status_code == 200: + watch_data = watch_response.json() + + # Check if the results for the specific country exist + if 'results' in watch_data and country_code in watch_data['results']: + country_watch_providers = watch_data['results'][country_code] + + filtered_types = { + "flatrate": [], + "buy": [], + "free": [] + } + + allowed_providers = os.getenv('ALLOWED_PROVIDERS').split(',') + + # Collect only relevant providers + for provider_type in filtered_types.keys(): + if provider_type in country_watch_providers: + # Filter the providers based on the allowed providers + filtered_types[provider_type] = [ + provider for provider in country_watch_providers[provider_type] + if provider['provider_name'].lower() in allowed_providers + ] + + # Check if any filtered providers exist + if any(filtered_types[key] for key in filtered_types): + return jsonify(filtered_types) + else: + return jsonify({"message": f"No matching providers found for '{movie_name}' in {country_code}."}), 404 + else: + return jsonify({"message": f"No streaming sources found for '{movie_name}' in {country_code}."}), 404 + else: + return jsonify({"error": f"Error fetching watch providers for {movie_name}. Status code: {watch_response.status_code}"}), 500 + else: + return jsonify({"message": f"No results found for '{movie_name}'"}), 404 + else: + return jsonify({"error": f"Error fetching movie data. Status code: {response.status_code}"}), 500 + + app.register_blueprint(auth, url_prefix='/auth') app.register_blueprint(user, url_prefix='/user') diff --git a/frontend/src/components/movie.component.pug b/frontend/src/components/movie.component.pug index e3e2128454979fc92e714502473154f157ef4e98..b83088202470629ecaf193047c6e945d05a5bec3 100644 --- a/frontend/src/components/movie.component.pug +++ b/frontend/src/components/movie.component.pug @@ -10,9 +10,40 @@ [ngStyle]="genre.colour" ) {{genre.label}} + button.btn.w-100.where-to-watch.my-3((click)='whereToWatch(); expanded = !expanded;') Where to Watch + span.ml-2 + i.fa-solid.fa-chevron-down(*ngIf='expanded else nope') + ng-template(#nope) + i.fa-solid.fa-chevron-up + + ng-container(*ngIf='empty') + i.fa-solid.fa-times + + ng-container(*ngIf='providers && expanded') + ng-container(*ngIf='providers.buy.length') + span + i.fa-solid.fa-cart-shopping + ng-container(*ngFor='let buy of providers.buy') + span + img.provider.m-2([src]='"https://image.tmdb.org/t/p/original/" + buy.logo_path') + div + ng-container(*ngIf='providers.flatrate.length') + span + i.fa-solid.fa-credit-card + ng-container(*ngFor='let flatrate of providers.flatrate') + span + img.provider.m-2([src]='"https://image.tmdb.org/t/p/original/" + flatrate.logo_path') + div + ng-container(*ngIf='providers.free.length') + span + i.fa-solid.fa-gift + ng-container(*ngFor='let free of providers.free') + span + img.provider.m-2([src]='"https://image.tmdb.org/t/p/original/" + free.logo_path') + div - .text-sm.opacity-75.mt-3 - span.mr-1 + .text-sm.opacity-75.mt-3 + span.mr-1 i.fa.fa-regular.fa-star span {{movie.rating}} diff --git a/frontend/src/components/movie.component.scss b/frontend/src/components/movie.component.scss index a649269724b9c2c087988e5838fa332507c10871..c041a1efa54d07619a1b266a18c5fdbe7691d775 100644 --- a/frontend/src/components/movie.component.scss +++ b/frontend/src/components/movie.component.scss @@ -8,3 +8,15 @@ border-radius: var(--rounded-2xl); color: white; } + +.where-to-watch { + color: white; + background-color: navy; + border-radius: var(--rounded-2xl); +} + +.provider { + border: 2px solid #cccccc; + border-radius: var(--rounded-xl); + width: 3rem; +} \ No newline at end of file diff --git a/frontend/src/components/movie.component.ts b/frontend/src/components/movie.component.ts index beaf318d0632e48b6a6efb64007eceeffb3e0341..cff23e87ea1afcec705ce5175d9ecc74b0118d42 100644 --- a/frontend/src/components/movie.component.ts +++ b/frontend/src/components/movie.component.ts @@ -2,6 +2,15 @@ import {Component, Input, OnInit} from '@angular/core'; import {Movie} from './movie.model'; import {CommonModule} from '@angular/common'; import {HoursPipe} from '../app/util/hours.pipe'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {ChangeDetectorRef, inject} from '@angular/core'; +import {lastValueFrom} from 'rxjs'; +import {environment} from '../environments/environment'; + +const authHeaders = { + headers: new HttpHeaders({'Content-Type': 'application/json'}), + withCredentials: true, +} export const genreMapping: {[key: string]: string} = { Action: '#df2819', // red @@ -24,6 +33,19 @@ export const genreMapping: {[key: string]: string} = { Western: '#996633', // brown }; +interface RawWhereToWatchItem { + display_priority: number, + logo_path: string, + provider_id: string, + provider_name: string +} + +interface RawWhereToWatchReturn { + buy: RawWhereToWatchItem[], + flatrate: RawWhereToWatchItem[], + free: RawWhereToWatchItem[], +} + @Component({ imports: [CommonModule, HoursPipe], selector: 'apse-movie', @@ -32,14 +54,24 @@ export const genreMapping: {[key: string]: string} = { templateUrl: './movie.component.pug', }) export class MovieComponent implements OnInit { + readonly #http = inject(HttpClient); + + readonly #cdr = inject(ChangeDetectorRef); + + public providers?: RawWhereToWatchReturn; + @Input('movie') public movie!: Movie; public genres?: { - colour: Record<string, any>; + colour: Record<string, any>; label: string }[] = []; + public empty = false; + + public expanded = false; + #genreMapping = genreMapping; public ngOnInit(): void { @@ -50,4 +82,13 @@ export class MovieComponent implements OnInit { }) }) } + + public async whereToWatch(): Promise<void> { + try { + this.providers = await lastValueFrom(this.#http.get<RawWhereToWatchReturn>(`${environment.baseApiUrl}/where?movie_name=${this.movie.name}`, authHeaders)); + } catch { + this.empty = true; + } + this.#cdr.markForCheck(); + } }