Skip to content

Commit

Permalink
Merge pull request #63 from nandyalu/dev
Browse files Browse the repository at this point in the history
Release v0.2.2-beta
  • Loading branch information
nandyalu authored Oct 21, 2024
2 parents f80f713 + 7aa5e49 commit ab55f6e
Show file tree
Hide file tree
Showing 36 changed files with 1,430 additions and 1,027 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ="America/New_York" \
APP_NAME="Trailarr" \
APP_VERSION="0.0.4-beta"
APP_VERSION="0.2.1-beta"

# Set the python path
ENV PYTHONPATH "${PYTHONPATH}:/app/backend"
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ alembic==1.13.2
apscheduler==3.10.4
async-lru==2.0.4
fastapi[standard]==0.115.0
bcrypt==4.2.0
pillow==10.4.0
sqlmodel==0.0.22
yt-dlp==2024.8.6
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Thank you for creating them.

Contributions are welcome! Please see the [Contributing](https://github.com/nandyalu/trailarr/blob/main/.github/CONTRIBUTING.md) guide for more information.

Looking for a frontend developer (Angular) to help with the UI, if you are interested, please reach out in the [Discussions](https://github.com/nandyalu/trailarr/discussions) or [Reddit](https://www.reddit.com/r/trailarr/).

## License

This project is licensed under the terms of the GPL v3 license. See [GPL-3.0 license](https://github.com/nandyalu/trailarr?tab=GPL-3.0-1-ov-file) for more details.
Expand Down
64 changes: 59 additions & 5 deletions backend/api/v1/authentication.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import secrets
from typing import Annotated
import bcrypt
from fastapi import Cookie, Depends, HTTPException, status
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBasic, HTTPBasicCredentials

Expand All @@ -9,19 +10,55 @@
browser_security = HTTPBasic()


# Hash a password using bcrypt
def get_password_hash(password: str) -> bytes:
"""Converts the password to bytes and hashes it using bcrypt \n
Args:
password (str): The password to hash \n
Returns:
bytes: The hashed password as bytes"""
pwd_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password=pwd_bytes, salt=salt)
return hashed_password


def set_password(new_password: str) -> str:
"""Sets the new password for the webui \n
Args:
new_password (str): The new password to set \n
Returns:
str: The result message"""
app_settings.webui_password = get_password_hash(new_password).decode("utf-8")
return "Password updated successfully"


# Check if the provided password matches the stored password (hashed)
def verify_password(plain_password: str) -> bool:
"""Checks if the provided password matches the stored password (hashed) \n
Args:
plain_password (str): The password to check \n
Returns:
bool: True if the password matches, False otherwise"""
password_byte_enc = plain_password.encode("utf-8")
hashed_password = app_settings.webui_password.encode("utf-8")
return bcrypt.checkpw(password=password_byte_enc, hashed_password=hashed_password)


def validate_login(
credentials: Annotated[HTTPBasicCredentials, Depends(browser_security)],
):
"""Validates the login credentials provided by the user \n
Args:
credentials (HTTPBasicCredentials): The login credentials \n
Raises:
HTTPException: If the username or password is incorrect"""
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"admin"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"trailarr"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
is_correct_password = verify_password(credentials.password)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -37,6 +74,11 @@ def validate_login(


def verify_api_key(api_key: str) -> bool:
"""Verifies the API key provided by the user \n
Args:
api_key (str): The API key to verify \n
Returns:
bool: True if the API key is valid, False otherwise"""
return api_key == app_settings.api_key


Expand All @@ -46,6 +88,11 @@ def verify_api_key(api_key: str) -> bool:
def validate_api_key_cookie(
trailarr_api_key: Annotated[str | None, Cookie()] = None,
) -> bool:
"""Validates the API key provided in the cookie \n
Args:
trailarr_api_key (Annotated[str | None, Cookie]): The API key provided in the cookie \n
Raises:
HTTPException: If the API key is missing or invalid"""
# Check if the API key is provide and valid
if trailarr_api_key and verify_api_key(trailarr_api_key):
return True
Expand All @@ -59,6 +106,13 @@ def validate_api_key(
header_api_key: str | None = Depends(header_scheme),
trailarr_api_key: Annotated[str | None, Cookie()] = None,
) -> bool:
"""Validates the API key provided in the query, header or cookie \n
Args:
query_api_key (str | None): The API key provided in the query \n
header_api_key (str | None): The API key provided in the header \n
trailarr_api_key (Annotated[str | None, Cookie]): The API key provided in the cookie \n
Raises:
HTTPException: If the API key is missing or invalid"""
_api_key = ""
# Check if the API key is provided in query
if query_api_key:
Expand Down
5 changes: 5 additions & 0 deletions backend/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ class Settings(BaseModel):
class UpdateSetting(BaseModel):
key: str
value: int | str | bool


class UpdatePassword(BaseModel):
current_password: str
new_password: str
14 changes: 13 additions & 1 deletion backend/api/v1/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter

from api.v1.models import Settings, UpdateSetting
from api.v1.models import Settings, UpdatePassword, UpdateSetting
from api.v1 import authentication
from config.settings import app_settings
from core.base.database.manager.general import GeneralDatabaseManager, ServerStats

Expand Down Expand Up @@ -32,3 +33,14 @@ async def update_setting(update: UpdateSetting) -> str:
_new_value = getattr(app_settings, update.key, None)
_name = update.key.replace("_", " ").title()
return f"Setting {_name} updated to {_new_value}"


@settings_router.put("/updatepassword")
async def update_password(passwords: UpdatePassword) -> str:
if not passwords.current_password:
return "Error updating password: Current password is required!"
if not passwords.new_password:
return "Error updating password: Password cannot be empty!"
if not authentication.verify_password(passwords.current_password):
return "Error updating password: Current password is incorrect!"
return authentication.set_password(passwords.new_password)
19 changes: 19 additions & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def __new__(cls) -> "_Config":
_DEFAULT_LANGUAGE = "en"
_DEFAULT_DB_URL = f"sqlite:///{APP_DATA_DIR}/trailarr.db"
_DEFAULT_FILE_NAME = "{title} - Trailer-trailer.{ext}"
# Default WebUI password 'trailarr' hashed
_DEFAULT_WEBUI_PASSWORD = (
"$2b$12$CU7h.sOkBp5RFRJIYEwXU.1LCUTD2pWE4p5nsW3k1iC9oZEGVWeum"
)

_VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
_VALID_AUDIO_FORMATS = ["aac", "ac3", "eac3", "flac", "opus"]
Expand Down Expand Up @@ -118,6 +122,7 @@ def __init__(self):
"True",
).lower() in ["true", "1"]
self.trailer_file_name = os.getenv("TRAILER_FILE_NAME", self._DEFAULT_FILE_NAME)
self.webui_password = os.getenv("WEBUI_PASSWORD", self._DEFAULT_WEBUI_PASSWORD)
self.yt_cookies_path = os.getenv("YT_COOKIES_PATH", "")

def as_dict(self):
Expand Down Expand Up @@ -445,6 +450,20 @@ def trailer_web_optimized(self, value: bool):
self._trailer_web_optimized = value
self._save_to_env("TRAILER_WEB_OPTIMIZED", self._trailer_web_optimized)

@property
def webui_password(self):
"""Password for the WebUI (hashed and stored). \n
Default is 'trailarr'. \n
Valid values are any hashed string of password."""
return self._webui_password

@webui_password.setter
def webui_password(self, value: str):
if not value:
value = self._DEFAULT_WEBUI_PASSWORD
self._webui_password = value
self._save_to_env("WEBUI_PASSWORD", value)

@property
def yt_cookies_path(self):
"""Path to the YouTube cookies file. \n
Expand Down
10 changes: 8 additions & 2 deletions backend/core/base/database/manager/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ async def create(
"""
# Validate the connection details, will raise an error if invalid
status = await validate_connection(connection)
# Convert path mappings to database objects
# Calling Connection.model_validate(connection) will raise an error \
# with the current implementation of PathMappingCRU
# https://github.com/nandyalu/trailarr/issues/53
_path_mappings = self._convert_path_mappings(connection)
connection.path_mappings = [] # Clear path mappings from input connection
# Create db connection object from input
db_connection = Connection.model_validate(connection)
# Create db path mappings from input
db_connection.path_mappings = self._convert_path_mappings(connection)
# Add path mappings to database connection
db_connection.path_mappings = _path_mappings
# Use the session to add the connection to the database
_session.add(db_connection)
_session.commit()
Expand Down
2 changes: 1 addition & 1 deletion backend/core/files_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ async def _check_trailer_as_file(path: str) -> bool:
continue
if not entry.name.endswith((".mp4", ".mkv", ".avi", ".webm")):
continue
if "-trailer." not in entry.name:
if "trailer" not in entry.name:
continue
return True
return False
Expand Down
15 changes: 9 additions & 6 deletions backend/core/tasks/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from core.tasks.cleanup import trailer_cleanup
from core.tasks.download_trailers import download_missing_trailers
from core.tasks.image_refresh import refresh_images
from core.updates.docket_check import check_for_update
from core.updates.docker_check import check_for_update

# from core.tasks.task_runner import TaskRunner

Expand Down Expand Up @@ -91,8 +91,8 @@ def update_check_job():
func=check_for_update,
trigger="interval",
days=1,
id="update_check_job",
name="Image Update Check",
id="docker_update_check_job",
name="Docker Update Check",
next_run_time=datetime.now() + timedelta(seconds=240),
max_instances=1,
)
Expand Down Expand Up @@ -148,15 +148,18 @@ def schedule_all_tasks():
# Schedule API Refresh to run every hour
refresh_api_data_job()

# Schedule update check task to run once a day, start in 4 minutes from now
update_check_job()

# Schedule trailer download task to run every hour, start in 15 minutes from now
download_missing_trailers_job()

# Schedule Image Refresh to run every 6 hours, start in 10 minutes from now
image_refresh_job()

# Schedule trailer cleanup task to run every hour, start in 5 minutes from now
trailer_cleanup_job()

# Schedule trailer download task to run every hour, start in 15 minutes from now
download_missing_trailers_job()

logger.info("All tasks scheduled!")
return

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import requests

from app_logger import ModuleLogger
from config.settings import app_settings

logger = ModuleLogger("UpdateChecker")


def get_image_versions(image_name):
"""Gets all available versions of a Docker image from Docker Hub.\n
Expand Down Expand Up @@ -85,16 +88,18 @@ def check_for_update():
current_digest = get_current_version_digest(all_versions, current_version)

if not latest_digest or not current_digest:
print("Error: Could not update details from Docker Hub.")
logger.error("Error: Could not update details from Docker Hub.")
return

if latest_digest != current_digest:
print(
logger.info(
f"A newer version ({latest_version}) of the image is available. Please update!"
)
app_settings.update_available = True
else:
print(f"You are using the latest version ({current_version}) of the image.")
logger.info(
f"You are using the latest version ({current_version}) of the image."
)


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ alembic==1.13.2
apscheduler==3.10.4
async-lru==2.0.4
fastapi[standard]==0.115.0 # Update version in README.md as well
bcrypt==4.2.0
pillow==10.4.0
sqlmodel==0.0.22
yt-dlp==2024.8.6
Loading

0 comments on commit ab55f6e

Please sign in to comment.