Skip to content

Commit

Permalink
Merge changes from dev to main (#13)
Browse files Browse the repository at this point in the history
* FIX: Failed download return value

* FIX: Added 'TESTING' in settings to use for testing, that sets database to an in-memory db

* FIX: API calls from movie not working!

* FIX: Minor changes

* UPD: Install ffmpeg based on amd64 or arm64 architectires

* UPD: Move ffprobe and ffplay as well to /usr/local/bin/

* UPD: Allow configuring PUID and PGID from ENV Variables

* FIX: parse Arr data even if youTubeTrailerId is not received!

* UPD: v0.0.5-beta frontend build

* FIX: VS Code tasks

* UPD: Visual improvements to search results (now shows poster and year too)

* FIX: catch YT Video download error and try to download next result from search

* UPD: Frontend Visual  improvements!

* FIX: Delete media when connection is deleted!

* UPD: Add appuser with PUID and PGID from ENV Variables
  • Loading branch information
nandyalu committed Aug 7, 2024
1 parent e6dc207 commit b0605a6
Show file tree
Hide file tree
Showing 39 changed files with 289 additions and 88 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@
"python.testing.pytestEnabled": true,
"todo-tree.tree.showBadges": true,
"python.analysis.autoImportCompletions": true,
"task.allowAutomaticTasks": "off",
"task.autoDetect": "off",
"typescript.tsc.autoDetect": "off",
}
64 changes: 43 additions & 21 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,77 @@
"version": "2.0.0",
"tasks": [
{
// Build the docker image without frontend changes
"type": "docker-build",
"type": "shell",
"label": "Docker build without Frontend",
"detail": "Build the docker image without frontend changes",
"platform": "python",
"dockerBuild": {
"tag": "trailarr2:latest",
"dockerfile": "${workspaceFolder}/Dockerfile",
"context": "${workspaceFolder}",
"pull": true
}
"options": {
"cwd": "${workspaceFolder}"
},
"command": "docker image build --pull --file ./Dockerfile --tag 'trailarr2:latest' .",
"group": "build",
"icon": {
"id": "library",
"color": "terminal.ansiGreen"
},
},
{
// Build the docker image with frontend changes
"type": "docker-build",
"label": "Docker build with Frontend",
"detail": "Build the docker image with frontend changes",
"icon": {
"id": "library",
"color": "terminal.ansiBlue"
},
"dependsOrder": "sequence",
"dependsOn": [
"frontend - build",
"Frontend build",
],
"platform": "python",
"group": "build",
"dockerBuild": {
"tag": "trailarr2:latest",
"dockerfile": "${workspaceFolder}/Dockerfile",
"context": "${workspaceFolder}",
"pull": true
}
},
"runOptions": {}
},
{
// Build & Run the docker image without frontend changes
"type": "shell",
"label": "Docker build run without Frontend",
"detail": "Run the docker image without frontend changes",
"icon": {
"id": "open-preview",
"color": "terminal.ansiBlue"
},
"dependsOrder": "sequence",
"dependsOn": [
"Docker build without Frontend"
],
"command": "python ./restart_stack.py",
"problemMatcher": []
},
{
// Build & Run the docker image with frontend changes
"type": "shell",
"label": "Docker build run with Frontend",
"detail": "Run the docker image with frontend changes",
"icon": {
"id": "open-preview",
"color": "terminal.ansiGreen"
},
"dependsOn": [
"Docker build with Frontend"
],
"command": "python ./restart_stack.py",
"problemMatcher": []
},
{
// Generate PyTest HTML Report by running the tests
"label": "Generate PyTest HTML Report",
"detail": "Generate PyTest HTML Report by running the tests",
"icon": {
"id": "beaker",
"color": "terminal.ansiYellow"
},
"type": "shell",
"command": "cd backend && pytest --cov=. --cov-report=html",
"group": {
Expand Down Expand Up @@ -113,21 +130,26 @@
}
},
{
// Build the frontend using Angular CLI
"label": "frontend - build",
"label": "Frontend build",
"detail": "Build the frontend using Angular CLI",
"icon": {
"id": "preview",
"color": "terminal.ansiMagenta"
},
"type": "npm",
"script": "build",
"path": "frontend",
"group": "build",
"problemMatcher": []
},
{
"label": "fastapi: run",
"label": "Fastapi run",
"detail": "Run the FastAPI server",
"icon": {
"id": "vm-running",
"color": "terminal.ansiCyan"
},
"type": "shell",
"dependsOn": [
"frontend - build"
],
"isBackground": true,
"command": "uvicorn backend.main:trailarr_api --host 0.0.0.0 --port 7888",
"problemMatcher": []
Expand Down
22 changes: 7 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
COPY ./backend/requirements.txt .
RUN python -m pip install --disable-pip-version-check --no-cache-dir --upgrade -r requirements.txt

# Install ffmpeg
RUN apt-get update && apt-get install -y curl xz-utils

# Download and extract the appropriate ffmpeg build
RUN curl -L -o /tmp/ffmpeg.tar.xz "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz" \
&& mkdir /tmp/ffmpeg \
&& tar -xf /tmp/ffmpeg.tar.xz -C /tmp/ffmpeg --strip-components=1 \
&& mv /tmp/ffmpeg/bin/* /usr/local/bin/ \
&& rm -rf /tmp/ffmpeg.tar.xz /tmp/ffmpeg
# Install ffmpeg using install_ffmpeg.sh script
COPY install_ffmpeg.sh /tmp/install_ffmpeg.sh
RUN chmod +x /tmp/install_ffmpeg.sh && \
/tmp/install_ffmpeg.sh

# Stage 2 - Final image
FROM python:3.12-slim
Expand All @@ -28,7 +23,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ="America/New_York" \
APP_NAME="Trailarr" \
APP_VERSION="0.0.4-beta"
APP_VERSION="0.0.5-beta"

# Install tzdata, gosu and set timezone
RUN apt update && apt install -y tzdata gosu && \
Expand All @@ -49,7 +44,7 @@ COPY ./assets /app/assets
COPY ./backend /app/backend

# Copy the frontend built files
COPY ./frontend-build/browser /app/frontend-build
COPY ./frontend-build /app/frontend-build

# Copy the installed Python dependencies and ffmpeg
COPY --from=python-deps /usr/local/ /usr/local/
Expand All @@ -60,9 +55,6 @@ ENV PYTHONPATH="${PYTHONPATH}:/app/backend"
# Create a volume for data directory
VOLUME ["/data"]

# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Copy the entrypoint script and make it executable
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
Expand All @@ -75,7 +67,7 @@ RUN chmod +x /app/start.sh
EXPOSE 7889

# Set permissions for appuser on /app directory
RUN chown -R appuser:appuser /app && chmod -R 750 /app
RUN chmod -R 750 /app

# Run entrypoint script to create directories, set permissions and timezone \
# and start the application as appuser
Expand Down
1 change: 1 addition & 0 deletions backend/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SearchMedia(BaseModel):
imdb_id: str
txdb_id: str
is_movie: bool
poster_path: str | None


class Settings(BaseModel):
Expand Down
13 changes: 13 additions & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self):
# Read properties from ENV variables or set default values if not present
self.api_key = os.getenv("API_KEY", "")
self.debug = os.getenv("DEBUG", "False").lower() in ["true", "1"]
self.testing = os.getenv("TESTING", "False").lower() in ["true", "1"]
self.database_url = os.getenv("DATABASE_URL", self._DEFAULT_DB_URL)
self.monitor_enabled = os.getenv(
"MONITOR_ENABLED",
Expand Down Expand Up @@ -164,6 +165,18 @@ def debug(self, value: bool):
self._debug = value
self._save_to_env("DEBUG", self._debug)

@property
def testing(self):
"""Testing mode for the application. \n
Default is False. \n
Valid values are True/False."""
return self._testing

@testing.setter
def testing(self, value: bool):
self._testing = value
self._save_to_env("TESTING", self._testing)

@property
def database_url(self):
"""Database URL for the application. \n
Expand Down
9 changes: 9 additions & 0 deletions backend/core/base/database/manager/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
)

from core.base.database.utils.engine import manage_session
from core.radarr.models import Movie
from core.sonarr.models import Series
from exceptions import ItemNotFoundError
from core.radarr.api_manager import RadarrManager
from core.sonarr.api_manager import SonarrManager
Expand Down Expand Up @@ -177,6 +179,13 @@ def delete(
"""
connection = self._get_db_item(connection_id, _session=_session)
_session.delete(connection)
if connection.arr_type == ArrType.RADARR:
_statement = select(Movie).where(Movie.connection_id == connection_id)
else:
_statement = select(Series).where(Series.connection_id == connection_id)
media_list = _session.exec(_statement).all()
for media in media_list:
_session.delete(media)
_session.commit()
return True

Expand Down
2 changes: 1 addition & 1 deletion backend/core/base/database/utils/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# sqlite_file_name = "database.db"
sqlite_url = app_settings.database_url
if app_settings.debug:
if app_settings.testing:
# Use an in-memory SQLite database for testing
sqlite_url = "sqlite:///:memory:"
engine = create_engine(
Expand Down
14 changes: 10 additions & 4 deletions backend/core/download/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any

from yt_dlp import YoutubeDL
from yt_dlp.utils import YoutubeDLError

from app_logger import ModuleLogger
from config.settings import app_settings
Expand Down Expand Up @@ -79,7 +80,7 @@ def _get_ytdl_options() -> dict[str, Any]:
# Set generic options for youtube-dl
ydl_options = {
"compat_opts": {"no-keep-subs"},
"ffmpeg_location": "/usr/local/bin/ffmpeg", # ?figure out how to get this from os
"ffmpeg_location": "/usr/local/bin/ffmpeg",
"noplaylist": True,
# "verbose": True,
"extract_flat": "discard_in_playlist",
Expand All @@ -92,6 +93,8 @@ def _get_ytdl_options() -> dict[str, Any]:
"restrictfilenames": True,
"noprogress": True,
"no_warnings": True,
"ignore_no_formats_error": True,
"ignoreerrors": True,
"quiet": True,
"merge_output_format": app_settings.trailer_file_format,
"postprocessors": [],
Expand Down Expand Up @@ -182,6 +185,9 @@ def download_video(url: str, file_path: str | None = None) -> str:
try:
with YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
except Exception:
logger.exception(f"Failed to download video from {url}")
return str(data["filepath"])
if data and "filepath" in data:
return str(data["filepath"])
except (YoutubeDLError, Exception):
pass
logger.exception(f"Failed to download video from {url}")
return ""
8 changes: 6 additions & 2 deletions backend/core/radarr/data_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ class RadarrDataParser(BaseModel):
arr_id: int = Field(validation_alias="id")
title: str = Field()
year: int = Field()
language: str = Field(validation_alias=AliasPath("originalLanguage", "name"))
language: str = Field(
validation_alias=AliasPath("originalLanguage", "name"), default="en"
)
overview: str | None = Field(default=None)
runtime: int = Field(default=0)
youtube_trailer_id: str | None = Field(validation_alias="youTubeTrailerId")
youtube_trailer_id: str | None = Field(
validation_alias="youTubeTrailerId", default=None
)
folder_path: str | None = Field(validation_alias="path", default="")
imdb_id: str | None = Field(validation_alias="imdbId", default="")
txdb_id: str = Field(validation_alias="tmdbId")
Expand Down
4 changes: 3 additions & 1 deletion backend/core/sonarr/data_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ class SonarrDataParser(BaseModel):
arr_id: int = Field(validation_alias="id")
title: str = Field()
year: int = Field()
language: str = Field(validation_alias=AliasPath("originalLanguage", "name"))
language: str = Field(
validation_alias=AliasPath("originalLanguage", "name"), default="en"
)
overview: str | None = Field(default=None)
runtime: int = Field(default=0)
# Sonarr does not have youtbetrailerid
Expand Down
2 changes: 1 addition & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async def serve_frontend(rest_of_path: str = ""):


# Check if the frontend directory exists, if not create it
static_dir = os.path.abspath("/app/frontend-build")
static_dir = os.path.abspath("/app/frontend-build/browser")
if not os.path.exists(static_dir):
logging.info("Creating static directory")
os.makedirs(static_dir)
Expand Down
4 changes: 2 additions & 2 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

# TODO! Update all tests to current codebase
def pytest_configure():
os.environ["DEBUG"] = "True"
os.environ["TESTING"] = "True"
from core.base.database.utils.init_db import init_db

init_db()


@pytest.fixture(autouse=True)
def debug_database():
os.environ["DEBUG"] = "True"
os.environ["TESTING"] = "True"


TEST_AIOHTTP_APIKEY = "API_KEY"
Expand Down
29 changes: 23 additions & 6 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh

# THIS SCRIPT WILL BE RUN AS THE ROOT USER IN THE CONTAINER BEFOR APP STARTS
# THIS SCRIPT WILL BE RUN AS THE ROOT USER IN THE CONTAINER BEFORE APP STARTS

# Set TimeZone based on env variable
# Print date time before
Expand All @@ -16,13 +16,30 @@ echo "Creating '/data' folder for storing database and other config files"
mkdir -p /data/logs && chmod -R 755 /data
chmod -R 755 /app/assets

# Change the owner of /data to the non-root user
echo "Changing the owner of data directory to the appuser"
chown -R appuser:appuser /data
# Set default values for PUID and PGID if not provided
PUID=${PUID:-1000}
PGID=${PGID:-1000}
APPUSER=${APPUSER:-appuser}

# Create the appuser group and user
echo "Creating user and group '$APPUSER' with UID '$PUID' and GID '$PGID'"
if ! getent group "$APPUSER" > /dev/null 2>&1; then
groupadd -g "$PGID" "$APPUSER"
fi

if ! id -u "$APPUSER" > /dev/null 2>&1; then
useradd -u "$PUID" -g "$APPUSER" -m "$APPUSER"
fi

# Set permissions for appuser on /app and /data directories
echo "Changing the owner of app and data directories to '$APPUSER'"
chmod -R 750 /app
chown -R "$APPUSER":"$APPUSER" /app
chown -R "$APPUSER":"$APPUSER" /data

# Switch to the non-root user and execute the command
echo "Switching to appuser and starting the application"
exec gosu appuser bash -c /app/start.sh
echo "Switching to user '$APPUSER' and starting the application"
exec gosu "$APPUSER" bash -c /app/start.sh

# DO NOT ADD ANY OTHER COMMANDS HERE! THEY WON'T BE EXECUTED!
# Instead add them in the start.sh script
Binary file removed frontend-build/browser/assets/logo-full-512.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b0605a6

Please sign in to comment.