diff --git a/.gitignore b/.gitignore index 4e173e7..a979ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -ignore/ \ No newline at end of file +/venv \ No newline at end of file diff --git a/README.md b/README.md index 1eef681..8bd2955 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,91 @@ # FilmLabs -Local hosted streaming site. +This repository does not store any media files it uses streaming apis to stream movies/tv shows. -This repository does not store any film files it uses streaming apis to stream movies/tv shows. +# Features -You can create accounts to store watch history, you can search for movies, tv shows and anime. +* Local hosted streaming site. +* Create/Login accounts. +* Account favoriting for Movies/TV Shows. +* Account Watch history for Movies/TV Shows. +* Account Settings. +* Home page showing popular, trending, new, etc Movies/TV Shows. +* Search for Movies/TV Shows. +* Watch Movies/TV Shows. -The database uses mysql. \ No newline at end of file +# Technical Features +* MYSQL Database. +* Python Flask Server. +* TMDB Api. + +# FilmLabs Installation Intructions + +## WINDOWS + +## LINUX + +### Downloading Files +Clone/download the repository by running `git clone -b film-labs https://github.com/Monnapse/FilmLabs.git`. + +Now go into directory by running `cd FilmLabs/`. + +### Create Virtual Environemnt (Optional) +If you want to use a virtual environment then you want to first run `python3 -m venv filmlabs-environment` +(if you dont have the venv package install by running `apt install python3-venv`) + +Activate the virtual environemnt by running `source filmlabs-environment/bin/activate`. + +### Install required packages +Now install required packages by running `pip install -r requirements.txt`. + +### Install mysql +Now install the mysql server by running `sudo apt install mysql-server`. Now you need to secure mysql server by +running `sudo mysql_secure_installation` it will ask some questions, for the first question say `y` +then password security level question just say `2` for highest security, +and then all the rest of the questions just say `y` to all of them. + +### Create mysql password +Now enter mysql by running `sudo mysql -u root` +add this line and enter a password in `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_new_password';` replace `'your_new_password'` with your password then press enter, once you have done that add this line `FLUSH PRIVILEGES;` + +### Create database +Now create the database by adding `source mysql/forward_engineer.sql` to the next line. Exit by entering `exit`. + +### Creating environment variables +Now create your environment variables by running `sudo nano /etc/environment` +next in that file add the following on different lines: + +1. `FILMLABS_DB="filmlabs"` (this is the name of the database) + +2. `FILMLABS_JWT_KEY="df6^Dju-3Ffsf__d9pFDfSh#7#$)448*#4v$Jf-#HQHjf9hdf-FH(#r#"lrjfx-dSGsDJ4td)"` (This variable is your secret key creating your tokens so you can change it to whatever you want but its best to make it long and randomized so no hacker can get it) + +3. `MYSQL_PASSWORD="mysql password"` (Change mysql passsword with your password you created for your mysql root) + +4. `MYSQL_USER="root"` (This is your mysql user that is being logged into just keep it the same) + +5. `TMDB_API_KEY="your tmdb api key"` (Put in your TMDB api key here, you can get an api key from this [link](https://developer.themoviedb.org/docs/getting-started)) + +Now save the file by doing `ctrl+o` then press `enter` now exit the file by doing `ctrl+x`. + +Now restart, so the changes to take effect by running `sudo reboot`. + +Once restarted go to your the FilmLabs folder (`cd FilmLabs/`) +(and if you set up a virtual environment you need to activate it by running `source filmlabs-environment/bin/activate`) +now run your server by running `python3 main.py`, it should print out alot of stuff but what you are looking for is a list of ip adresses like this +``` +* Running on all addresses (0.0.0.0) +* Running on http://127.0.0.1:2400 +* Running on http://10.0.0.52:2400 +``` +`http://127.0.0.1:2400` will only allow access on your local machine. The last one `http://10.0.0.52:2400` allows access to the site on your network. + +Now enjoy. + +## Update files +if you want to update the files to latest release you do, pull latest changes `git pull origin film-labs`. + +# Modifying Home Page Categories +Open `home_page.json` + +# Adding/Removing Streaming Apis +Open `services.json` \ No newline at end of file diff --git a/home_page.json b/home_page.json new file mode 100644 index 0000000..921967c --- /dev/null +++ b/home_page.json @@ -0,0 +1,100 @@ +{ + "pages": [ + { + "current_page": 1, + + "categories": [ + { + "name": "My watch history", + "media_type": null, + "list_type": "watch_history", + "time_window": null + }, + { + "name": "My favorites", + "media_type": null, + "list_type": "favorites", + "time_window": null + }, + { + "name": "Trending today", + "media_type": null, + "list_type": "trending", + "time_window": "day" + } + ] + }, + { + "current_page": 2, + + "categories": [ + { + "name": "Trending this week", + "media_type": null, + "list_type": "trending", + "time_window": "week" + }, + { + "name": "Popular movies", + "media_type": "movie", + "list_type": "popular", + "time_window": null + }, + { + "name": "Now playing movies", + "media_type": "movie", + "list_type": "now_playing", + "time_window": null + } + ] + }, + { + "current_page": 3, + + "categories": [ + { + "name": "Top rated tv series", + "media_type": "tv", + "list_type": "top_rated", + "time_window": null + }, + { + "name": "Top rated movies", + "media_type": "movie", + "list_type": "popular", + "time_window": null + }, + { + "name": "Popular tv series", + "media_type": "tv", + "list_type": "popular", + "time_window": null + } + ] + }, + { + "current_page": 4, + + "categories": [ + { + "name": "Airing today tv series", + "media_type": "tv", + "list_type": "airing_today", + "time_window": null + }, + { + "name": "On the air tv series", + "media_type": "tv", + "list_type": "on_the_air", + "time_window": null + }, + { + "name": "Upcoming movies", + "media_type": "movie", + "list_type": "upcoming", + "time_window": null + } + ] + } + ] +} \ No newline at end of file diff --git a/ignore/FilmLabs.pur b/ignore/FilmLabs.pur new file mode 100644 index 0000000..cd04708 Binary files /dev/null and b/ignore/FilmLabs.pur differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..0962069 --- /dev/null +++ b/main.py @@ -0,0 +1,101 @@ +""" + + Film Box Server + Made by Monnapse + + Created 11/6/2024 + Last Updated 11/6/2024 + + 0.1.0 + +""" + +from flask import Flask, request +#from flask_session import Session +from datetime import timedelta +from os import environ +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +from flask_jwt_extended import JWTManager +import json + +import server.web as web +from server.packages import db, films, tmdb, json_controller, service +from server.packages.tmdb import FilmType, TVListType, MovieListType, TimeWindow, TMDB, ListResult + +# Settings +token_max_days = 7 + +password_min_length = 8 +password_max_length = 20 + +username_min_length = 3 +username_max_length = 20 + +# w9, w154, w185, w342, w500, w780, original +poster_sizing = "w185" # Smaller = more optimized = faster loading + +# Define the flask app +app = Flask(__name__) +limiter = Limiter(app, key_func=get_remote_address) + +#app.config["SESSION_PERMANENT"] = False +#app.config["SESSION_TYPE"] = "filesystem" +#app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=session_days) +app.config['JWT_SECRET_KEY'] = environ.get("FILMLABS_JWT_KEY") +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=token_max_days) + +#Session(app) +jwt = JWTManager(app) + +api = tmdb.TMDB( + environ.get("TMDB_API_KEY"), + poster_sizing +) +service_controller = service.ServiceController( + json_controller.load_json("services.json") +) + +#print(service_controller.get_services()[0].get_tv_url("693134", 1, 1)) + +#print(film_controller.get_next_categories(2)) + +db_controller = db.FilmLabsDB( + password_max_length, + password_min_length, + username_min_length, + username_max_length +) +film_controller = films.FilmsController( + api, + db_controller, + json_controller.load_json("home_page.json") +) +web_controller = web.WebClass( + app, + limiter, + db_controller, + jwt, + film_controller, + service_controller, + token_max_days, + password_max_length, + password_min_length, + username_min_length, + username_max_length +) +if __name__ == '__main__': + # Connect to database + db_controller.connect( + "localhost", + environ.get("MYSQL_USER"), + environ.get("MYSQL_PASSWORD"), + environ.get("FILMLABS_DB") + ) + + # Now make the web directories + # and run the flask app + web_controller.run_directories() + + # Run flask app + app.run(debug=False, port="2400", host="0.0.0.0") \ No newline at end of file diff --git a/mysql/forward_engineer.sql b/mysql/forward_engineer.sql new file mode 100644 index 0000000..7759cc8 --- /dev/null +++ b/mysql/forward_engineer.sql @@ -0,0 +1,131 @@ +-- MySQL Workbench Forward Engineering + +SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; +SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; +SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; + +-- ----------------------------------------------------- +-- Schema filmlabs +-- ----------------------------------------------------- +DROP SCHEMA IF EXISTS `filmlabs` ; + +-- ----------------------------------------------------- +-- Schema filmlabs +-- ----------------------------------------------------- +CREATE SCHEMA IF NOT EXISTS `filmlabs` DEFAULT CHARACTER SET utf8 ; +USE `filmlabs` ; + +-- ----------------------------------------------------- +-- Table `filmlabs`.`account` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`account` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`account` ( + `user_id` INT NOT NULL AUTO_INCREMENT, + `username` VARCHAR(45) NOT NULL, + `password` VARCHAR(80) NOT NULL, + PRIMARY KEY (`user_id`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `filmlabs`.`film` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`film` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`film` ( + `tmdb_id` INT NOT NULL, + `media_type` VARCHAR(8) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `release_date` VARCHAR(15) NOT NULL, + `rating` FLOAT NOT NULL, + `poster` VARCHAR(120) NOT NULL, + PRIMARY KEY (`tmdb_id`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `filmlabs`.`account_watch_history` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`account_watch_history` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`account_watch_history` ( + `account_history_id` INT NOT NULL AUTO_INCREMENT, + `tmdb_id` INT NOT NULL, + `user_id` INT NOT NULL, + INDEX `fk_account_watch_history_account1_idx` (`user_id` ASC) VISIBLE, + PRIMARY KEY (`account_history_id`), + INDEX `fk_account_watch_history_film1_idx` (`tmdb_id` ASC) VISIBLE, + CONSTRAINT `fk_account_watch_history_account1` + FOREIGN KEY (`user_id`) + REFERENCES `filmlabs`.`account` (`user_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_account_watch_history_film1` + FOREIGN KEY (`tmdb_id`) + REFERENCES `filmlabs`.`film` (`tmdb_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `filmlabs`.`account_favorites` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`account_favorites` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`account_favorites` ( + `tmdb_id` INT NOT NULL, + `user_id` INT NOT NULL, + INDEX `fk_account_favorites_account_idx` (`user_id` ASC) VISIBLE, + CONSTRAINT `fk_account_favorites_account` + FOREIGN KEY (`user_id`) + REFERENCES `filmlabs`.`account` (`user_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_account_favorites_film1` + FOREIGN KEY (`tmdb_id`) + REFERENCES `filmlabs`.`film` (`tmdb_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `filmlabs`.`movie_history` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`movie_history` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`movie_history` ( + `account_history_id` INT NOT NULL, + `progress` VARCHAR(10) NULL, + INDEX `fk_movie_history_account_watch_history1_idx` (`account_history_id` ASC) VISIBLE, + CONSTRAINT `fk_movie_history_account_watch_history1` + FOREIGN KEY (`account_history_id`) + REFERENCES `filmlabs`.`account_watch_history` (`account_history_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `filmlabs`.`episode_history` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `filmlabs`.`episode_history` ; + +CREATE TABLE IF NOT EXISTS `filmlabs`.`episode_history` ( + `account_history_id` INT NOT NULL, + `episode_number` INT NOT NULL, + `season_number` INT NOT NULL, + `progress` VARCHAR(10) NULL, + CONSTRAINT `fk_episode_history_account_watch_history1` + FOREIGN KEY (`account_history_id`) + REFERENCES `filmlabs`.`account_watch_history` (`account_history_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +SET SQL_MODE=@OLD_SQL_MODE; +SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; +SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; diff --git a/mysql/model/film_labs_model.mwb b/mysql/model/film_labs_model.mwb new file mode 100644 index 0000000..00e929a Binary files /dev/null and b/mysql/model/film_labs_model.mwb differ diff --git a/mysql/model/film_labs_model.mwb.bak b/mysql/model/film_labs_model.mwb.bak new file mode 100644 index 0000000..66dab42 Binary files /dev/null and b/mysql/model/film_labs_model.mwb.bak differ diff --git a/mysql/model/film_labs_model_diagram.mwb b/mysql/model/film_labs_model_diagram.mwb new file mode 100644 index 0000000..8c2a657 Binary files /dev/null and b/mysql/model/film_labs_model_diagram.mwb differ diff --git a/mysql/queries.sql b/mysql/queries.sql new file mode 100644 index 0000000..1d80922 --- /dev/null +++ b/mysql/queries.sql @@ -0,0 +1,100 @@ +use filmlabs; + +########## +# QUERIES +########## + +# Select all accounts +select * from account; + +# Account Create +insert into account (user_id, username, password) +values (1, "Monnapse", "password"); + +# Account Login +select user_id, username, password +from account +where username = "Monnapse"; + +# Username Checker +select user_id from account +where username = "Monnapse"; + +# Select film ids +select * from film where tmdb_id = 1; +select * from film ; + +# Add film +insert into film values (1, "tv", "test", "2024", 10, "https.com"); +insert into film values (2, "movie", "test", "2024", 10, "https.com"); +insert into film values (6, "movie", "test", "2024", 10, "https.com"); + +# Get Favorites +select distinct f.* +from account_favorites af +join film f on af.tmdb_id = f.tmdb_id +where af.user_id = 1 +group by f.tmdb_id; + +select distinct * from account_favorites af where af.user_id = 1; +select distinct * from account_favorites af where af.user_id = 1 and af.tmdb_id = 1; + +# Add Favorite +insert into account_favorites values(1, 1); +insert into account_favorites values(2, 1); + +# Remove Favorite +delete from account_favorites where tmdb_id = 2 and user_id = 1; + +# Add to watch history +insert into account_watch_history +(account_history_id, tmdb_id, user_id) +values (1, 1, 1); + +insert into account_watch_history +(account_history_id, tmdb_id, user_id) +values (2, 2, 1); + +insert into account_watch_history +(account_history_id, tmdb_id, user_id) +values (3, 6, 1); + +# TV +insert into episode_history +(account_history_id, episode_number, season_number, progress) +values (1, 1, 1, "00:00:00"); +insert into episode_history +(account_history_id, episode_number, season_number, progress) +values (1, 2, 1, "00:00:00"); + +# MOVIE +insert into movie_history +(account_history_id, progress) +values (2, "00:00:00"); +insert into movie_history +(account_history_id, progress) +values (3, "00:00:00"); + +# Get Watch History +select f.tmdb_id, +coalesce(group_concat(tv.progress), group_concat(movie.progress)) as progress, +coalesce(group_concat(tv.episode_number)) as episode_number, +coalesce(group_concat(tv.season_number)) as season_number +from account_watch_history awh +join film f on awh.tmdb_id = f.tmdb_id +left join episode_history tv on f.media_type = "tv" and awh.account_history_id = tv.account_history_id +left join movie_history movie on f.media_type = "movie" and awh.account_history_id = movie.account_history_id +where awh.user_id = 1 +group by f.tmdb_id; + +# Get Account Watch History Table +select * from account_watch_history where user_id = 1; +select * from account_watch_history where user_id = 1 and tmdb_id = 94605; + +# Get History Movie Table +select * from movie_history where account_history_id = 1; + +# Get TV Episode Table +select * from episode_history where account_history_id = 1; + +delete from episode_history; \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5039619 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==3.0.3 +Flask_JWT_Extended==4.6.0 +Flask_Limiter==2.8.1 +bcrypt==3.2.2 +Requests==2.32.3 +mysql-connector-python==9.1.0 \ No newline at end of file diff --git a/server/__pycache__/web.cpython-39.pyc b/server/__pycache__/web.cpython-39.pyc new file mode 100644 index 0000000..88b5656 Binary files /dev/null and b/server/__pycache__/web.cpython-39.pyc differ diff --git a/server/directories/__pycache__/account.cpython-39.pyc b/server/directories/__pycache__/account.cpython-39.pyc new file mode 100644 index 0000000..585d0a1 Binary files /dev/null and b/server/directories/__pycache__/account.cpython-39.pyc differ diff --git a/server/directories/__pycache__/authentication.cpython-39.pyc b/server/directories/__pycache__/authentication.cpython-39.pyc new file mode 100644 index 0000000..48c6104 Binary files /dev/null and b/server/directories/__pycache__/authentication.cpython-39.pyc differ diff --git a/server/directories/__pycache__/error_handling.cpython-39.pyc b/server/directories/__pycache__/error_handling.cpython-39.pyc new file mode 100644 index 0000000..acabcd4 Binary files /dev/null and b/server/directories/__pycache__/error_handling.cpython-39.pyc differ diff --git a/server/directories/__pycache__/films.cpython-39.pyc b/server/directories/__pycache__/films.cpython-39.pyc new file mode 100644 index 0000000..38f25d8 Binary files /dev/null and b/server/directories/__pycache__/films.cpython-39.pyc differ diff --git a/server/directories/__pycache__/home.cpython-39.pyc b/server/directories/__pycache__/home.cpython-39.pyc new file mode 100644 index 0000000..cbc0b84 Binary files /dev/null and b/server/directories/__pycache__/home.cpython-39.pyc differ diff --git a/server/directories/account.py b/server/directories/account.py new file mode 100644 index 0000000..0a3d6c2 --- /dev/null +++ b/server/directories/account.py @@ -0,0 +1,37 @@ +""" + Account page directories + Made by Monnapse + + 11/7/2024 +""" + +from server.web import WebClass + +from flask import render_template, redirect + +# SETTINGS + +def run(app: WebClass): + print("Account >>> Account directories loaded") + + @app.flask.route("/account") + @app.limiter.limit("30 per minute") + def account(): + try: + authorization = app.get_authorization_data() + + if (authorization["logged_in"]): + # If logged in then continue to account page + return render_template( + app.base, + template = "account.html", + title = "Account", + javascript = "account", + no_footer = True, + authorization=authorization + ) + else: + # If not logged in then redirect to login page + return redirect("/login") + except Exception as e: + print(f"Error in authentication controller at {account.__name__}: {e}") \ No newline at end of file diff --git a/server/directories/authentication.py b/server/directories/authentication.py new file mode 100644 index 0000000..78c019a --- /dev/null +++ b/server/directories/authentication.py @@ -0,0 +1,130 @@ +""" + Authentication + Made by Monnapse + + 11/6/2024 +""" + +from server.packages import authentication +from server.web import WebClass + +from flask import render_template, request, jsonify, redirect, make_response +from flask_jwt_extended import create_access_token +from datetime import timedelta + +def run(app: WebClass): + print("Authentication >>> Authentication directories loaded") + + # Directories + @app.flask.route("/login") + @app.limiter.limit("30 per minute") + def login(): + try: + return render_template( + app.base, + template = "login.html", + title = "Login", + javascript = "customForm", + no_header_additional = True, + no_footer = True, + + username_min = app.username_min_length, + username_max = app.username_max_length, + password_min = app.password_min_length, + password_max = app.password_max_length + ) + except Exception as e: + print(f"Error in authentication controller at {login.__name__}: {e}") + + @app.flask.route("/register") + @app.limiter.limit("30 per minute") + def register(): + try: + return render_template( + app.base, + template = "register.html", + title = "Register", + javascript = "customForm", + no_header_additional = True, + no_footer = True, + + username_min = app.username_min_length, + username_max = app.username_max_length, + password_min = app.password_min_length, + password_max = app.password_max_length + ) + except Exception as e: + print(f"Error in authentication controller at {register.__name__}: {e}") + + # Middle ground + @app.flask.route("/logout") + @app.limiter.limit("30 per minute") + def logout(): + try: + # Remove cookie so it doesnt go into infinite loops and self ddos + response = make_response(redirect("/")) + response.delete_cookie("access_token") + return response + except Exception as e: + print(f"Error in authentication controller at {logout.__name__}: {e}") + # API + + @app.flask.route("/login_submit", methods=['POST']) + @app.limiter.limit("10 per minute") # Stops any bruteforcers. + def login_submit(): + data = authentication.bytes_to_json(request.get_data()) + + try: + username = data["username"] + password = data["password"] + remember = data["remember"] + + # Login into account + account = app.db_controller.login(username, password) + + if (account.account_exists): + # Account logged in successfully + + remember_time = timedelta(hours=1) + + if (remember): + remember_time = timedelta(days=app.token_max_days) + + token = create_access_token(identity=account.user_id) + response = make_response(jsonify(success=True, message="Successfully logged into account"), 200) + response.set_cookie( + "access_token", + token, + httponly=True, + #secure=True, + samesite='Strict', + max_age=remember_time + ) # add expiration time + + return response + else: + return jsonify(success=False, message=account.account_message), 400 + except Exception as e: + print(f"Error in authentication controller at {login_submit.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + + @app.flask.route("/register_submit", methods=['POST']) + @app.limiter.limit("10 per minute") # Blocks any account creation bots from making an absurd amount. + def register_submit(): + data = authentication.bytes_to_json(request.get_data()) + + try: + username = data["username"] + password = data["password"] + reenter_password = data["reenter_password"] + + # Create account + new_account = app.db_controller.create_account(username, password, reenter_password) + + if (new_account.account_exists): + return jsonify(success=True, message="Successfully created account"), 200 + else: + return jsonify(success=False, message=new_account.account_message), 400 + except Exception as e: + print(f"Error in authentication controller at {register_submit.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 \ No newline at end of file diff --git a/server/directories/error_handling.py b/server/directories/error_handling.py new file mode 100644 index 0000000..62352da --- /dev/null +++ b/server/directories/error_handling.py @@ -0,0 +1,26 @@ +""" + Error handling + Made by Monnapse + + 11/7/2024 +""" + +from server.web import WebClass + +from flask import redirect, make_response + +# SETTINGS +def run(app: WebClass): + print("Error Handler >>> Error directories loaded") + + @app.jwt.expired_token_loader + @app.limiter.limit("30 per minute") + def expired_token_callback(jwt_header, jwt_payload): + try: + # Remove cookie so it doesnt go into infinite loops and self ddos + response = make_response(redirect("/")) + response.delete_cookie("access_token") + return response + except Exception as e: + print(f"Error in films error handling at {expired_token_callback.__name__}: {e}") + \ No newline at end of file diff --git a/server/directories/films.py b/server/directories/films.py new file mode 100644 index 0000000..f5ed97a --- /dev/null +++ b/server/directories/films.py @@ -0,0 +1,418 @@ +""" + Authentication + Made by Monnapse + + 11/6/2024 +""" + +from server.packages import authentication +from server.packages.tmdb import FilmType +from server.web import WebClass + +from flask import render_template, request, jsonify, redirect, make_response +import json +from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity +from datetime import timedelta, datetime + +def run(app: WebClass): + print("Films >>> Films directories loaded") + + # Function Routes + def category_base(media_type = None, list_type = None, time_window = None): + try: + authorization = app.get_authorization_data() + + return render_template( + app.base, + template = "selected.html", + javascript = "selectedFilms", + authorization = authorization, + title = app.film_controller.get_category_name(media_type, list_type, time_window) + ) + except Exception as e: + print(f"Error in films at {category_base.__name__}: {e}") + + # Routes + @app.flask.route("/category") + @app.limiter.limit("30 per minute") + def category(): + try: + # /// + media_type = request.args.get("media_type") + list_type = request.args.get("list_type") + time_window = request.args.get("time_window") + return category_base(media_type, list_type, time_window) + except Exception as e: + print(f"Error in films at {category.__name__}: {e}") + + @app.flask.route("/search") + @app.limiter.limit("30 per minute") + def category_time(): + try: + query = request.args.get("query") + + authorization = app.get_authorization_data() + + return render_template( + app.base, + template = "search.html", + javascript = "search", + authorization = authorization, + query=query + ) + except Exception as e: + print(f"Error in films at {category_time.__name__}: {e}") + + @app.flask.route("/film/tv///") + @app.limiter.limit("30 per minute") + def tv(id, season, episode): + try: + #current_season = season + authorization = app.get_authorization_data() + current_service = request.args.get("service") + + service = app.service_controller.get_service_data(current_service) + + film_details = app.film_controller.tmdb.get_details(FilmType.TV, id, True) + + user_id = None + if authorization != None and authorization["logged_in"]: + user_id = authorization["user_id"] + + film_details = app.film_controller.do_db_history_pass(user_id, film_details) + + if film_details != None: + season_details = film_details.get_season(season) + episodes = season_details.episodes + + #try: + # current_season = int(season) + # if film_details.seasons[current_season]: + # episodes = film_details.seasons[current_season].episodes + ##except: + ## current_season = int(season)-1 + ## if film_details.seasons[current_season]: + ## episodes = film_details.seasons[current_season].episodes + #except: + # pass + #print(film_details.seasons[1].episodes[6].progress) + #for season in film_details.seasons: + # for episode in season.episodes: + # print(f"Season: {season.season_number}, Episode: {episode.episode_number}, Progress: {episode.progress}") + + return render_template( + app.base, + template = "film.html", + javascript = "film", + authorization = authorization, + + service_url=service.get_tv_url(id, season, episode), + + # SERVICES + selected_service=service.name, + service_providers=app.service_controller.get_services(), + + # Film Details + title = film_details.name, + year = datetime.strptime(film_details.release_date, "%Y-%m-%d").year, + media_type = film_details.media_type, + overview = film_details.overview, + rating = round(film_details.vote_average, 1), + tmdb_url = f"https://www.themoviedb.org/tv/{id}", + + # TV + current_season = season_details.season_number, + seasons = film_details.seasons, + current_episode = int(episode), + episodes = episodes, + + id=id, + is_favorited = app.db_controller.is_favorited(id, authorization.get("user_id")), + current_service = current_service + ) + else: + return render_template( + app.base, + template = "film.html", + javascript = "film", + authorization = authorization, + title = "Could not find movie", + ) + except Exception as e: + print(f"Error in films at {tv.__name__}: {e}") + + @app.flask.route("/film/movie/") + @app.limiter.limit("30 per minute") + def movie(id): + try: + authorization = app.get_authorization_data() + current_service = request.args.get("service") + + service = app.service_controller.get_service_data(current_service) + + film_details = app.film_controller.tmdb.get_details(FilmType.Movie, id) + + if film_details != None: + return render_template( + app.base, + template = "film.html", + javascript = "film", + authorization = authorization, + + service_url=service.get_movie_url(id), + + # SERVICES + selected_service=service.name, + service_providers=app.service_controller.get_services(), + + # Film Details + title = film_details.name, + year = datetime.strptime(film_details.release_date, "%Y-%m-%d").year, + media_type = film_details.media_type, + overview = film_details.overview, + rating = round(film_details.vote_average, 1), + tmdb_url = f"https://www.themoviedb.org/movie/{id}", + + id=id, + is_favorited = app.db_controller.is_favorited(id, authorization.get("user_id")), + current_service=current_service + ) + else: + return render_template( + app.base, + template = "film.html", + javascript = "film", + authorization = authorization, + title = "Could not find movie", + ) + except Exception as e: + print(f"Error in films at {movie.__name__}: {e}") + + # API + def get_trailer(film_type, id): + try: + trailer = app.film_controller.tmdb.get_trailer(film_type, id) + if trailer != None: + return jsonify(success=True, embed=trailer.embed_url), 200 + else: + return jsonify(success=False, message="Please specify a valid id"), 400 + except Exception as e: + print(f"Error in films at {get_trailer.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + @app.flask.route("/trailer/tv", methods=['GET']) + @app.limiter.limit("10 per minute") + def tv_trailer(): + id = request.args.get("id") + return get_trailer(FilmType.TV, id) + @app.flask.route("/trailer/movie", methods=['GET']) + @app.limiter.limit("10 per minute") + def movie_trailer(): + id = request.args.get("id") + return get_trailer(FilmType.Movie, id) + + @app.flask.route("/get_home_page_categories", methods=['GET']) + @app.limiter.limit("30 per minute") + def get_home_page_categories(): + #print(request.args.get("page")) + page = request.args.get("page") + try: + if page != None: + authorized_account = app.get_authorized_account() + user_id = None + if authorized_account != None and authorized_account.account_exists: + user_id = authorized_account.user_id + #print(f"authorization: {authorized_account}") + categories = app.film_controller.get_next_categories(int(page), user_id) + + if categories != None: + categories_json = app.film_controller.categories_to_json(categories) + #print("Success True") + return jsonify(success=True, data=categories_json), 200 + else: + return jsonify(success=False, message="Could not find page"), 404 + else: + return jsonify(success=False, message="Please specify a page number"), 400 + except Exception as e: + print(f"Error in films at {get_home_page_categories.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + + # /get_category?page=5&media_type=tv&list_type=top_rated&time_window=null + @app.flask.route("/get_category", methods=['GET']) + @app.limiter.limit("30 per minute") + def category_api(): + page = request.args.get("page") + mediaType = request.args.get("media_type") + listType = request.args.get("list_type") + timeWindow = request.args.get("time_window") +# + try: + authorized_account = app.get_authorized_account() + user_id = None + if authorized_account != None and authorized_account.account_exists: + user_id = authorized_account.user_id + if page != None and mediaType != None and listType != None: + category = app.film_controller.get_category( + user_id=user_id, + title=None, + media_type=mediaType, + list_type=listType, + time_window=timeWindow, + has_more_pages=True, + page=page + ) + + #category_results = [] + #if mediaType == FilmType.Movie.value: + # # Is movie list type + # category_results = app.film_controller.tmdb.get_film_list(FilmType.Movie, listType, int(page), timeWindow) +# + #elif mediaType == FilmType.TV.value: + # # Is tv list type + # category_results = app.film_controller.tmdb.get_film_list(FilmType.TV, listType, int(page), timeWindow) +# + #else: + # #print("not tv or movie") + # if listType == "trending": + # category_results = app.film_controller.tmdb.get_trending_films_list(int(page), timeWindow) + # elif listType == "favorites" and user_id != None: + # #print("favorites") + # favorites = app.db_controller.get_favorites(user_id) + # favorites_result = app.film_controller.db_films_to_tmdb_films(favorites) + # category_results = favorites_result + # category_results.has_more_pages = False +# + # #print(favorites) + # #print(favorites_result.results) + # #elif listType == "watch_history" and user_id != None: + # + + if category != None: + category.film_list.has_more_pages = category.has_more_pages + #print(category.film_list.has_more_pages) + categories_json = app.film_controller.tmdb.list_result_to_json(category.film_list) + return jsonify(success=True, data=categories_json), 200 + else: + return jsonify(success=False, message="Page, media type, list type or time window is invalid."), 404 + else: + return jsonify(success=False, message="Please specify a page number"), 400 + except Exception as e: + print(f"Error in films at {category_api.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + + # + @app.flask.route("/search_media", methods=['GET']) + @app.limiter.limit("30 per minute") + def search_media(): + page = request.args.get("page") or "1" + mediaType = request.args.get("media_type") or "all" + query = request.args.get("query") + includeAdult = request.args.get("include_adult") or "false" + + try: + if query != None: + search_results = app.film_controller.tmdb.search_films( + query, + mediaType, + page, + includeAdult + ) + + authorized_account = app.get_authorized_account() + user_id = None + if authorized_account != None and authorized_account.account_exists: + user_id = authorized_account.user_id + + search_results = app.film_controller.do_db_history_passes(user_id, search_results) + #print(f"{query}, {mediaType}, {page}, {includeAdult}") + if search_results != None: + search_json = app.film_controller.tmdb.list_result_to_json(search_results) + + return jsonify(success=True, data=search_json), 200 + else: + return jsonify(success=False, message="Query did not work"), 404 + else: + return jsonify(success=False, message="Please specify a page number"), 400 + except Exception as e: + print(f"Error in films at {search_media.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + + # toggle_favorite + @app.flask.route("/toggle_favorite", methods=['POST']) + @app.limiter.limit("20 per minute") + def toggle_favorite(): + data = authentication.bytes_to_json(request.get_data()) + + try: + tmdb_id = data["tmdb_id"] + media_type = data["media_type"].lower() + + authorized_account = app.get_authorized_account() + if authorized_account != None and authorized_account.account_exists: + film_data = app.film_controller.tmdb.get_details(media_type, tmdb_id, False) + + name = film_data.name + + toggled_favorite = app.db_controller.toggle_favorite(tmdb_id, authorized_account.user_id, + media_type, + name, + datetime.strptime(film_data.release_date, "%Y-%m-%d").year, + film_data.vote_average, + film_data.poster_path + ) + + #print(toggled_favorite) + if (toggled_favorite != None): + return jsonify(success=True, favorited=toggled_favorite), 200 + else: + return jsonify(success=False, message="Enter a correct tmdb_id"), 400 + else: + return jsonify(success=False, message="Please specify a valid token"), 401 + except Exception as e: + print(f"Error in films at {toggle_favorite.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 + + @app.flask.route("/add_to_watch_history", methods=['POST']) + @app.limiter.limit("20 per minute") + def add_to_watch_history(): + data = authentication.bytes_to_json(request.get_data()) + + try: + tmdb_id = data["tmdb_id"] + media_type = data["media_type"].lower() + season = None + episode = None + + if media_type.lower() == "tv": + season = data["season"].lower() + episode = data["episode"].lower() + + + film_data = app.film_controller.tmdb.get_details( + film_type=media_type, + id=tmdb_id, + get_episodes=True + ) + + authorized_account = app.get_authorized_account() + + if authorized_account != None and authorized_account.account_exists: + history_added = app.db_controller.add_film_history( + tmdb_id=tmdb_id, + user_id=authorized_account.user_id, + season=season, + episode=episode, + media_type=film_data.media_type, + name=film_data.name, + release_date=film_data.release_date, + rating=film_data.vote_average, + poster=film_data.poster_path + ) + if (history_added): + return jsonify(success=True), 200 + else: + return jsonify(success=False, message="Maybe incorrect tmdb_id"), 400 + else: + return jsonify(success=False, message="Please specify a valid token"), 401 + except Exception as e: + print(f"Error in films at {add_to_watch_history.__name__}: {e}") + return jsonify(success=False, message="Something went wrong"), 500 \ No newline at end of file diff --git a/server/directories/home.py b/server/directories/home.py new file mode 100644 index 0000000..a4466cc --- /dev/null +++ b/server/directories/home.py @@ -0,0 +1,30 @@ +""" + Home page directories + Made by Monnapse + + 11/6/2024 +""" + +from server.web import WebClass + +from flask import Flask, render_template + +# SETTINGS + +def run(app: WebClass): + print("Home >>> Home directories loaded") + + @app.flask.route("/") + @app.limiter.limit("30 per minute") + def home(): + try: + authorization = app.get_authorization_data() + + return render_template( + app.base, + template = "index.html", + javascript = "index", + authorization=authorization + ) + except Exception as e: + print(f"Error in home controller at {home.__name__}: {e}") \ No newline at end of file diff --git a/server/packages/__pycache__/authentication.cpython-39.pyc b/server/packages/__pycache__/authentication.cpython-39.pyc new file mode 100644 index 0000000..dc3e077 Binary files /dev/null and b/server/packages/__pycache__/authentication.cpython-39.pyc differ diff --git a/server/packages/__pycache__/db.cpython-39.pyc b/server/packages/__pycache__/db.cpython-39.pyc new file mode 100644 index 0000000..e2522a3 Binary files /dev/null and b/server/packages/__pycache__/db.cpython-39.pyc differ diff --git a/server/packages/__pycache__/films.cpython-39.pyc b/server/packages/__pycache__/films.cpython-39.pyc new file mode 100644 index 0000000..ded85c6 Binary files /dev/null and b/server/packages/__pycache__/films.cpython-39.pyc differ diff --git a/server/packages/__pycache__/json_controller.cpython-39.pyc b/server/packages/__pycache__/json_controller.cpython-39.pyc new file mode 100644 index 0000000..62d1271 Binary files /dev/null and b/server/packages/__pycache__/json_controller.cpython-39.pyc differ diff --git a/server/packages/__pycache__/page.cpython-39.pyc b/server/packages/__pycache__/page.cpython-39.pyc new file mode 100644 index 0000000..cfb0183 Binary files /dev/null and b/server/packages/__pycache__/page.cpython-39.pyc differ diff --git a/server/packages/__pycache__/service.cpython-39.pyc b/server/packages/__pycache__/service.cpython-39.pyc new file mode 100644 index 0000000..ce0a72b Binary files /dev/null and b/server/packages/__pycache__/service.cpython-39.pyc differ diff --git a/server/packages/__pycache__/tmdb.cpython-39.pyc b/server/packages/__pycache__/tmdb.cpython-39.pyc new file mode 100644 index 0000000..17b9d0c Binary files /dev/null and b/server/packages/__pycache__/tmdb.cpython-39.pyc differ diff --git a/server/packages/authentication.py b/server/packages/authentication.py new file mode 100644 index 0000000..4d37358 --- /dev/null +++ b/server/packages/authentication.py @@ -0,0 +1,25 @@ +import bcrypt +import json +from flask import Flask, render_template, session, request, jsonify + +def hash_password(plain_password): + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(plain_password.encode(), salt) + return hashed_password + +def check_password(stored_hash, plain_password): + #print(stored_hash, plain_password) + return bcrypt.checkpw(plain_password.encode(), stored_hash) + +def bytes_to_json(bytes_string): + decoded_string = bytes_string.decode('utf-8') + return json.loads(decoded_string) + +# Testing +""" +password = "WhatThatIsCrazy#19294" +hashed_password = hash_password(password) +is_correct_password1 = check_password(hashed_password, password) +is_correct_password2 = check_password(hashed_password, "password") +print(f"Hashed password: {hashed_password}\nPassword 1: {is_correct_password1}\nPassword 2: {is_correct_password2}") +""" \ No newline at end of file diff --git a/server/packages/db.py b/server/packages/db.py new file mode 100644 index 0000000..29354bb --- /dev/null +++ b/server/packages/db.py @@ -0,0 +1,823 @@ +""" + Film Labs Database + Made by Monnapse + + 11/6/2024 +""" + +# Server +from server.packages import authentication +#from server.web import web_class + +# Testing +#import authentication + +from os import environ +import mysql.connector as mysql +import mysql.connector +from mysql.connector import pooling +from mysql.connector.pooling import PooledMySQLConnection + +class Account: + def __init__(self, + user_id: int = None, + username: str = None, + password: str = None, + account_exists: bool = True, + account_message: str = "Account exists" + ): + self.user_id = user_id + self.username = username + self.password = password + self.favorites = [] + self.watch_history = [] + self.account_exists = account_exists + self.account_message = account_message + +class Film: + def __init__(self, + tmdb_id: int = None, + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None, + + progress: str = None, + current_season: int = 1, + current_episode = 1 + ) -> None: + self.tmdb_id = tmdb_id + self.media_type = media_type + self.name = name + self.release_date = release_date + self.rating = rating + self.poster = poster + self.progress = progress + self.current_season = current_season + self.current_episode = current_episode + +class MovieHistory: + def __init__(self, + account_history_id: int = None, + progress: str = None + ) -> None: + self.account_history_id = account_history_id + self.progress = progress + +class EpisodeHistory: + def __init__(self, + account_history_id: int = None, + episode_number: int = None, + season_number: int = None, + progress: str = None + ) -> None: + self.account_history_id = account_history_id + self.episode_number = episode_number + self.season_number = season_number + self.progress = progress + +class WatchHistory: + def __init__(self, + account_history_id: int = None, + tmdb_id: int = None, + user_id: int = None, + movie_history: MovieHistory = None, + episode_history: list[EpisodeHistory] = [] + ) -> None: + self.account_history_id = account_history_id + self.tmdb_id = tmdb_id + self.user_id = user_id + self.movie_history = movie_history + self.episode_history = episode_history + +class FilmLabsDB: + def __init__( + self, + password_max_length, + password_min_length, + username_min_length, + username_max_length + ) -> None: + self.db_connection = None + #self.db_cursor = None + + self.password_max_length = password_max_length + self.password_min_length = password_min_length + self.username_min_length = username_min_length + self.username_max_length = username_max_length + + print("DB >>> Created database class") + + def connect(self, host: str, user: str, password: str, database:str) -> None: + self.db_pool = mysql.connector.pooling.MySQLConnectionPool( + host=host, + user=user, + password=password, + database=database, + ssl_disabled = True, + pool_name="filmlabs_db_pool", + pool_size=5, + pool_reset_session=True, + ) + + #self.db_cursor = self.db_connection.cursor() + + def get_connection(self) -> PooledMySQLConnection: + try: + connection = self.db_pool.get_connection() + #if connection.is_connected(): + # print("Successfully retrieved a connection from the pool") + return connection + except mysql.connector.Error as e: + print(f"DB Error occurred inside {self.get_connection.__name__}: {e}") + return None + + def execute_query_fetchall(self, query: str, params = None, commit: bool = False): + connection = None + cursor = None + try: + connection = self.get_connection() + cursor = connection.cursor() + cursor.execute(query, params) + if commit: + connection.commit() + result = cursor.fetchall() + return result + except mysql.connector.Error as e: + print(f"DB Error occurred inside {self.execute_query_fetchall.__name__}: {e}") + finally: + if cursor: + cursor.close() + if connection: + connection.close() + + def execute_query_lastrowid(self, query: str, params = None, commit: bool = False): + connection = None + cursor = None + try: + connection = self.get_connection() + cursor = connection.cursor() + cursor.execute(query, params) + if commit: + connection.commit() + result = cursor.lastrowid() + return result + except mysql.connector.Error as e: + print(f"DB Error occurred inside {self.execute_query_lastrowid.__name__}: {e}") + finally: + if cursor: + cursor.close() + if connection: + connection.close() + + def create_account(self, username: str, password: str, reenter_password) -> Account: + try: + # Check if password validates + if len(password) <= self.password_max_length and len(password) >= self.password_min_length and password == reenter_password: + if len(username) >= self.username_min_length and len(username) <= self.username_max_length: + if (not self.does_username_exist(username)): + # Now create the account + hashed_password = authentication.hash_password(password) + + # Create queire + query = "insert into account (username, password) values (%s, %s)" + values = (username, hashed_password) +# + ## Execute querie + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + + user_id = self.execute_query_lastrowid(query, values, True)#self.db_cursor.lastrowid + + return Account( + user_id, + username, + password + ) + + else: + return Account( + account_exists=False, + account_message="Username is already taken" + ) + else: + return Account( + account_exists=False, + account_message=f"Username must be atleast {self.username_min_length} characters long and less than {self.username_max_length} characters" + ) + else: + return Account( + account_exists=False, + account_message=f"Password & Reenter Password inputs must match.\nPassword must be atleast {self.password_min_length} characters long and less than {self.password_max_length} characters" + ) + except Exception as e: + print(f"DB Error occurred inside {self.create_account.__name__}: {e}") + + def does_username_exist(self, username: str) -> bool: + try: + query = "select user_id from account where username = %s" + params = (username, ) + + #self.db_cursor.execute(query, username) + results = self.execute_query_fetchall(query, params, False)#self.db_cursor.fetchall() + + if (len(results) > 0): + return True + return False + except Exception as e: + print(f"DB Error occurred inside {self.does_username_exist.__name__}: {e}") + + def get_account(self, user_id: str) -> Account: + try: + query = "select user_id, username from account where user_id = %s" + params = (str(user_id), ) + + #self.db_cursor.execute(query, user_id) + results = self.execute_query_fetchall(query, params, False)#self.db_cursor.fetchall() + + # Check if there are any results if not then that means + # no account with that user_id exists + if results and len(results) > 0: + selected_account = Account( + results[0][0], + results[0][1], + ) + return selected_account + + return Account( + account_exists=False, + account_message="Account doesnt exist" + ) + except Exception as e: + print(f"DB Error occurred inside {self.get_account.__name__}: {e}") + + def login(self, username: str, password: str) -> Account: + try: + query = "select user_id, username, password from account where username = %s" + params = (username, ) + + #self.db_cursor.execute(query, username) + results = self.execute_query_fetchall(query, params, False)#self.db_cursor.fetchall() + + # Check if there are any results if not then that means + # no account with that username exists + if results and len(results) > 0: + hashed_password = results[0][2].encode() + current_account = Account( + results[0][0], + results[0][1], + password + ) + + # Check if password is correct + is_password_correct = authentication.check_password(hashed_password, password) + #print("Password Correct: " + str(is_password_correct)) + + if is_password_correct: + return current_account + else: + return Account( + account_exists=False, + account_message="Incorrect password" + ) + return Account( + account_exists=False, + account_message="Account doesnt exist" + ) + except Exception as e: + print(f"DB Error occurred inside {self.login.__name__}: {e}") + + def add_film(self, + tmdb_id: int = None, + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None + ) -> Film: + # insert into film values (1, "tv", "test", "2024", 10); + # tmdb_id int PK + # media_type varchar(20) + # name varchar(85) + # release_date varchar(8) + # rating float + # poster varchar(45) + + try: + query = "insert into film values (%s, %s, %s, %s, %s, %s)" + values = (tmdb_id, media_type, name, release_date, rating, poster) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + self.execute_query_fetchall(query, values, True) + + return Film( + tmdb_id, + media_type, + name, + release_date, + rating, + poster + ) + except Exception as e: + print(f"DB Error occurred inside {self.add_film.__name__}: {e}") + + def get_film(self, tmdb_id: int) -> Film: + # select * from film where tmdb_id = 1; + try: + query = "select * from film where tmdb_id = %s" + values = (tmdb_id, ) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + + if results and len(results) > 0: + result = results[0] + film = Film( + result[0], + result[1], + result[2], + result[3], + result[4], + result[5] + ) + return film + except Exception as e: + print(f"DB Error occurred inside {self.get_film.__name__}: {e}") + + def check_film(self, + tmdb_id: int = None, + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None + ) -> Film: + try: + film = self.get_film(tmdb_id) + + if film == None: + #print(poster) + self.add_film( + tmdb_id, + media_type, + name, + release_date, + rating, + poster + ) + + return Film( + tmdb_id, + media_type, + name, + release_date, + rating, + poster + ) + + return film + except Exception as e: + print(f"DB Error occurred inside {self.check_film.__name__}: {e}") + + def remove_favorite(self, tmdb_id: int, user_id: int): + # delete from account_favorites where tmdb_id = 2 and user_id = 1 + # tmdb_id int + # user_id int + try: + query = "delete from account_favorites where tmdb_id = %s and user_id = %s" + values = (tmdb_id, user_id) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + self.execute_query_fetchall(query, values, True) + except Exception as e: + print(f"DB Error occurred inside {self.remove_favorite.__name__}: {e}") + + def add_favorite(self, tmdb_id: int, user_id: int): + # insert into account_favorites values(1, 1) + # tmdb_id int + # user_id int + try: + #print("ADDING FAVORITE") + query = "insert into account_favorites values(%s, %s)" + values = (tmdb_id, user_id) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + self.execute_query_fetchall(query, values, True) + except Exception as e: + print(f"DB Error occurred inside {self.add_favorite.__name__}: {e}") + + def get_favorites(self, user_id: int) -> list[Film]: + # select distinct f.* from account_favorites af join film f on af.tmdb_id = f.tmdb_id where af.user_id = 1 group by f.tmdb_id + try: + query = "select distinct f.* from account_favorites af join film f on af.tmdb_id = f.tmdb_id where af.user_id = %s group by f.tmdb_id" + values = (user_id, ) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + + favorites = [] + + if results and len(results) > 0: + for result in results: + film = Film( + result[0], + result[1], + result[2], + result[3], + result[4], + result[5], + ) + favorites.append(film) + + return favorites + except Exception as e: + print(f"DB Error occurred inside {self.get_favorites.__name__}: {e}") + + def is_favorited(self, tmdb_id: int, user_id: int) -> bool: + # select distinct * from account_favorites af where af.user_id = 1 and af.tmdb_id = 1 + try: + query = "select distinct * from account_favorites af where af.user_id = %s and af.tmdb_id = %s" + values = (user_id, tmdb_id) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + + favorites = [] + + if results and len(results) > 0: + return True + + return False + except Exception as e: + print(f"DB Error occurred inside {self.is_favorited.__name__}: {e}") + + def toggle_favorite(self, tmdb_id: int, user_id: int, + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None + ) -> bool: + """ + return (bool) the outcome of the toggle/switch + """ + # select distinct * from account_favorites af where af.user_id = 1 and af.tmdb_id = 1 + try: + self.check_film( + tmdb_id, + media_type, + name, + release_date, + rating, + poster + ) # Check film + + is_favorited = self.is_favorited(tmdb_id, user_id) + + if is_favorited == True: + self.remove_favorite(tmdb_id, user_id) + return False + elif is_favorited == False: + self.add_favorite(tmdb_id, user_id) + return True + + except Exception as e: + print(f"DB Error occurred inside {self.toggle_favorite.__name__}: {e}") + + def get_movie_history(self, + account_history_id: int + ) -> MovieHistory: + # select * from movie_history where account_history_id = 1 + try: + query = "select * from movie_history where account_history_id = %s" + values = (account_history_id, ) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + + if results and len(results) > 0: + result = results[0] + movie = MovieHistory( + result[0], + result[1], + ) + return movie + + return None + except Exception as e: + print(f"DB Error occurred inside {self.get_movie_history.__name__}: {e}") + + def get_episodes_history(self, + account_history_id: int + ) -> list[EpisodeHistory]: + # select * from episode_history where account_history_id = 1 + try: + query = "select * from episode_history where account_history_id = %s" + values = (account_history_id, ) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + episodes = [] + + if results and len(results) > 0: + for result in results: + episode = EpisodeHistory( + result[0], + result[1], + result[2], + result[3] + ) + #print(episode.progress) + episodes.append(episode) + return episodes + + return None + except Exception as e: + print(f"DB Error occurred inside {self.get_episodes_history.__name__}: {e}") + + #def do_history_pass(self, user_id, films: list[]) + + def has_seen(self, user_id: int, tmdb_id: int, completely_fill: bool = True) -> WatchHistory: + # select * from account_watch_history where user_id = 1 and tmdb_id = 94605 + try: + query = "select * from account_watch_history where user_id = %s and tmdb_id = %s" + values = (user_id, tmdb_id) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values, False)#self.db_cursor.fetchall() + + if results and len(results) > 0: + result = results[0] + + watch_history = WatchHistory( + result[0], + result[1], + result[2] + ) + + if completely_fill: + watch_history.movie_history = self.get_movie_history(watch_history.account_history_id) + watch_history.episode_history = self.get_episodes_history(watch_history.account_history_id) + + return watch_history + + return None + except Exception as e: + print(f"DB Error occurred inside {self.get_episodes_history.__name__}: {e}") + + def get_history(self, user_id: int) -> list[Film]: + try: + history = self.get_watch_history(user_id, True) + + history_films = [] + + if history and len(history) > 0: + for watch_history in history: + db_film_details = self.get_film(watch_history.tmdb_id) + + season = None + episode = None + + if db_film_details.media_type == "tv" and watch_history.episode_history != None and len(watch_history.episode_history) > 0: + current_episode_details = watch_history.episode_history[len(watch_history.episode_history)-1] + season = current_episode_details.season_number + episode = current_episode_details.episode_number + + film = Film( + db_film_details.tmdb_id, + db_film_details.media_type, + db_film_details.name, + db_film_details.release_date, + db_film_details.rating, + db_film_details.poster, + + current_episode_details.season_number, + current_episode_details.episode_number + ) + history_films.append(film) + return history_films + except Exception as e: + print(f"DB Error occurred inside {self.get_history.__name__}: {e}") + + def get_watch_history(self, + user_id: int, + completely_fill: bool = True + ) -> list[WatchHistory]: + # select f.tmdb_id, coalesce(group_concat(tv.progress), group_concat(movie.progress)) as progress, coalesce(group_concat(tv.episode_id)) as episode_id from account_watch_history awh join film f on awh.tmdb_id = f.tmdb_id left join episode_history tv on f.media_type = "tv" and awh.account_history_id = tv.account_history_id left join movie_history movie on f.media_type = "movie" and awh.account_history_id = movie.account_history_id where awh.user_id = 1 group by f.tmdb_id + try: + query = "select * from account_watch_history where user_id = %s;" + values = (user_id, ) + #self.db_cursor.execute(query, values) + + results = self.execute_query_fetchall(query, values)#self.db_cursor.fetchall() + history = [] + + if results and len(results) > 0: + for result in results: + watch_history = WatchHistory( + result[0], + result[1], + result[2] + ) + + if completely_fill: + watch_history.movie_history = self.get_movie_history(watch_history.account_history_id) + watch_history.episode_history = self.get_episodes_history(watch_history.account_history_id) + + history.append(watch_history) + return history + except Exception as e: + print(f"DB Error occurred inside {self.get_watch_history.__name__}: {e}") + + def add_movie_history(self, + account_history_id: int, + progress: str + ): + # insert into movie_history values (2, "00:00:00") + try: + # Check if movie already added + movie = self.get_movie_history(account_history_id) + if movie != None: + return + + # First Query + query = "insert into movie_history values (%s, %s)" + values = (account_history_id, progress) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + self.execute_query_fetchall(query, values, True) + except Exception as e: + print(f"DB Error occurred inside {self.add_movie_history.__name__}: {e}") + + def add_episode_history(self, + account_history_id: int, + episode_number: int, + season_number: int, + progress: str + ): + # insert into episode_history values (1, 1, 1, "00:00:00") + try: + # Check if episode already added + episodes = self.get_episodes_history(account_history_id) + if episodes != None: + for episode in episodes: + if episode.episode_number == episode_number and season_number == season_number: + return + + # First Query + query = "insert into episode_history values (%s, %s, %s, %s)" + values = (account_history_id, episode_number, season_number, progress) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + self.execute_query_fetchall(query, values, True) + except Exception as e: + print(f"DB Error occurred inside {self.add_episode_history.__name__}: {e}") + + def add_watch_history(self, + tmdb_id: int, + user_id: int, + + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None + ) -> int: + """ + returns `account_history_id` + """ + # insert into account_watch_history (tmdb_id, user_id) values (1, 1) + try: + # Check if film is in db, because if isnt than gives error + self.check_film( + tmdb_id = tmdb_id, + media_type = media_type, + name = name, + release_date = release_date, + rating = rating, + poster = poster + ) + + # First Query + query = "insert into account_watch_history (tmdb_id, user_id) values (%s, %s)" + values = (str(tmdb_id), str(user_id)) + #self.db_cursor.execute(query, values) + #self.db_connection.commit() + + watch_history_id = self.execute_query_lastrowid(query, values, True)#self.db_cursor.lastrowid + return watch_history_id + except Exception as e: + print(f"DB Error occurred inside {self.add_watch_history.__name__}: {e}") + + def get_episode(self, episode_history: list[EpisodeHistory], season: int, episode: int) -> EpisodeHistory: + if episode_history != None and len(episode_history) > 0: + for current_episode in episode_history: + if current_episode.episode_number == int(episode) and current_episode.season_number == int(season): + return current_episode + return None + + def get_watch_history_by_id(self, + tmdb_id: int, + user_id: int + ) -> WatchHistory: + history = self.get_watch_history(user_id, True) + + if len(history) > 0: + for i in history: + #print(i.tmdb_id) + if i.tmdb_id == int(tmdb_id): + return i + return None + + def add_film_history(self, + tmdb_id: int, + user_id: int, + season: int, + episode: int, + + media_type: str = None, + name: str = None, + release_date: str = None, + rating: float = None, + poster: str = None + ) -> bool: + try: + history = self.get_watch_history_by_id(tmdb_id, user_id) + + if not history: + self.add_watch_history( + tmdb_id = tmdb_id, + user_id = user_id, + + media_type = media_type, + name = name, + release_date = release_date, + rating = rating, + poster = poster + ) + history = self.get_watch_history_by_id(tmdb_id, user_id) + + #print(history) + if history: + # Already exists + if season == None and episode == None and history.movie_history == None: + # Is Movie without movie history + #print("is movie") + self.add_movie_history(history.account_history_id, "00:00:00") + elif season != None and episode != None: + # Is TV Show + #print("is tv show") + has_episode = self.get_episode(history.episode_history, season, episode) + #print(has_episode) + if has_episode == None: + self.add_episode_history(history.account_history_id, episode, season, "00:00:00") + else: + return False + + return True + except Exception as e: + print(f"DB Error occurred inside {self.add_film_history.__name__}: {e}") + + return False + + def get_most_recent_history_episode(self, tmdb_id, user_id, history: list[EpisodeHistory]): + try: + watch_history = history + if watch_history == None: + watch_history = self.get_watch_history_by_id(tmdb_id, user_id).episode_history + + if watch_history and len(watch_history) > 0: + highest_episode = watch_history[0] + #print(highest_episode) + for episode in watch_history: + if episode.season_number >= highest_episode.season_number and episode.episode_number > highest_episode.episode_number: + highest_episode = episode + + return highest_episode + + return None + except Exception as e: + print(f"DB Error occurred inside {self.get_watch_history_by_id.__name__}: {e}") + + # TODO + # Add film progress support + + +# Testing +""" +db = FilmLabsDB( + 5,5,5,5 +) +db.connect( + "localhost", + environ.get("MYSQL_USER"), + environ.get("MYSQL_PASSWORD"), + environ.get("FILMLABS_DB") +) + +admin_account = db.create_account("sugriva", "fortnite") +#login = db.login("Monnapse", "Password") +#film = db.get_film(1) +print(admin_account.user_id) +""" \ No newline at end of file diff --git a/server/packages/films.py b/server/packages/films.py new file mode 100644 index 0000000..d63593c --- /dev/null +++ b/server/packages/films.py @@ -0,0 +1,268 @@ +""" + FilmLabs Page Manager + Made by Monnapse + + 11/7/2024 +""" + +from typing import Optional, Union +from server.packages.tmdb import FilmType, TVListType, MovieListType, TimeWindow, TMDB, ListResult, Movie, TV, ListResult, TVSeason, TVEpisode +from server.packages.db import FilmLabsDB, Film, EpisodeHistory + +class Category: + def __init__(self, + title: str, + media_type: FilmType, + list_type: Optional[Union[TVListType, MovieListType]], + time_window: TimeWindow = TimeWindow.Day, + has_more_pages: bool = False + ) -> None: + + self.title = title + self.media_type = media_type + self.list_type = list_type + self.time_window = time_window + self.film_list: ListResult = None + self.has_more_pages = has_more_pages + +class FilmsController: + def __init__(self, tmdb: TMDB, db: FilmLabsDB, default_page_layout: dict) -> None: + self.tmdb = tmdb + self.db = db + self.default_page_layout = default_page_layout["pages"] + + def categories_to_json(self, categories: list[Category]): + result = [] + + for i in categories: + category = i.__dict__ + category["film_list"] = self.tmdb.list_result_to_json(i.film_list) + + result.append(category) + + return result + + def get_raw_category_by_page(self, page: int): + try: + for i in self.default_page_layout: + if i["current_page"] == page: + return i + except: + return None + + def null_check(self, string: str) -> any: + if string == None or string == "null": + return None + return string + + def get_category_name(self, media_type: str = None, list_type: str = None, time_window: str = None) -> str: + try: + media_type = self.null_check(media_type) + list_type = self.null_check(list_type) + time_window = self.null_check(time_window) + for page in self.default_page_layout: + for list in page["categories"]: + if list["media_type"] == media_type and list["list_type"] == list_type and list["time_window"] == time_window: + return list["name"] + except: + return "No title" + + def db_film_to_tmdb_film(self, film: Film) -> Optional[Union[TV, Movie]]: + if film.media_type == "tv": + return TV( + #tmdb_id + #media_type + #name + #year + #rating + #poster + id = film.tmdb_id, + name = film.name, + release_date = film.release_date, + vote_average = film.rating, + poster_path = film.poster + + ) + elif film.media_type == "movie": + return Movie( + #tmdb_id + #media_type + #name + #year + #rating + #poster + id = film.tmdb_id, + title = film.name, + release_date = film.release_date, + vote_average = film.rating, + poster_path = film.poster + ) + + def db_films_to_tmdb_films(self, db_films: list[Film]) -> ListResult: + tmdb_films = [] + for db_film in db_films: + tmdb_films.append(self.db_film_to_tmdb_film(db_film)) + list_result = ListResult(results=tmdb_films) + return list_result + + def seen_episode(self, current_episode: TVEpisode, episodes: list[EpisodeHistory]) -> EpisodeHistory: + for episode in episodes: + if current_episode.season_number == episode.season_number and current_episode.episode_number == episode.episode_number: + return episode + return None + + def do_db_episodes_pass(self, episodes: list[TVEpisode], episodes_history: list[EpisodeHistory]): + episodes_pass = [] + + for episode in episodes: + #print(f"Season: {episode.season_number}, Episode: {episode.episode_number}, Progress: {episode.progress}") + seen = self.seen_episode(episode, episodes_history) + if seen != None: + #print(f"Seen: Season: {seen.season_number}, Episode: {seen.episode_number}, Progress: {seen.progress}") + episode.progress = seen.progress + episodes_pass.append(episode) + return episodes_pass + + def do_db_history_pass(self, user_id: int, film: Optional[Union[Movie, TV]]) -> Optional[Union[Movie, TV]]: + try: + if user_id == None: return film + + seen_film = self.db.has_seen(user_id, film.id, True) + #for season in film_details.seasons: + #for episode in seen_film.episode_history: + # print(f"Season: {episode.season_number}, Episode: {episode.episode_number}, Progress: {episode.progress}") + #print(seen_film) + if seen_film: + if film.media_type == "movie": + film.progress = seen_film.movie_history.progress + elif film.media_type == "tv": + highest_episode = self.db.get_most_recent_history_episode(seen_film.tmdb_id, seen_film.user_id, seen_film.episode_history) + + season_pass = [] + + for season in film.seasons: + new_episodes = self.do_db_episodes_pass(season.episodes, seen_film.episode_history) + season.episodes = new_episodes + #print(new_episodes) + season_pass.append(season) + + film.seasons = season_pass + #for season in film.seasons: + # for episode in season.episodes: + # print(f"Season: {episode.season_number}, Episode: {episode.episode_number}, Progress: {episode.progress}") + if highest_episode != None: + #print(seen_film.episode_history[0].progress) + film.current_season = highest_episode.season_number + film.current_episode = highest_episode.episode_number + #film.progress = None + #print(f"Season: {highest_episode.season_number}, Episode: {highest_episode.episode_number}, Progress: {highest_episode.progress}") + return film + except Exception as e: + print(f"Error in films controller at {self.do_db_history_pass.__name__}") + + return film + + def do_db_history_passes(self, user_id: int, films: ListResult) -> ListResult: + try: + if user_id == None: return films + new_list = [] + + if films and films.results and len(films.results) > 0: + for film in films.results: + #film = self.db.has_seen(user_id, film.id) + #if film: + new_list.append(self.do_db_history_pass(user_id, film)) + + return ListResult(results=new_list) + except Exception as e: + print(f"Error in films controller at {self.do_db_history_passes.__name__}: {e}") + + return films + + def get_category(self, + user_id: int, + title: str, + media_type: FilmType, + list_type: Optional[Union[TVListType, MovieListType]], + time_window: TimeWindow = TimeWindow.Day, + has_more_pages: bool = False, + page: int = 1 + ) -> Category: + try: + category = Category( + title=title, + media_type=media_type, + list_type=list_type, + time_window=time_window, + has_more_pages=has_more_pages + ) + + if category.media_type == None or category.media_type == "null": + # Media type is both + #print("category.media_type == None") + # trending + #category.film_list = self.tmdb.get_trending_films_list(1, category.time_window) + if category.list_type == "trending": + category.film_list = self.tmdb.get_trending_films_list(page, category.time_window) + #print("Trending") + elif category.list_type == "favorites" and user_id != None: + #print("favorites") + favorites = self.db.get_favorites(user_id) + favorites_result = self.db_films_to_tmdb_films(favorites) + category.film_list = favorites_result + category.has_more_pages = False + category.film_list.has_more_pages = False + #print(favorites) + elif category.list_type == "watch_history" and user_id != None: + #print("watch_history") + history = self.db.get_history(user_id) + + history_result = self.db_films_to_tmdb_films(history) + category.film_list = history_result + category.has_more_pages = False + category.film_list.has_more_pages = False + #elif category.media_type == None and category.list_type == "favorites": + # # Get favorited films + elif category.media_type == FilmType.Movie.value: + # Is movie list type + category.film_list = self.tmdb.get_film_list(FilmType.Movie, category.list_type, page, category.time_window) + elif category.media_type == FilmType.TV.value: + # Is tv list type + category.film_list = self.tmdb.get_film_list(FilmType.TV, category.list_type, page, category.time_window) + + if category.film_list != None: + category.film_list = self.do_db_history_passes(user_id, category.film_list) + + #print(category.film_list.results) + + return category + except Exception as e: + print(f"Error in films controller at {self.get_category.__name__}: {e}") + + + def get_next_categories(self, current_page: int, user_id: int) -> list[Category]: + try: + if self.default_page_layout != None and self.get_raw_category_by_page(current_page): + page = self.get_raw_category_by_page(current_page) + + if page == None: return + + categories = [] + + for category_data in page["categories"]: + category = self.get_category( + user_id=user_id, + title=category_data["name"], + media_type=category_data["media_type"], + list_type=category_data["list_type"], + time_window=category_data["time_window"], + has_more_pages=True, + page=1 + ) + + if category.film_list and len(category.film_list.results) > 0: + categories.append(category) + + return categories + except Exception as e: + print(f"Error in films controller at {self.get_next_categories.__name__}: {e}") \ No newline at end of file diff --git a/server/packages/json_controller.py b/server/packages/json_controller.py new file mode 100644 index 0000000..ba15cff --- /dev/null +++ b/server/packages/json_controller.py @@ -0,0 +1,5 @@ +import json + +def load_json(directory: str): + with open(directory) as j: + return json.load(j) \ No newline at end of file diff --git a/server/packages/service.py b/server/packages/service.py new file mode 100644 index 0000000..91cab0b --- /dev/null +++ b/server/packages/service.py @@ -0,0 +1,59 @@ +""" + Service Controller + Made by Monnapse + + 11/8/2024 +""" + +class Service: + def __init__(self, name: str, movie: str, tv: str) -> None: + self.name = name + self.movie_api = movie + self.tv_api = tv + + def get_movie_url(self, id) -> str: + try: + return self.movie_api.format(id=str(id)) + except: + return None + + def get_tv_url(self, id, season, episode) -> str: + try: + return self.tv_api.format(id=str(id),season=str(season),episode=str(episode)) + except: + return None + +class ServiceController: + def __init__(self, services: dict): + self.services = services + + def get_services(self) -> list[Service]: + services = [] + + for service_data in self.services: + service = Service( + service_data["name"], + service_data["movie"], + service_data["tv"] + ) + services.append(service) + + return services + + def get_service(self, name) -> Service: + for service in self.get_services(): + if service.name == name: + return service + + return None + + def get_service_data(self, service_name): + services = self.get_services() + + current_service_class = self.get_service(service_name) + + if (service_name == None or current_service_class == None): + # Service not found in argument + current_service_class = services[0] + + return current_service_class \ No newline at end of file diff --git a/server/packages/tmdb.py b/server/packages/tmdb.py new file mode 100644 index 0000000..7847fb1 --- /dev/null +++ b/server/packages/tmdb.py @@ -0,0 +1,573 @@ +""" + TMDB API Wrapper + Made by Monnapse + + 11/7/2024 +""" + +import requests +from requests import Response +from enum import Enum +from typing import Optional, Union + +# Enums +class FilmType(Enum): + All = "all" + Movie = "movie" + TV = "tv" + +class TimeWindow(Enum): + Day = "day" + Week = "week" + +class MovieListType(Enum): + NowPlaying = "now_playing" + Popular = "popular" + TopRated = "top_rated" + UpComing = "upcoming" + Trending = "trending" + +class TVListType(Enum): + AiringToday = "airing_today" + OnTheAir = "on_the_air" + Popular = "popular" + TopRated = "top_rated" + Trending = "trending" + +# Classes +class TVEpisode: + def __init__(self, + air_date: str = None, + episode_number: int = None, + episode_type: str = None, + id: int = None, + name: str = None, + overview: str = None, + production_code: str = None, + runtime: int = None, + season_number: int = None, + show_id: int = None, + still_path: str = None, + vote_average: float = None, + vote_count: int = None + ) -> None: + self.air_date = air_date + self.episode_number = episode_number + self.episode_type = episode_type + self.id = id + self.name = name + self.overview = overview + self.production_code = production_code + self.runtime = runtime + self.season_number = season_number + self.show_id = show_id + self.still_path = still_path + self.vote_average = vote_average + self.vote_count = vote_count + + self.progress = None + +class TVSeason: + def __init__(self, + air_date: str = None, + episode_count: int = None, + id: int = None, + name: str = None, + overview: str = None, + poster_path: str = None, + season_number: int = None, + vote_average: float = None, + episodes: list[TVEpisode] = [] + ) -> None: + self.air_date = air_date + self.episode_count = episode_count + self.id = id + self.name = name + self.overview = overview + self.poster_path = poster_path + self.season_number = season_number + self.vote_average = vote_average + self.episodes = episodes + +class Movie: + def __init__( + self, + adult: bool = None, + backdrop_path: str = None, + genre_ids: list[int] = None, + id: int = None, + original_language: str = None, + original_title: str = None, + overview: str = None, + popularity: float = None, + poster_path: str = None, + release_date: str = None, + title: str = None, + video: bool = None, + vote_average: int = None, + vote_count: int = None + ): + self.media_type = "movie" + + self.adult = adult + self.backdrop_path = backdrop_path + self.genre_ids = genre_ids + self.id = id + self.original_language = original_language + self.original_name = original_title + self.overview = overview + self.popularity = popularity + self.poster_path = poster_path + self.release_date = release_date + self.name = title + self.video = video + self.vote_average = vote_average + self.vote_count = vote_count + + self.progress = None + +class TV: + def __init__( + self, + adult: bool = None, + backdrop_path: str = None, + genre_ids: list[int] = None, + id: int = None, + origin_country: list[str] = None, + original_language: str = None, + original_name: str = None, + overview: str = None, + popularity: float = None, + poster_path: str = None, + release_date: str = None, + name: str = None, + vote_average: float = None, + vote_count: int = None, + + seasons: list[TVSeason] = [], + ): + self.media_type = "tv" + + self.adult = adult + self.backdrop_path = backdrop_path + self.genre_ids = genre_ids + self.id = id + self.origin_country = origin_country + self.original_language = original_language + self.original_name = original_name + self.overview = overview + self.popularity = popularity + self.poster_path = poster_path + self.release_date = release_date + self.name = name + self.vote_average = vote_average + self.vote_count = vote_count + + self.seasons = seasons + self.current_season = 1 + self.current_episode = 1 + + def get_season(self, season_number: int) -> TVSeason: + for season in self.seasons: + if season.season_number == int(season_number): + return season + return None + +class FilmVideo: + def __init__(self, + iso_639_1: str = None, + iso_3166_1: str = None, + name: str = None, + key: str = None, + site: str = None, + size: int = None, + type: str = None, + official: bool = None, + published_at: str = None, + id: str = None, + url: str = None, + embed_url: str = None + ) -> None: + self.name = name + self.key = key + self.site = site + self.size = size + self.type = type + self.official = official + self.published_at = published_at + self.id = id + self.url = url + self.embed_url = embed_url + +class ListResult: + def __init__(self, + date_minimum: str = None, + date_maximum: str = None, + page: int = None, + results: Optional[list[Union[Movie, TV]]] = [], + total_pages: int = None, + total_results: int = None + ) -> None: + self.date_minimum = date_minimum + self.date_maximum = date_maximum + self.page = page + self.results = results + self.total_pages = total_pages + self.total_results = total_results + self.has_more_pages = True + +class TMDB: + def __init__(self, key, poster_sizing: str = "original"): + self.key = key + + self.poster_sizing = poster_sizing + + self.api_base_url = "https://api.themoviedb.org/3/" + self.config = self.get_config() + + def get_config(self): + response = self.send_api("configuration") + if (response.status_code == 200): + # Success + return response.json()["images"] + + def send_api(self, url: str) -> Response: + headers = { + "accept": "application/json", + #"Authorization": self.key + } + paramter_method = "?" + if ("?" in url): + paramter_method = "&" + return requests.get(f"{self.api_base_url}{url}{paramter_method}api_key={self.key}", headers=headers) + + def to_img_url(self, path: str): + if path == None: + return None + try: + base_url = self.config["base_url"] + return f"{base_url}{self.poster_sizing}{path}" + except: + return path + + def video_to_watch_url(self, site: str = "", key: str = ""): + # https://www.youtube.com/watch?v={key} + site = str(site).lower() + if site == "youtube": + return f"https://www.youtube.com/watch?v={key}" + + def video_to_embed_url(self, site: str = "", key: str = ""): + # https://www.youtube.com/embed/XZ8daibM3AE + site = str(site).lower() + if site == "youtube": + return f"https://www.youtube.com/embed/{key}" + + def to_film_video(self, data: dict) -> FilmVideo: + site = data.get("site") + key = data.get("key") + url = self.video_to_watch_url(site, key) + embed_url = self.video_to_embed_url(site, key) + + video_class = FilmVideo( + iso_639_1 = data.get("iso_639_1"), + iso_3166_1 = data.get("iso_3166_1"), + name = data.get("name"), + key = key, + site = site, + size = data.get("size"), + type = data.get("type"), + official = data.get("official"), + published_at = data.get("published_at"), + id = data.get("id"), + url = url, + embed_url = embed_url + ) + + return video_class + + def to_tv_episode(self, data: dict) -> TVEpisode: + episode_class = TVEpisode( + air_date = data.get("air_date"), + episode_number = data.get("episode_number"), + episode_type = data.get("episode_type"), + id = data.get("id"), + name = data.get("name"), + overview = data.get("overview"), + production_code = data.get("production_code"), + runtime = data.get("runtime"), + season_number = data.get("season_number"), + show_id = data.get("show_id"), + still_path = data.get("still_path"), + vote_average = data.get("vote_average"), + vote_count = data.get("vote_count"), + ) + return episode_class + + def get_tv_season_episodes(self, tv_id: int, season: int) -> list[TVEpisode]: + # tv/1396/season/0?language=en-US" + api = f"tv/{str(tv_id)}/season/{str(season)}?language=en-US" + + response = self.send_api(api) + + if (response.status_code == 200): + response_json = response.json() + + episodes = [] + + if (response_json["episodes"] != None): + + for episode in response_json["episodes"]: + new_episode_class = self.to_tv_episode(episode) + #print(new_episode_class.progress) + episodes.append(new_episode_class) + return episodes + + return [] + + def to_tv_season(self, data: dict, get_episodes: bool = False, tv_id: int = None) -> TVSeason: + episodes = [] + season_number = data.get("season_number") + + if get_episodes: + episodes = self.get_tv_season_episodes(tv_id, season_number) + + season_class = TVSeason( + air_date = data.get("air_date"), + episode_count = data.get("episode_count"), + id = data.get("id"), + name = data.get("name"), + overview = data.get("overview"), + poster_path = data.get("poster_path"), + season_number = season_number, + vote_average = data.get("vote_average"), + episodes = episodes + ) + return season_class + + def to_movie(self, data: dict) -> Movie: + movie_class = Movie( + adult = data.get("adult"), + backdrop_path = self.to_img_url(data.get("backdrop_path")), + genre_ids = data.get("genre_ids"), + id = data.get("id"), + original_language = data.get("original_language"), + original_title = data.get("original_title"), + overview = data.get("overview"), + popularity = data.get("popularity"), + poster_path = self.to_img_url(data.get("poster_path")), + release_date = data.get("release_date"), + title = data.get("title"), + video = data.get("video"), + vote_average = data.get("vote_average"), + vote_count = data.get("vote_count") + ) + return movie_class + + def to_tv(self, data: dict, get_episodes: bool = False) -> TV: + tv_class = TV( + adult = data.get("adult"), + backdrop_path = self.to_img_url(data.get("backdrop_path")), + genre_ids = data.get("genre_ids"), + id = data.get("id"), + origin_country = data.get("origin_country"), + original_language = data.get("original_language"), + original_name = data.get("original_name"), + overview = data.get("overview"), + popularity = data.get("popularity"), + poster_path = self.to_img_url(data.get("poster_path")), + release_date = data.get("first_air_date"), + name = data.get("name"), + vote_average = data.get("vote_average"), + vote_count = data.get("vote_count") + ) + + try: + if data["seasons"] != None: + seasons = [] + for season in data["seasons"]: + tv_season = self.to_tv_season(season, get_episodes, str(tv_class.id)) + #for i in tv_season.episodes: + # print(i.progress) + seasons.append(tv_season) + tv_class.seasons = seasons + #print(seasons[1].episodes) + except: + return tv_class + + return tv_class + + def to_film_class(self, film: dict, film_type: FilmType): + if (enum_to_string(film_type) == FilmType.Movie.value): + return self.to_movie(film) + elif (enum_to_string(film_type) == FilmType.TV.value): + return self.to_tv(film) + elif (enum_to_string(film_type) == FilmType.All.value): + #print(self.to_film_class(film, film["media_type"])) + return self.to_film_class(film, film["media_type"]) + else: + return None + + def json_to_list_result(self, json_dict: dict, film_type: FilmType) -> ListResult: + list_result = ListResult( + page = json_dict["page"], + results = [], + total_pages = json_dict["total_pages"], + total_results = json_dict["total_results"] + ) + + films_results = json_dict["results"] + + for film in films_results: + film_class = self.to_film_class(film, film_type) + list_result.results.append(film_class) + + return list_result + + + def get_film_list(self, film_type: FilmType, list_type: Optional[Union[TVListType, MovieListType]] = None, page: int = 1, time_window: TimeWindow = TimeWindow.Day) -> ListResult: + """ + Gets a list of films either movie or tv + + Args: + time_window (TimeWindow): you only need to specify this if the list requires it (known required lists that require this are 'trending') + """ + + time = enum_to_string(time_window) + media_type = enum_to_string(film_type) + media_list_type = enum_to_string(list_type) + + api = f"{media_type}/{media_list_type}?language=en-US&page={str(page)}" + + if media_type == FilmType.All.value and media_list_type == "trending": + api = f"{media_list_type}/all/{time}?language=en-US&page={str(page)}" + + #if media_list_type == "trending" and media_type != FilmType.All.value: + # # If list is trending then add the time_window as required + # api = f"{media_list_type}/{media_type}/{time}?language=en-US&page={str(page)}" + + response = self.send_api(api) + + if (response.status_code == 200): + response_json = response.json() + return self.json_to_list_result(response_json, film_type) + + return None + + def get_trending_films_list(self, page: int = 1, time_window: TimeWindow = TimeWindow.Day) -> ListResult: + list = self.get_film_list(FilmType.All, MovieListType.Trending, page, time_window) or ListResult() + + return list + + def list_result_to_json(self, list: ListResult): + result = list.__dict__ + + films_list = [] + + for i in list.results: + if (i != None): + films_list.append(i.__dict__) + #if i.media_type == "tv" and len(i.seasons) > 0: + # print(i.seasons) + + result["results"] = films_list + #print(result) + return result + + def search_films(self, query: str, film_type: FilmType = FilmType.All, page: int = 1, includeAdult: str = "false"): + media_type = enum_to_string(film_type) + + if media_type == FilmType.All.value: + media_type = "multi" + + api = f"search/{media_type}?query={str(query)}&include_adult={str(includeAdult)}&language=en-US&page={str(page)}" + response = self.send_api(api) + + if (response.status_code == 200): + response_json = response.json() + #print(response_json) + return self.json_to_list_result(response_json, film_type) + return None + + def get_details(self, film_type: FilmType = FilmType.All, id: str = None, get_episodes: bool = False) -> Optional[Union[Movie, TV]]: + # Movies "movie/693134?language=en-US" + # TV "tv/series_id?language=en-US" + + media_type = enum_to_string(film_type) + + api = f"{media_type}/{str(id)}?language=en-US" + + response = self.send_api(api) + + if (response.status_code == 200): + response_json = response.json() + + if media_type == FilmType.Movie.value: + return self.to_movie(response_json) + elif media_type == FilmType.TV.value: + return self.to_tv(response_json, get_episodes) + + return None + + def get_videos(self, film_type: FilmType = FilmType.All, id: str = None) -> list[FilmVideo]: + # movie/693134/videos?language=en-US + # tv/1396/videos?language=en-US" + + media_type = enum_to_string(film_type) + + api = f"{media_type}/{str(id)}/videos?language=en-US" + + response = self.send_api(api) + + if (response.status_code == 200): + response_json = response.json() + + video_list = [] + + for video in response_json["results"]: + video_list.append(self.to_film_video(video)) + + return video_list + return None + + def get_trailer(self, film_type: FilmType = FilmType.All, id: str = None) -> FilmVideo: + videos = self.get_videos(film_type, id) + + trailers = [] + + for video in videos: + try: + if video.type.lower() == "trailer": + trailers.append(video) + except: + pass + #print(f"{video.type}, {video.name}") + + if len(trailers) > 0: + return trailers[len(trailers)-1] + else: + return None + +# Functions +def sort_by_rating(film: Optional[Union[Movie, TV]]): + return film.vote_average + +def enum_to_string(enum): + if (type(enum) is not str and enum != None): + enum = enum.value + return enum + +# TESTING +""" +from os import environ +api = TMDB(environ.get("TMDB_API_KEY")) +#movies_list = api.get_movie_list(MovieListType.UpComing, 5) +#tv_list = api.get_film_list(FilmType.TV, TVListType.AiringToday, 5) +#trending_film_list = api.get_trending_films_list(1, TimeWindow.Week) +#search_query = api.search_films("bojack", FilmType.All, 1, "false") +#dict = api.list_result_to_json(search_query) +#details = api.get_details(FilmType.TV, 1396, True) +#print(details.seasons[3].episodes[3].name) +films_video = api.get_trailer(FilmType.TV, 1396) +print(f"Trailer Url: {films_video.url}, Trailer Name: {films_video.name}") +# +""" \ No newline at end of file diff --git a/server/web.py b/server/web.py new file mode 100644 index 0000000..040bcc4 --- /dev/null +++ b/server/web.py @@ -0,0 +1,115 @@ +""" + Web Manager + Made by Monnapse + + 11/6/2024 +""" + +from flask import Flask, render_template, session, request +from flask_limiter import Limiter +from server.packages.db import FilmLabsDB, Account +from server.packages import db +from server.packages.films import FilmsController +from server.packages.service import ServiceController +import os, glob +import importlib.util +import json +from flask_jwt_extended import JWTManager, decode_token +import time +from datetime import timedelta + +class WebClass: + # This defines the main attributes of the class + base = "base.html" + flask: Flask = None + + # The initializer of the class + # Defines the class attributes + def __init__( + app, + flask_app: Flask, + limiter: Limiter, + db_controller: FilmLabsDB, + jwt: JWTManager, + film_controller: FilmsController, + service_controller: ServiceController, + token_max_days: int, + password_max_length: int, + password_min_length: int, + username_min_length: int, + username_max_length: int + ) -> None: + # Defaults + app.flask = flask_app + app.limiter = limiter + app.db_controller = db_controller + app.jwt = jwt + app.film_controller = film_controller + app.service_controller = service_controller + + # Settings + app.token_max_days = token_max_days + app.password_max_length = password_max_length + app.password_min_length = password_min_length + app.username_min_length = username_min_length + app.username_max_length = username_max_length + + print("Web Controller >>> is running") + + # Run Directories goes through the Directories Folder + # and imports the module and runs it + # directories folder contains all the directories of the site + def run_directories(app): + print("running directories"); + for filename in glob.glob(os.path.join("server/directories", '*.py')): + spec = importlib.util.spec_from_file_location(filename, filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.run(app) + + def get_authorized_account(app) -> Account: + #current_user = get_jwt_identity() + token = request.cookies.get("access_token") + if (token): + decoded_token = decode_token(token) + # Check if expired + #logged_in_timestamp = decoded_token.get("nbf") + #print(decoded_token) + + #if (time.time() - logged_in_timestamp): + # # Token is expired + # return False + + + # Check if valid token + # Check if user_id is valid + user_id = decoded_token.get("sub") + + selected_account = app.db_controller.get_account(user_id) + + if (selected_account and selected_account.account_exists): + return selected_account + else: + return Account( + account_exists=False, + account_message="Could not find account with that user_id" + ) + + return Account( + account_exists=False, + account_message="Token is invalid" + ) + def is_logged_in(app) -> bool: + return app.check_authentication() != None + def get_authorization_data(app) -> dict[bool, str]: + authorized_account = app.get_authorized_account() + if authorized_account.account_exists: + return { + "logged_in": authorized_account.account_exists, + "username": authorized_account.username, + "user_id": authorized_account.user_id + } + else: + return { + "logged_in": authorized_account.account_exists + } diff --git a/services.json b/services.json new file mode 100644 index 0000000..5d3d00b --- /dev/null +++ b/services.json @@ -0,0 +1,77 @@ +[ + { + "name": "Embed Su", + "movie": "https://embed.su/embed/movie/{id}", + "tv": "https://embed.su/embed/tv/{id}/{season}/{episode}" + }, + { + "name": "Moviee", + "movie": "https://moviee.tv/embed/movie/{id}", + "tv": "https://moviee.tv/embed/tv/{id}?season={season}&episode={episode}" + }, + { + "name": "VidSrc Rip", + "movie": "https://vidsrc.rip/embed/movie/{id}", + "tv": "https://vidsrc.rip/embed/tv/{id}/{season}/{episode}" + }, + { + "name": "AutoEmbed", + "movie": "https://player.autoembed.cc/embed/movie/{id}", + "tv": "https://player.autoembed.cc/embed/tv/{id}/{season}/{episode}" + }, + { + "name": "SuperEmbed", + "movie": "https://multiembed.mov/?video_id={id}&tmdb=1", + "tv": "https://multiembed.mov/?video_id={id}&tmdb=1&s={season}&e={episode}" + }, + { + "name": "SuperEmbed VIP", + "movie": "https://multiembed.mov/directstream.php?video_id={id}&tmdb=1", + "tv": "https://multiembed.mov/directstream.php?video_id={id}&tmdb=1&s={season}&e={episode}" + }, + { + "name": "2Embed cc", + "movie": "https://www.2embed.cc/embed/{id}", + "tv": "https://www.2embed.cc/embedtv/{id}&s={season}&e={episode}" + }, + { + "name": "2Embed Skin", + "movie": "https://www.2embed.skin/embed/{id}", + "tv": "https://www.2embed.skin/embedtv/{id}&s={season}&e={episode}" + }, + { + "name": "VidLink Pro", + "movie": "https://vidlink.pro/movie/{id}", + "tv": "https://vidlink.pro/tv/{id}/{season}/{episode}" + }, + { + "name": "VidSrc XYZ", + "movie": "https://vidsrc.xyz/embed/movie/{id}", + "tv": "https://vidsrc.xyz/embed/tv/{id}/{season}-{episode}" + }, + { + "name": "VidSrc To", + "movie": "https://vidsrc.to/embed/movie/{id}", + "tv": "https://vidsrc.to/embed/tv/{id}/{season}/{episode}" + }, + { + "name": "VidSrc Dev", + "movie": "https://vidsrc.dev/embed/movie/{id}", + "tv": "https://vidsrc.dev/embed/tv/{id}/{season}/{episode}" + }, + { + "name": "Smashy Stream", + "movie": "https://player.smashy.stream/movie/{id}", + "tv": "https://player.smashy.stream/tv/{id}?s={season}&e={episode}" + }, + { + "name": "Any Embed", + "movie": "https://anyembed.xyz/movie/{id}", + "tv": "https://anyembed.xyz/tv/{id}/{season}/{episode}" + }, + { + "name": "MoviesApi Club", + "movie": "https://moviesapi.club/movie/{id}", + "tv": "https://moviesapi.club/tv/{id}-{season}-{episode}" + } +] \ No newline at end of file diff --git a/static/js/account.js b/static/js/account.js new file mode 100644 index 0000000..6de3ac5 --- /dev/null +++ b/static/js/account.js @@ -0,0 +1,27 @@ +window.onload = function() { + console.log("account.js loaded"); + + loadGlobal(); + + const sections = document.getElementsByClassName("account-properties-container"); + const buttons = document.getElementsByClassName("side-button-empty"); + + const options = { + root: null, + threshold: 0.3 + }; + + const callback = (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + iterate(buttons, (btn)=>{ + btn.classList.remove("side-button"); + }) + document.getElementById(entry.target.id+"-btn").classList.add("side-button"); + } + }); + }; + + const observer = new IntersectionObserver(callback, options); + iterate(sections, (container) => observer.observe(container)); +}; \ No newline at end of file diff --git a/static/js/customForm.js b/static/js/customForm.js new file mode 100644 index 0000000..a62e08e --- /dev/null +++ b/static/js/customForm.js @@ -0,0 +1,128 @@ +window.onload = function() { + console.log("customForm.js loaded"); + + // Checkboxes + const sumbitBtns = document.getElementsByClassName("submit-btn"); + + iterate(sumbitBtns, (btn)=>{ + btn.addEventListener("click", (e)=>{ + e.preventDefault(); + }) + }); +}; + +function iterate(list, callback) +{ + for (let i=0; i { + if (response.status == 429) + { + alert("To many request's, please try again later."); + } + else if (!response.ok) { + return response.json().then(errorData => { + doOutcome(errorData, redirect); + }); + } + + return response.json(); + }) + .then(data => { + doOutcome(data, redirect); + }) + .catch((error) => { + doOutcome(error, redirect); + }); +} + +function register() +{ + const username = document.getElementById("username").value; + const passwordValue = document.getElementById("password").value; + const reenterPasswordValue = document.getElementById("reenter_password").value; + + const redirect = "/login"; + + if (passwordValue != reenterPasswordValue) + { + alert("Password and Reenter Password must match"); + return; + } + + const formData = { + username: username, + password: passwordValue, + reenter_password: reenterPasswordValue + }; + + fetch("register_submit", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(formData) + }) + .then(response => { + if (response.status == 429) + { + alert("To many request's, please try again later."); + } + else if (!response.ok) { + return response.json().then(errorData => { + doOutcome(errorData, redirect); + }); + } + + return response.json(); + }) + .then(data => { + doOutcome(data, redirect); + }) + .catch((error) => { + doOutcome(error, redirect); + }); +} + +function doOutcome(data, redirect) +{ + if (data == null) {return; } + if (data.success && redirect != null) + { + window.location.href = redirect; + } + else + { + alertError(data); + } +} + +function alertError(error) +{ + if (error && error.message != null) + { + alert(error.message); + } +} \ No newline at end of file diff --git a/static/js/film.js b/static/js/film.js new file mode 100644 index 0000000..0aca8ef --- /dev/null +++ b/static/js/film.js @@ -0,0 +1,125 @@ +let watchingTrailer = false; +let addedToWatchHistory = false; + +window.onload = function() { + console.log("film.js loaded"); + + loadGlobal(); + + // Seasons drropdown + const DropdownBtn = document.getElementsByClassName("dropdown-button")[0]; + const DropdownList = document.getElementsByClassName("dropdown-list-container")[0]; + addDropdown(DropdownBtn, DropdownList); + + //const mediaIFrameOverlay = document.getElementById("media-iframe-overlay"); +} + +function mediaFrameClicked() +{ + // Add to watch history + addedToWatchHistory = true; + + iframeOverlay = document.getElementById("media-iframe-overlay"); + iframeOverlay.style.pointerEvents = 'none'; + + const url = window.location.pathname; + const urlPaths = url.split("/"); + console.log(urlPaths); + // /film/tv/48866/1/1 + + let mediaType = urlPaths[2]; + let tmdbId = urlPaths[3]; + let season = null, episode = null; + + if (mediaType.toLowerCase() == "tv") + { + season = urlPaths[4]; + episode = urlPaths[5]; + } + + //alert(`Media Frame Clicked.\n TMDB Id: ${tmdbId}, Media Type: ${mediaType}, Season: ${season}, Episode: ${episode}`) + + const formData = { + tmdb_id: tmdbId, + media_type: mediaType, + season: season, + episode: episode + }; + + fetch(`/add_to_watch_history`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(formData) + }); +} + +async function setTrailer(mediaType, id) +{ + const response = await fetch(`/trailer/${mediaType.toLowerCase()}?id=${id}`); + + if (response.ok) + { + const data = await response.json(); + + setMediaIFrame(data.embed); + watchingTrailer = true; + //createHtmlElement(``) + } +} + +function toggleFavorite(id, media) +{ + const formData = { + tmdb_id: id, + media_type: media + }; + + fetch(`/toggle_favorite`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(formData) + }).then(response => { + if (response.status == 429) + { + alert("To many request's, please try again later."); + } + else if (!response.ok) { + return response.json().then(errorData => { + + }); + } + + return response.json(); + }) + .then(data => { + if (data.success) + { + const favoriteBtn = document.getElementById("favorite"); + const favoriteBtnText = document.querySelector("#favorite span"); + if (data.favorited) + { + favoriteBtn.classList.add("favorited"); + favoriteBtnText.textContent = "Favorited" + } + else + { + favoriteBtn.classList.remove("favorited"); + favoriteBtnText.textContent = "Favorite" + } + } + }) + .catch((error) => { + + }); +} + +function setMediaIFrame(embed) +{ + const mediaIFrame = document.getElementById("media-iframe"); + mediaIFrame.src = embed; + watchingTrailer = false; +} \ No newline at end of file diff --git a/static/js/global.js b/static/js/global.js new file mode 100644 index 0000000..25d967f --- /dev/null +++ b/static/js/global.js @@ -0,0 +1,355 @@ +let hideOnClickList = []; +let blacklist = []; + +let autoLoadGlobal = false; + +document.addEventListener("DOMContentLoaded", () => { + document.body.addEventListener("click", (e) => { + //console.log(e.target) + if (e.target.id == "media-iframe-overlay") + { + mediaFrameClicked(); + } + hideOnClickList.forEach((element)=>{ + if (e.target != element && !isBlacklisted(e.target) && element != null) + { + element.classList.add("hide"); + element.classList.remove("show"); + } + }) + }); +}); + +window.onload = function() { + if (autoLoadGlobal) + { + loadGlobal(); + } +}; + +function dynamicScrollBarLoading(loadEvent) +{ + window.addEventListener('scroll', () => { + if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && !isLoading) { + loadEvent(); + } + }); + + loadEvent(); + + // Keep loading more until scrolling is possible + // because if client cant scroll then they cant load anything more. + async function call_until(stop, passes) + { + if (stop || passes > 5) { return; } + + setTimeout(()=>{ + loadEvent().then((result) => { + //console.log(result) + if (result) + { + return call_until(isScrollingPossible(), passes+1); + } + }) + .catch(console.error) + }, 1000) + } + call_until(isScrollingPossible(), 0); +} + +function isScrollingPossible() { + return document.documentElement.scrollHeight > window.innerHeight; +} + +function loadGlobal() +{ + console.log("global.js loaded"); + + // Search bar + const searchbar = document.getElementById("searchbar"); + const searchbarBtn = document.getElementById("searchbar-btn"); + + const params = new URLSearchParams(window.location.search);; + const query = params.get("query"); + + if (query) + { + searchbar.value = query; + } + + searchbar.addEventListener('keydown', (e)=>{ + if (e.code == "Enter") + { + search(searchbar.value); + } + }); + searchbarBtn.addEventListener('click', ()=>{ + search(searchbar.value); + }); + + // Scrolling + const mediaList = document.getElementsByClassName("scroll-list"); + iterate(mediaList, (listContainer)=>{ + addScrollList(listContainer); + }) +} + +function search(query) { + // search_media?media_type=multi&page=1&query=the%20100&include_adult=true + if (query) + { + redirect(`/search?media_type=all&page=1&query=${encodeURIComponent(query)}&include_adult=false`); + } +} + +function addScrollList(listContainer) +{ + + let isDown = false; + let startX; + let scrollLeft; + + listContainer.addEventListener("mousedown", (e)=>{ + isDown = true; + startX = e.pageX - listContainer.offsetLeft; + scrollLeft = listContainer.scrollLeft; + }) + listContainer.addEventListener('mouseleave', () => { + isDown = false; + }); + listContainer.addEventListener('mouseup', () => { + isDown = false; + }); + listContainer.addEventListener('mousemove', (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - listContainer.offsetLeft; + const walk = (x - startX) * 2; + listContainer.scrollLeft = scrollLeft - walk; + }); + listContainer.addEventListener('wheel', (e) => { + e.preventDefault(); + listContainer.scrollLeft += e.deltaY; + }); +} + +function isBlacklisted(element) +{ + let isBlacklistedBool = false; + blacklist.forEach((b)=>{ + if (b == element) + { + isBlacklistedBool = true; + } + }) + return isBlacklistedBool; +} + +function iterate(list, callback) +{ + for (let i=0; i{ + //console.log("toggle"); + list.classList.toggle("hide"); + }) +} + +function hasReachedPageLimit(data) +{ + if (data && data.status == 200) + { + return false; + } + else if (data && data.status == 404) + { + // Reached page limit + return true; + } +} + +function createFilmCard(film, url) +{ + /* +
+
+
+

Dune: Part Two

+
+

2024

+

8.5/10

+

2h 46m

+

PG-13

+

Movie

+
+
+
+ +
+ */ + + if (film == null) { return; } + + //console.log(film); + + const title = film.name; + const year = film.release_date.split("-")[0]; + const rating = parseFloat(film.vote_average.toFixed(1));; + const mediaType = film.media_type === "movie" ? "Movie" : film.media_type === "tv" ? "TV" : "Loading"; + let img = film.poster_path; + + //console.log(img); + + if (img == null) + { + img = "/static/media/movie_poster_not_found.jpg" + } + + //const html = `

${title}

${year}

${rating}

${time}

${ageRating}

${mediaType}

` + const html = `

${title}

${year}

${rating}

${mediaType}

` + + const card = createHtmlElement(html); + + function onClick() + { + redirect(url) + } + + if (isMobile()) { + card.addEventListener('dblclick', onClick); + } else { + card.addEventListener('click', onClick); + } + + return card; +} + +function isMobile() { + return /Mobi|Android/i.test(navigator.userAgent); +} + +function redirect(url) { window.location.href = url; } + +function createCategory(title, mediaType, listType, timeWindow) +{ + /* +
+

Featured today

+
+ */ + + /* + media_type + list_type + time_window + */ + + const timeHtml = timeWindow != null ? `&time_window=${timeWindow}` : "" + + const html = `
${title}
` + + return createHtmlElement(html); +} + +function createHtmlElement(htmlString) +{ + const container = document.createElement('div'); + container.innerHTML = htmlString; + return container.firstChild; +} + +function addScrollWithRequest(apiString, mediaGrid) +{ + let page = 1; + let reachedPageLimit = false; + + async function scroll() + { + if (isLoading || reachedPageLimit) { return; }; + isLoading = true; + try { + /* + /get_category?page=5&media_type=tv&list_type=top_rated&time_window=null + get_category + + page + media_type + list_type + time_window + */ + const response = await fetch(`${apiString}&page=${page}`); //`/get_category?page=${page}&media_type=${mediaType}&list_type=${listType}&time_window=${timeWindow}`); + if (reachedPageLimit) { return; } + reachedPageLimit = hasReachedPageLimit(response); + if (reachedPageLimit) { return; } + + const data = await response.json(); + console.log(data) + if (!data.data.has_more_pages) { reachedPageLimit = true; } + if (data.data.results.length <= 0) + { + reachedPageLimit = true; + return false; + } + + // Category + //let cardsList = ""; + data.data.results.forEach((item)=>{ + const card = createFilmCard(item, getFilmUrl(item)); + mediaGrid.appendChild(card); + }) + + //const container = document.createElement('div'); + //container.innerHTML = cardsList; +// + //Array.from(container.children).forEach(child => { + // mediaGrid.appendChild(child); + //}); + + page++; + } catch (error) { + console.error('Failed to load data:', error); + return false; + } finally { + isLoading = false; + return true; + } + } + dynamicScrollBarLoading(scroll); +} + +function getFilmUrl(film) +{ + let season = null; + let episode = null; + + const media_type = film.media_type + let url = `/film/${media_type}/${film.id}` + //console.log(item) + if (film.media_type == "tv") + { + season = film.current_season + episode = film.current_episode + } + + if (media_type == "tv") + { + url += `/${season}/${episode}` + } + return url; +} \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..52dbeb9 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,62 @@ +let isLoading = false; + +window.onload = function() { + console.log("index.js loaded"); + + loadGlobal(); + + let page = 1; + let reachedPageLimit = false; + + const categoryContainer = document.getElementById("categories") + + dynamicScrollBarLoading(async ()=>{ + if (isLoading || reachedPageLimit) { return; }; + isLoading = true; + + try { + const response = await fetch(`get_home_page_categories?page=${page}`); + + //if (!response.ok) throw new Error('Network response was not ok'); + + reachedPageLimit = hasReachedPageLimit(response); + + if (reachedPageLimit) { return; } + + const data = await response.json(); + console.log(data) + data.data.forEach(item => { + // Category + //cardsList = ""; + + + category = createCategory(item.title, item.media_type, item.list_type, item.time_window); + + //console.log(category); + //categoryContainer.insertAdjacentElement("beforeend", category) + + //const container = document.createElement('div'); +// + //container.innerHTML = category; + + categoryContainer.appendChild(category); + + const categoryMediaList = document.getElementById(`scroll-${item.title}`); + + item.film_list.results.forEach((film)=>{ + categoryMediaList.appendChild(createFilmCard(film, getFilmUrl(film))); + }) + + addScrollList(document.getElementById(`scroll-${item.title}`)); + }); + + page++; + } catch (error) { + console.error('Failed to load data:', error); + return false; + } finally { + isLoading = false; + return true; + } + }); +}; diff --git a/static/js/search.js b/static/js/search.js new file mode 100644 index 0000000..754267b --- /dev/null +++ b/static/js/search.js @@ -0,0 +1,89 @@ +let isLoading = false; + +window.onload = function() { + console.log("selectedFilms.js loaded"); + + loadGlobal(); + + const mediaGrid = document.getElementById("media-grid"); + + + // media_type + // list_type + // time_window + const params = new URLSearchParams(window.location.search); + const mediaType = params.get("media_type"); + const query = params.get("query"); + const includeAdult = params.get("include_adult") || true; + + // search_media?media_type=multi&page=1&query=the%20100&include_adult=true + const apiString = `/search_media?media_type=${mediaType}&query=${query}&include_adult=${includeAdult}`; + + addScrollWithRequest(apiString, mediaGrid); + + /* + let page = 1; + let reachedPageLimit = false; + + const mediaGrid = document.getElementById("media-grid"); + + + // media_type + // list_type + // time_window + + const params = new URLSearchParams(window.location.search); + + const mediaType = params.get("media_type"); + const query = params.get("query"); + const includeAdult = params.get("include_adult") || true; + + async function scroll() + { + if (isLoading || reachedPageLimit) { return; }; + isLoading = true; + try { + // search_media?media_type=multi&page=1&query=the%20100&include_adult=true + const response = await fetch(`/search_media?page=${page}&media_type=${mediaType}&query=${query}&include_adult=${includeAdult}`); + + reachedPageLimit = hasReachedPageLimit(response); + + if (reachedPageLimit) { return; } + + const data = await response.json(); + + if (data.data.results.length <= 0) + { + reachedPageLimit = true; + return false; + } + + // Category + let cardsList = ""; + data.data.results.forEach((item)=>{ + cardsList += createFilmCard(item); + }) + + //category = createCategory(item.title, cardsList, item.media_type, item.list_type); + + const container = document.createElement('div'); + container.innerHTML = cardsList; + + //mediaGrid.appendChild(container.children); + + Array.from(container.children).forEach(child => { + mediaGrid.appendChild(child); + }); + + page++; + } catch (error) { + console.error('Failed to load data:', error); + return false; + } finally { + isLoading = false; + return true; + } + } + dynamicScrollBarLoading(scroll); + */ +}; diff --git a/static/js/selectedFilms.js b/static/js/selectedFilms.js new file mode 100644 index 0000000..2afb347 --- /dev/null +++ b/static/js/selectedFilms.js @@ -0,0 +1,78 @@ +let isLoading = false; + +window.onload = function() { + console.log("selectedFilms.js loaded"); + + loadGlobal(); + + let page = 1; + let reachedPageLimit = false; + + const mediaGrid = document.getElementById("media-grid"); + + /* + media_type + list_type + time_window + */ + const params = new URLSearchParams(window.location.search); + + const mediaType = params.get("media_type"); + const listType = params.get("list_type"); + const timeWindow = params.get("time_window"); + + const apiString = `/get_category?media_type=${mediaType}&list_type=${listType}&time_window=${timeWindow}`; + + addScrollWithRequest(apiString, mediaGrid); + /* + async function scroll() + { + if (isLoading || reachedPageLimit) { return; }; + isLoading = true; + try { + + // /get_category?page=5&media_type=tv&list_type=top_rated&time_window=null + // get_category +// + // page + // media_type + // list_type + // time_window + + const response = await fetch(`/get_category?page=${page}&media_type=${mediaType}&list_type=${listType}&time_window=${timeWindow}`); + + reachedPageLimit = hasReachedPageLimit(response); + + if (reachedPageLimit) { return; } + + const data = await response.json(); + + // Category + let cardsList = ""; + data.data.results.forEach((item)=>{ + cardsList += createFilmCard(item); + }) + + //category = createCategory(item.title, cardsList, item.media_type, item.list_type); + + const container = document.createElement('div'); + container.innerHTML = cardsList; + + //mediaGrid.appendChild(container.children); + + Array.from(container.children).forEach(child => { + mediaGrid.appendChild(child); + }); + + page++; + } catch (error) { + console.error('Failed to load data:', error); + return false; + } finally { + isLoading = false; + return true; + } + } + dynamicScrollBarLoading(scroll); + */ +}; diff --git a/static/media/down_arrow.svg b/static/media/down_arrow.svg new file mode 100644 index 0000000..0e47bfb --- /dev/null +++ b/static/media/down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/media/eye.svg b/static/media/eye.svg new file mode 100644 index 0000000..a72a9b5 --- /dev/null +++ b/static/media/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/media/filmlabslogo.svg b/static/media/filmlabslogo.svg new file mode 100644 index 0000000..2ee1041 --- /dev/null +++ b/static/media/filmlabslogo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/media/heart.svg b/static/media/heart.svg new file mode 100644 index 0000000..a572ea8 --- /dev/null +++ b/static/media/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/media/magnifying.svg b/static/media/magnifying.svg new file mode 100644 index 0000000..5f18a0e --- /dev/null +++ b/static/media/magnifying.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/media/media_view.jpg b/static/media/media_view.jpg new file mode 100644 index 0000000..5a50ae1 Binary files /dev/null and b/static/media/media_view.jpg differ diff --git a/static/media/movie_poster_not_found.jpg b/static/media/movie_poster_not_found.jpg new file mode 100644 index 0000000..93f1eeb Binary files /dev/null and b/static/media/movie_poster_not_found.jpg differ diff --git a/static/media/star.png b/static/media/star.png new file mode 100644 index 0000000..98458cc Binary files /dev/null and b/static/media/star.png differ diff --git a/static/styles/style.css b/static/styles/style.css new file mode 100644 index 0000000..0bd0a30 --- /dev/null +++ b/static/styles/style.css @@ -0,0 +1,865 @@ +/* + _______ + + Globals + _______ +*/ +:root { + --primary-color: #1e2434; + --secondary-color: #2d3344; + --hover-color: #3b4255; + --smoke-color: #838383; + --gold-color: #FFB834; + --white-color: #F5F5F7; + --red-color: #FF5757; +} + +* { + font-family: 'Segoe UI'; + color: var(--white-color); + margin: 0 auto; +} +html { + scroll-behavior: smooth; +} +body { + background-color: var(--primary-color); + overflow-x: hidden; + overflow-y: auto; +} + +input,button,a { + border: none; + outline: none; + text-decoration: none; +} +a { + transition: color 0.15s ease; +} +a:hover { + color: var(--smoke-color); +} +.styled-a { + color: var(--gold-color); +} +h1 { + font-weight: 1000; +} +h5 { + font-weight: 1000; + color: var(--smoke-color); +} +.h3 { + font-weight: 1000; + font-size: 1.5em; +} + +hr { + border-color: var(--smoke-color); + width: 100%; +} +header { + position: fixed; + width: 100%; + height: 120px; + z-index: 10; + + background-color: var(--primary-color); +} +aside { + position: fixed; + left: 0; + width: 30%; + padding: 10px; + top: 150px; +} +main { + position: relative; + top: 150px; +} +footer { + position: relative; + bottom: 0; + top: 200px; + + background-color: var(--secondary-color); + padding: 30px; + + display: flex; + flex-direction: row; + gap: 100px; +} + +.unselectable { + -webkit-user-select: none; /* For Safari and older Chrome */ + -moz-user-select: none; /* For older Firefox */ + -ms-user-select: none; /* For Internet Explorer and Edge */ + user-select: none; /* Standard */ +} +.unclickable { + pointer-events: none; +} +.center-vertically { + position: absolute; + top: 50%; + transform: translateY(-50%); +} +.center-horizontally { + position: absolute; + left: 50%; + transform: translateX(-50%); +} +.center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +.gold { + color: var(--gold-color) +} +.show { + display: block !important; +} +.hide { + display: none !important; +} + +/* + ______________ + + Top Navigation + ______________ +*/ +.header-container { + width: 100%; + height: 100%; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} +/* logo */ +.logo { + display: flex; + gap: 20px; + float: left; + text-decoration: none; +} +.logo:hover{ + cursor: pointer; +} +.logo img { + width: 40; + height: auto; + pointer-events: none; +} +/* searchbar */ +.searchbar { + position: relative; + height: 30px; + width: 50%; +} +.searchbar button { + position: absolute; + width: 35px; + height: 25px; + left: 5px; + background-color: transparent; + cursor: pointer; +} +.searchbar button img { + width: 15px; + height: auto; +} +.searchbar input { + background-color: var(--secondary-color); + text-indent: 40px; + width: 100%; + height: 100%; + border-radius: 13px; + font-weight: 400; +} +.searchbar input::placeholder { + color: var(--smoke-color); +} +/* account */ +.account-btn { + background-color: var(--secondary-color); + cursor: pointer; + padding: 15px 50px 15px 50px; + border-radius: 100px; + + display: flex; + + align-items: center; + justify-content: center; +} +/* account login button */ +.login-btn { + display: block; + background-color: var(--red-color); + width: 150px; + height: 30px; + border-radius: 13px; + font-weight: 700; + transition: background-color 0.3s ease, color 0.3s ease; + text-align: center; +} +.login-btn span { + display: inline; + width: 100%; + height: 100%; + vertical-align: middle; +} +.login-btn:hover { + cursor: pointer; + background-color: var(--gold-color); + color: var(--primary-color); +} +.login-btn p { + width: 100%; + height: 100%; +} +/* account */ +.account { + position: relative; + display: inline-block; +} +/* account popout box */ +.account-popout-box { + position: absolute; + background-color: var(--secondary-color); + + margin-top: 15px; + width: 100%; + border-radius: 15px; + z-index: 2; +} +.account-popout-box a,hr { + width: 100%; + text-align: left; +} +/* account popout box list */ +.account-popout-box-list { + padding: 15px; + + display: flex; + flex-direction: column; + + gap: 15px; +} + +/* + ____ + + Main + ____ +*/ +main { + position: relative; + display: flex; + flex-direction: column; +} +/* category */ +.category, .films-grid { + position: relative; + width: 90vw; + overflow-x: auto; + margin-top: 20px; + + left: 0; + float: left; +} +/* categories */ +.categories { + width: 90vw; +} +/* media list */ +.media-list { + display: flex; + flex-direction: row; + gap: 10px; + overflow-x: auto; + width: 100%; + margin-top: 10px; +} +.media-list::-webkit-scrollbar { + display: none; +} +/* media modal */ +.media-modal { + position: relative; + width: 200px; + height: 300px; + overflow: hidden; + flex: 0 0 auto; +} +.media-modal img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +/* media info */ +.media-info { + position: absolute; + width: 100%; + height: 100%; + + background: #00000073; + + text-align: center; + + /* animation */ + transform: translateY(100%); + opacity: 0; + transition: transform 0.2s ease, opacity 1.0s ease; +} +.media-modal:hover .media-info { + cursor: pointer; + transform: translateY(0%); + opacity: 1; +} +.media-info div { + position: absolute; + width: 100%; + bottom: 25px; + + display: flex; + flex-direction: column; +} +.media-info div div { + position: relative; + width: 90%; + top: 8px; + + font-size: 0.5em; + + display: flex; + flex-direction: row; +} + +/* + ___________________ + + Account Login Modal + ___________________ +*/ +.login-main { + width: 100%; +} +/* modal */ +.account-login-modal { + background-color: var(--secondary-color); + padding: 50px; + border-radius: 13px; + width: 50%; +} +/* top text */ +.account-modal-top-text { + text-align: center; +} +/* account form */ +.account-login-form, .account-input-container { + display: flex; + flex-direction: column; + width: 100%; + border: none; + padding: 0; +} +.account-login-form label { + width: 100%; + margin-top: 20px; +} +.account-input-container input { + width: 100%; + margin-top: 10px; + background-color: var(--primary-color); + border-radius: 13px; + height: 40px; + text-indent: 10px; +} +/* checkbox */ +.checkbox-container { + display: flex; + align-items: center; + gap: 20px; +} +.checkbox-container span { + width: 100%; +} +.checkbox { + display: none; +} +.checkbox + .checkbox-box { + width: 20px; + height: 20px; + border: 4px solid var(--primary-color); + border-radius: 5px; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + + display: flex; + justify-content: center; +} +.checkbox:checked + .checkbox-box { + background-color: var(--gold-color); + border-color: var(--gold-color); +} +.checkbox:checked + .checkbox-box::after { + content: '✔'; + width: 10px; + height: 10px; + +} +/* account button */ +.submit-btn { + width: 30%; + height: 60px; + background-color: var(--red-color); + font-size: 1.3em; + font-weight: 700; + border-radius: 100px; + cursor: pointer; + margin-top: 30px; + + transition: background-color .15s ease; +} +.submit-btn:hover { + background-color: #ff6f6f; +} +/* bottom text */ +.account-login-form-bottom { + text-align: center; + margin-top: 20px; +} + +/* + _______ + + Account + _______ +*/ +/* side list */ +.side-list { + width: 70%; + display: flex; + flex-direction: column; + text-align: left; + text-indent: 30px; +} +.side-list a { + width: 100%; + margin-bottom: 20px; + margin-top: 10px; +} +/* side button */ +.side-button { + height: 60px; + background-color: var(--secondary-color); + border-radius: 21px; + + display: flex; + align-items: center; +} +.side-button span { + width: 100%; +} +/* account main tag */ +.account-main { + width: 60%; + float: right; + right: 5%; +} +/* account properties container */ +.account-properties-container { + width: 100%; + +} +.account-properties-container span { + color: var(--smoke-color); +} +/* account properties container table */ +.account-property-container { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto auto; + justify-content: start; + gap: 25%; + height: 200px; +} +/* account property */ +.account-property { + margin-top: 10px; +} + +/* + ______________ + + Selected films + ______________ +*/ +/* film grid*/ +.films-grid { + +} +/* media grid */ +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +/* + ____ + + Film + ____ +*/ +.film-main { + position: relative; + width: 90vw; + + margin-bottom: 100px; +} +/* film viewer */ +.film-viewer { + position: relative; + width: 100%; + padding-top: 56.25%; /* This is 16:9 aspect ratio */ + overflow: hidden; +} +.film-viewer img { + width: 100%; + height: auto; +} +.film-viewer iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} +.film-viewer div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} +/* film top */ +.film-top { + width: 100%; + height: 50px; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; +} +/* imdb */ +.rating-img { + width: 25px; + height: auto; +} +/* rating */ +.rating { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} +.film-end { + display: flex; + flex-direction: row; + gap: 30px; +} +/* button */ +.button { + padding: 10px 40px 10px 40px; + + display: flex; + flex-direction: row; + align-items: center; + border-radius: 100px; + + background-color: var(--secondary-color); + color: var(--smoke-color); + + font-weight: 800; + gap: 15px; + + cursor: pointer; + + transition: background-color 0.15s ease; +} +.button img { + width: 20px; + height: auto; +} +.button:hover { + background-color: var(--hover-color); +} +/* favorited */ +.favorited { + background-color: var(--red-color); + color: var(--white-color); +} +.favorited img { + filter: brightness(800%); +} +.favorited:hover{ + background-color: #ff7777; +} +/* film info tags */ +.film-info-tags { + margin-top: 10px; + font-weight: 100; +} +.film-info-tags span { + margin-right: 30px; +} +/* genre chips */ +.genre-chips { + margin-top: 30px; + + display: inline-flex; + gap: 8px; + + overflow-x: auto; + white-space: nowrap; + + -ms-overflow-style: none; + scrollbar-width: none; + + max-width: 100%; +} +.genre-chips::-webkit-scrollbar { + display: none; /* Hides scrollbar in Chrome/Safari */ +} +/* clear chips */ +.clear-chip { + border-color: var(--secondary-color); + border-radius: 13px; + border-width: 2px; + border-style: solid; + + padding: 7px 15px 7px 15px; + flex: 0 0 auto; + + white-space: nowrap; + + transition: background-color 0.15s ease; +} +.clear-chip:hover { + background-color: var(--hover-color); +} +/* film description */ +.film-description { + margin-top: 15px; + font-weight: 100; +} +/* see more */ +.see-more { + display: inline-flex; + align-items: center; + gap: 15px; + margin-top: 10px; +} +.see-more .rating-img { + vertical-align: middle; +} +/* film episodes */ +.film-episodes { + margin-top: 20px; +} +/* dropdown */ +.dropdown { + position: relative; + display: inline-block; + width: 250px; + + z-index: 2; +} +/* dropdown button */ +.dropdown-button { + position: relative; + + background-color: var(--secondary-color); + + border-radius: 13px; + padding: 10px 15px 10px 15px; + width: 100%; + text-align: start; + + cursor: pointer; + z-index: 3; + + text-indent: 6px; + + font-size: 1.3em; + + display: flex; + align-items: center; + justify-content: space-between; +} +.dropdown-button span { + margin-right: auto; + text-align: start; + width: 100%; +} +.dropdown-button img { + width: 20px; + height: 20px; +} +/* dropdown list */ +.dropdown-list-container { + position: absolute; + + display: flex; + flex-direction: column; + + margin-top: -20px; + width: 100%; + + border-radius: 13px; + + background-color: var(--secondary-color); +} +.dropdown-button-list button { + width: 100%; + cursor: pointer; + + background-color: transparent; + + text-align: left; + + transition: background-color 0.15s ease; + + height: 30px; + border-radius: 7px; +} +.dropdown-button-list button:hover { + background-color: var(--hover-color); +} +.dropdown-button-active { + background-color: var(--primary-color) !important; +} +.dropdown-button-list { + padding: 15px; + margin-top: 10px; + width: 90%; +} +/* chips grid */ +.chips-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 10px; + margin-top: 10px; +} +/* chip buttons */ +.button-chip { + background-color: var(--secondary-color); + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + + cursor: pointer; + + width: 100%; + + border-radius: 13px; + + padding: 15px 50px; +} +.button-chip span { + font-size: 1.35em; +} +.button-chip p { + color: var(--smoke-color); + font-weight: 100; +} +.button-chip img { + width: 25px; + height: auto; +} +/* chip button selected */ +.selected-button-chip { + background-color: var(--gold-color); +} +.selected-button-chip p { + color: var(--white-color); + font-weight: 800; +} +.selected-button-chip img { + filter: brightness(800%); +} +/* service provider container */ +.service-provider-container { + margin-top: 40px; +} + +/* + __________ + + Animations + __________ +*/ +.loading { + background: #eee; + background: linear-gradient(110deg, var(--secondary-color) 8%, var(--hover-color) 18%, var(--secondary-color) 33%); + background-size: 200% 100%; + animation: 1.5s shine linear infinite; +} +@keyframes shine { + to { + background-position-x: -200%; + } +} + +/* + ____________ + + Screen Sizes + ____________ +*/ +@media (max-width: 768px), (max-height: 550px) { + header { + height: 100px; + } + .header-container { + flex-direction: column; + padding: 5px; + width: 80%; + } + .logo img { + width: 30px; + } + .searchbar { + width: 100%; + } + .account { + width: 100%; + margin-top: 10px; + } + .login-btn { + width: 100%; + } + .submit-btn { + width: 100%; + } + .account-popout-box { + width: 75%; + } + .submit-btn { + height: 30px; + } + .login-main { + top: 90%; + } +} \ No newline at end of file diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..b04109c --- /dev/null +++ b/templates/account.html @@ -0,0 +1,27 @@ + +
+ +
\ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..d76bfc0 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,86 @@ + + + + + + + {% if title %} + FilmLabs | {{ title }} + {% else %} + FilmLabs + {% endif %} + + + + + + {% if javascript %} + + {% endif %} + + + {% if not no_header %} +
+
+ + {% if not no_header_additional %} + + + {% endif %} +
+
+ {% endif %} + + {% if template %} + {% include template %} + {% endif %} + + {% if not no_footer %} +
+

+ FilmLabs is your ultimate destination for a vast world of movies and TV shows, + all completely ad-free. Dive into a premium streaming experience that offers high-quality entertainment across genres, + thoughtfully curated and available anytime, anywhere—all in one convenient platform. +

+

+ FilmLabs does not host or store any content on our servers. Instead, + we provide access to an extensive collection of movies and shows through trusted third-party streaming services, + ensuring a seamless and responsible viewing experience. +

+
+ {% endif %} + + \ No newline at end of file diff --git a/templates/embed.html b/templates/embed.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/film.html b/templates/film.html new file mode 100644 index 0000000..a6b5b9f --- /dev/null +++ b/templates/film.html @@ -0,0 +1,184 @@ +
+
+
+ + {% if service_url %} + +
+ {% else %} + + {% endif %} +
+
+

{{ title or "Title" }}

+
+
+ Rating + {{ rating or "Rating" }}/10 +
+ + + + {% if is_favorited %} + + {% else %} + + {% endif %} + + +
+
+
+ {{ year or "Year" }} + + + {{ media_type or "Media Type" }} +
+ {% if genres %} +
+ {% for genre in genres %} + + genre.name + + {% endfor %} +
+ {% endif %} +
+

+ {{ overview or "Description" }} +

+

+ See more at + + TMDB + +

+
+
+

Service Providers

+ {% if service_providers %} +
+ {% for service in service_providers %} + {% if service.name == selected_service %} + + {% else %} + + + {% endif %} + {% endfor %} +
+ {% endif %} +
+ {% if media_type == "tv" %} +
+ + {% if episodes %} +
+ {% for episode in episodes %} + {% if episode.episode_number == current_episode %} + + {% else %} + + {% endif %} + + {% endfor %} +
+ {% endif %} +
+ {% endif %} + +
\ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..38dfa83 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..12725b3 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,32 @@ +
+ +
\ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..c50710a --- /dev/null +++ b/templates/register.html @@ -0,0 +1,29 @@ +
+ +
\ No newline at end of file diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..d184670 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,10 @@ +
+
+ +

Search results for {{ query }}

+ +
+ +
+
+
\ No newline at end of file diff --git a/templates/selected.html b/templates/selected.html new file mode 100644 index 0000000..7f39871 --- /dev/null +++ b/templates/selected.html @@ -0,0 +1,14 @@ +
+
+ + {% if title %} +

{{ title }}

+ {% else %} +

Category could not be found

+ {% endif %} + +
+ +
+
+
\ No newline at end of file diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..3b4fbc1 --- /dev/null +++ b/testing.py @@ -0,0 +1,37 @@ +from server.packages import db +from server.packages.db import FilmLabsDB +from os import environ + +db = FilmLabsDB( + 15,5,5,15 +) +db.connect( + "localhost", + environ.get("MYSQL_USER"), + environ.get("MYSQL_PASSWORD"), + environ.get("FILMLABS_DB") +) + +admin_account = db.create_account("sugriva", "fortnite", "fortnite") +#login = db.login("Monnapse", "Password") + +#film = db.check_film( +# 6, +# "tv", +# "poster test tv 6", +# "200", +# 99.99, +# "poster.com" +#) +#film = db.get_film(5) +#db.add_favorite(6, 1) +#db.remove_favorite(6, 1) +#favorites = db.get_favorites(1) +#history = db.get_watch_history(1) +#movie_history = db.get_episodes_history(1) +#print(db.get_film(history[1].tmdb_id).media_type) +#db.add_episode_history(1, 1, "00:00:00") +#db.add_movie_history(2, "00:00:00") +#toggle = db.toggle_favorite(693134,1) +#print(history) +print(admin_account.account_message) \ No newline at end of file