Skip to content

Commit

Permalink
Tidy (#337)
Browse files Browse the repository at this point in the history
* chore: work on docker improvements

* chore: update docker. minor validation fixes.

* chore: update makefile

---------

Co-authored-by: Spoked <Spoked@localhost>
  • Loading branch information
dreulavelle and Spoked authored Jun 4, 2024
1 parent 5ca02a4 commit 8233f8f
Show file tree
Hide file tree
Showing 18 changed files with 407 additions and 272 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ htmlcov/
coverage.xml
.coverage*
*.svg
frontend/node_modules/

.vscode/
.ruff_cache/
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ makefile
.ruff_cache/
*.dat
profile.svg
*.gz
*.zip

# Python bytecode / Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
95 changes: 69 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,53 +1,96 @@
# Builder Image for Python Dependencies
FROM python:3.11-alpine AS builder
RUN apk add --no-cache build-base curl
RUN pip install --upgrade pip && pip install poetry==1.4.2

ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app

COPY pyproject.toml poetry.lock ./
RUN touch README.md
RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR

# Frontend Builder
FROM node:20-alpine AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN npm install
RUN npm install -g pnpm && pnpm install
COPY frontend/ .
RUN npm run build && npm prune --production
RUN pnpm run build && pnpm prune --prod

# Final Image
FROM node:20-alpine
FROM python:3.11-alpine
LABEL name="Iceberg" \
description="Iceberg Debrid Downloader" \
url="https://github.com/dreulavelle/iceberg"

# Install system dependencies
RUN apk --update add --no-cache python3 curl bash shadow && \
rm -rf /var/cache/apk/*
# Install system dependencies and Node.js
ENV PYTHONUNBUFFERED=1
RUN apk add --no-cache \
curl \
fish \
shadow \
nodejs \
npm \
rclone \
fontconfig \
unzip && \
npm install -g pnpm

# Install Poetry globally
ENV POETRY_HOME="/etc/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN curl -sSL https://install.python-poetry.org | python3 - --yes
# Install Nerd Fonts
RUN mkdir -p /usr/share/fonts/nerd-fonts && \
curl -fLo "/usr/share/fonts/nerd-fonts/FiraCode.zip" \
https://github.com/ryanoasis/nerd-fonts/releases/download/v2.1.0/FiraCode.zip && \
unzip /usr/share/fonts/nerd-fonts/FiraCode.zip -d /usr/share/fonts/nerd-fonts && \
rm /usr/share/fonts/nerd-fonts/FiraCode.zip && \
fc-cache -fv

# Setup the application directory
WORKDIR /iceberg
# Install Poetry
RUN pip install poetry==1.4.2

# Create user and group
RUN addgroup -g 1000 iceberg && \
adduser -u 1000 -G iceberg -h /home/iceberg -s /usr/bin/fish -D iceberg

# Create fish config directory
RUN mkdir -p /home/iceberg/.config/fish

# Expose ports
EXPOSE 3000 8080
EXPOSE 3000 8080 5572

# Set environment variable to force color output
ENV FORCE_COLOR=1
ENV TERM=xterm-256color

# Copy frontend build from the previous stage
COPY --from=frontend --chown=node:node /app/build /iceberg/frontend/build
COPY --from=frontend --chown=node:node /app/node_modules /iceberg/frontend/node_modules
COPY --from=frontend --chown=node:node /app/package.json /iceberg/frontend/package.json

# Copy the Python project files
COPY pyproject.toml poetry.lock* /iceberg/
# Set working directory
WORKDIR /iceberg

# Install Python dependencies
RUN poetry config virtualenvs.create false && \
poetry install --no-dev
# Copy the virtual environment from the builder stage
COPY --from=builder /app/.venv /app/.venv
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# Copy backend code and other necessary files
# Copy the rest of the application code
COPY backend/ /iceberg/backend
COPY pyproject.toml poetry.lock /iceberg/backend/
COPY VERSION entrypoint.sh /iceberg/

# Copy frontend build from the previous stage
COPY --from=frontend --chown=iceberg:iceberg /app/build /iceberg/frontend/build
COPY --from=frontend --chown=iceberg:iceberg /app/node_modules /iceberg/frontend/node_modules
COPY --from=frontend --chown=iceberg:iceberg /app/package.json /iceberg/frontend/package.json

# Ensure entrypoint script is executable
RUN chmod +x ./entrypoint.sh
RUN chmod +x /iceberg/entrypoint.sh

# Set correct permissions for the iceberg user
RUN chown -R iceberg:iceberg /home/iceberg/.config /iceberg

# Switch to fish shell
SHELL ["fish", "--login"]

ENTRYPOINT ["/bin/sh", "-c", "/iceberg/entrypoint.sh"]
ENTRYPOINT ["fish", "/iceberg/entrypoint.sh"]
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.2
0.6.3
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def run_in_thread(self):
app.program.start()
app.program.run()
app.program.stop()
except AttributeError as e:
logger.error(f"Program failed to initialize: {e}")
except KeyboardInterrupt:
app.program.stop()
sys.exit(0)
Expand Down
85 changes: 52 additions & 33 deletions backend/program/content/overseerr.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Overseerr content module"""

from typing import Union
from program.indexers.trakt import get_imdbid_from_tmdb
from program.media.item import MediaItem
from program.settings.manager import settings_manager
from utils.logger import logger
from utils.request import delete, get, ping, post
from requests.exceptions import RetryError, ConnectionError
from urllib3.exceptions import MaxRetryError
from urllib3.exceptions import NewConnectionError


class Overseerr:
Expand Down Expand Up @@ -39,8 +43,11 @@ def validate(self) -> bool:
)
return False
return response.ok
except Exception:
logger.error("Overseerr url is not reachable.")
except (ConnectionError, RetryError, MaxRetryError, NewConnectionError) as e:
logger.error("Overseerr URL is not reachable. Please check your network connection and URL settings.")
return False
except Exception as e:
logger.error("Unexpected error during Overseerr validation. Please check the logs for more details.")
return False

def run(self):
Expand All @@ -50,9 +57,13 @@ def run(self):
self.settings.url + f"/api/v1/request?take={10000}&filter=approved",
additional_headers=self.headers,
)
except ConnectionError as e:
logger.error("Failed to fetch requests from overseerr: %s", str(e))
except (ConnectionError, RetryError, MaxRetryError) as e:
logger.error(f"Failed to fetch requests from overseerr: {str(e)}")
return
except Exception as e:
logger.error(f"Unexpected error during fetching requests: {str(e)}")
return

if not response.is_ok or response.data.pageInfo.results == 0:
return

Expand All @@ -63,15 +74,20 @@ def run(self):
if item.status == 2 and item.media.status == 3
]
for item in pending_items:
mediaId: int = int(item.media.id)
if not item.media.imdbId:
imdb_id = self.get_imdb_id(item.media)
else:
imdb_id = item.media.imdbId
if not imdb_id or imdb_id in self.recurring_items:
try:
mediaId: int = int(item.media.id)
if not item.media.imdbId:
imdb_id = self.get_imdb_id(item.media)
else:
imdb_id = item.media.imdbId
if not imdb_id or imdb_id in self.recurring_items:
continue
self.recurring_items.add(imdb_id)
yield MediaItem({"imdb_id": imdb_id, "requested_by": self.key, "overseerr_id": mediaId})
except Exception as e:
logger.error(f"Error processing item {item}: {str(e)}")

continue
self.recurring_items.add(imdb_id)
yield MediaItem({"imdb_id": imdb_id, "requested_by": self.key, "overseerr_id": mediaId})

def get_imdb_id(self, data) -> str:
"""Get imdbId for item from overseerr"""
Expand All @@ -81,16 +97,21 @@ def get_imdb_id(self, data) -> str:
else:
external_id = data.tmdbId

response = get(
self.settings.url + f"/api/v1/{data.mediaType}/{external_id}?language=en",
additional_headers=self.headers,
)
try:
response = get(
self.settings.url + f"/api/v1/{data.mediaType}/{external_id}?language=en",
additional_headers=self.headers,
)
except (ConnectionError, RetryError, MaxRetryError) as e:
logger.error(f"Failed to fetch media details from overseerr: {str(e)}")
return
except Exception as e:
logger.error(f"Unexpected error during fetching media details: {str(e)}")
return

if not response.is_ok or not hasattr(response.data, "externalIds"):
return

title = getattr(response.data, "title", None) or getattr(
response.data, "originalName"
)
imdb_id = getattr(response.data.externalIds, "imdbId", None)
if imdb_id:
return imdb_id
Expand All @@ -100,12 +121,14 @@ def get_imdb_id(self, data) -> str:
for id_attr, fetcher in alternate_ids:
external_id_value = getattr(response.data.externalIds, id_attr, None)
if external_id_value:
new_imdb_id = fetcher(external_id_value)
if new_imdb_id:
logger.debug(
f"Found imdbId for {title} from {id_attr}: {external_id_value}"
)
try:
new_imdb_id: Union[str, None] = fetcher(external_id_value)
if not new_imdb_id:
continue
return new_imdb_id
except Exception as e:
logger.error(f"Error fetching alternate ID: {str(e)}")
continue
return

@staticmethod
Expand All @@ -121,9 +144,8 @@ def delete_request(mediaId: int) -> bool:
logger.info(f"Deleted request {mediaId} from overseerr")
return response.is_ok
except Exception as e:
logger.error("Failed to delete request from overseerr ")
logger.error(e)
return False
logger.error(f"Failed to delete request from overseerr: {str(e)}")
return False

@staticmethod
def mark_processing(mediaId: int) -> bool:
Expand All @@ -139,8 +161,7 @@ def mark_processing(mediaId: int) -> bool:
logger.info(f"Marked media {mediaId} as processing in overseerr")
return response.is_ok
except Exception as e:
logger.error("Failed to mark media as processing in overseerr with id %s", mediaId)
logger.error(e)
logger.error(f"Failed to mark media as processing in overseerr with id {mediaId}: {str(e)}")
return False

@staticmethod
Expand All @@ -157,8 +178,7 @@ def mark_partially_available(mediaId: int) -> bool:
logger.info(f"Marked media {mediaId} as partially available in overseerr")
return response.is_ok
except Exception as e:
logger.error("Failed to mark media as partially available in overseerr with id %s", mediaId)
logger.error(e)
logger.error(f"Failed to mark media as partially available in overseerr with id {mediaId}: {str(e)}")
return False

@staticmethod
Expand All @@ -175,8 +195,7 @@ def mark_completed(mediaId: int) -> bool:
logger.info(f"Marked media {mediaId} as completed in overseerr")
return response.is_ok
except Exception as e:
logger.error("Failed to mark media as completed in overseerr with id %s", mediaId)
logger.error(e)
logger.error(f"Failed to mark media as completed in overseerr with id {mediaId}: {str(e)}")
return False


Expand Down
39 changes: 23 additions & 16 deletions backend/program/content/trakt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import time
from types import SimpleNamespace
from urllib.parse import urlencode, urlparse
from utils.request import RateLimiter, post
import regex

import regex
from requests import RequestException
from program.media.item import MediaItem, Movie, Show
from program.settings.manager import settings_manager
from utils.logger import logger
from utils.request import get
from utils.request import RateLimiter, get, post


class TraktContent:
Expand All @@ -33,23 +33,30 @@ def __init__(self):

def validate(self) -> bool:
"""Validate Trakt settings."""
if not self.settings.enabled:
logger.warning("Trakt is set to disabled.")
try:
if not self.settings.enabled:
logger.warning("Trakt is set to disabled.")
return False
if not self.settings.api_key:
logger.error("Trakt API key is not set.")
return False
response = get(f"{self.api_url}/lists/2", additional_headers=self.headers)
if not getattr(response.data, 'name', None):
logger.error("Invalid user settings received from Trakt.")
return False
return True
except ConnectionError:
logger.error("Connection error during Trakt validation.")
return False
if not self.settings.api_key:
logger.error("Trakt API key is not set.")
except TimeoutError:
logger.error("Timeout error during Trakt validation.")
return False

# Simple GET request to test Trakt API key
response = get(f"{self.api_url}/lists/2", additional_headers=self.headers)
if not response.is_ok:
logger.error(f"Error connecting to Trakt: {response.status_code}")
except RequestException as e:
logger.error(f"Request exception during Trakt validation: {str(e)}")
return False

if not getattr(response.data, 'name', None):
logger.error("Invalid user settings received from Trakt.")
except Exception as e:
logger.error(f"Exception during Trakt validation: {str(e)}")
return False
return True

def missing(self):
"""Log missing items from Trakt"""
Expand Down
Loading

0 comments on commit 8233f8f

Please sign in to comment.