Skip to content

Commit

Permalink
Concurrency and show fixes (#52)
Browse files Browse the repository at this point in the history
* Add concurrency, fix shows

* Run prettier on default_settings.json

* Better logging, added item_ids and extended info endpoint for shows

---------

Co-authored-by: Gaisberg <None>
  • Loading branch information
Gaisberg authored Dec 18, 2023
1 parent 315f310 commit 8d54ed5
Show file tree
Hide file tree
Showing 19 changed files with 671 additions and 581 deletions.
408 changes: 204 additions & 204 deletions backend/.gitignore

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions backend/controllers/items.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from fastapi import APIRouter, HTTPException, Request
from program.media import MediaItemState
from utils.logger import logger


router = APIRouter(
Expand All @@ -26,14 +25,14 @@ async def get_items(request: Request):
}


@router.get("/{state}")
async def get_item(request: Request, state: str):
state = MediaItemState[state]
items = request.app.program.media_items.get_items_with_state(state).items

@router.get("/extended/{item_id}")
async def get_extended_item_info(request: Request, item_id: str):
item = request.app.program.media_items.get_item_by_id(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return {
"success": True,
"items": [item.to_dict() for item in items],
"item": item.to_extended_dict(), # Assuming this method exists
}


Expand Down
19 changes: 6 additions & 13 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from program.program import Program
from utils.thread import ThreadRunner
from program import Program
from controllers.settings import router as settings_router
from controllers.items import router as items_router
from controllers.default import router as default_router


sys.path.append(os.getcwd())
program = Program()
runner = ThreadRunner(program.run, 5)


def lifespan(app: FastAPI):
runner.start()
app.program.start()
yield
runner.stop()
app.program.stop()


app = FastAPI(lifespan=lifespan)
app.program = Program()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Expand All @@ -30,15 +29,9 @@ def lifespan(app: FastAPI):
allow_headers=["*"],
)

app.program = program

app.include_router(default_router)
app.include_router(settings_router)
app.include_router(items_router)

if __name__ == "__main__":
try:
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=False)
except KeyboardInterrupt:
print("Exiting...")
sys.exit(0)
uvicorn.run(app, host="0.0.0.0", port=8080, reload=False)
79 changes: 79 additions & 0 deletions backend/program/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Program main module"""
import os
from typing import Optional
from pydantic import BaseModel, HttpUrl, Field
from program.symlink import Symlinker
from utils.logger import logger, get_data_path
from utils.settings import settings_manager
from program.media import MediaItemContainer
from program.libraries.plex import Library as Plex
from program.debrid.realdebrid import Debrid as RealDebrid
from program.content import Content
from program.scrapers import Scraping


# Pydantic models for configuration
class PlexConfig(BaseModel):
user: str
token: str
address: HttpUrl
watchlist: Optional[HttpUrl] = None


class MdblistConfig(BaseModel):
lists: list[str] = Field(default_factory=list)
api_key: str
update_interval: int = 80


class OverseerrConfig(BaseModel):
url: HttpUrl
api_key: str


class RealDebridConfig(BaseModel):
api_key: str


class TorrentioConfig(BaseModel):
filter: str


class Settings(BaseModel):
version: str
debug: bool
service_mode: bool
log: bool
menu_on_startup: bool
plex: PlexConfig
mdblist: MdblistConfig
overseerr: OverseerrConfig
scraper_torrentio: TorrentioConfig
realdebrid: RealDebridConfig


class Program:
"""Program class"""

def __init__(self):
self.settings = settings_manager.get_all()
self.media_items = MediaItemContainer(items=[])
self.data_path = get_data_path()
self.media_items.load(os.path.join(self.data_path, "media.pkl"))
self.threads = [
Content(self.media_items), # Content must be first
Plex(self.media_items),
RealDebrid(self.media_items),
Symlinker(self.media_items),
Scraping(self.media_items),
]
logger.info("Iceberg initialized")

def start(self):
for thread in self.threads:
thread.start()

def stop(self):
for thread in self.threads:
thread.stop()
self.media_items.save(os.path.join(self.data_path, "media.pkl"))
45 changes: 45 additions & 0 deletions backend/program/content/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import threading
import time

from utils.logger import logger
from .mdblist import Mdblist
from .overseerr import Overseerr
from .plex_watchlist import PlexWatchlist


class Content(threading.Thread):
def __init__(self, media_items):
super().__init__(name="Content")
self.services = [
Mdblist(media_items),
Overseerr(media_items),
PlexWatchlist(media_items),
]
self.valid = False
self.running = False
while not self.validate():
logger.error(
"You have no content services enabled, please enable at least one!"
)
time.sleep(5)

for service in self.services:
if service.initialized:
service.run()

def validate(self):
return any(service.initialized for service in self.services)

def run(self) -> None:
while self.running:
for service in self.services:
if service.initialized:
service.run()

def start(self) -> None:
self.running = True
super().start()

def stop(self) -> None:
self.running = False
super().join()
16 changes: 6 additions & 10 deletions backend/program/content/mdblist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
from program.updaters.trakt import Updater as Trakt


class Content:
class Mdblist:
"""Content class for mdblist"""

def __init__(
self,
):
def __init__(self, media_items: MediaItemContainer):
self.initialized = False
self.media_items = media_items
self.settings = settings_manager.get("mdblist")
if not self._validate_settings():
logger.info("mdblist is not configured and will not be used.")
Expand All @@ -29,26 +28,23 @@ def _validate_settings(self):
)
return not "Invalid API key!" in response.text

def update_items(self, media_items: MediaItemContainer):
def run(self):
"""Fetch media from mdblist and add them to media_items attribute
if they are not already there"""
try:
with self.rate_limiter:
logger.debug("Getting items...")

items = []
for list_id in self.settings["lists"]:
if list_id:
items += self._get_items_from_list(
list_id, self.settings["api_key"]
)

new_items = [item for item in items if item not in media_items]
new_items = [item for item in items if item not in self.media_items]
container = self.updater.create_items(new_items)
added_items = media_items.extend(container)
added_items = self.media_items.extend(container)
if len(added_items) > 0:
logger.info("Added %s items", len(added_items))
logger.debug("Done!")
except RateLimitExceeded:
pass

Expand Down
15 changes: 6 additions & 9 deletions backend/program/content/overseerr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
from program.updaters.trakt import Updater as Trakt


class Content:
class Overseerr:
"""Content class for overseerr"""

def __init__(
self,
):
def __init__(self, media_items: MediaItemContainer):
self.initialized = False
self.media_items = media_items
self.settings = settings_manager.get("overseerr")
if self.settings.get("api_key") == "" or not self._validate_settings():
logger.info("Overseerr is not configured and will not be used.")
Expand All @@ -33,17 +32,15 @@ def _validate_settings(self):
except ConnectTimeout:
return False

def update_items(self, media_items: MediaItemContainer):
def run(self):
"""Fetch media from overseerr and add them to media_items attribute
if they are not already there"""
logger.debug("Getting items...")
items = self._get_items_from_overseerr(10000)
new_items = [item for item in items if item not in media_items]
new_items = [item for item in items if item not in self.media_items]
container = self.updater.create_items(new_items)
added_items = media_items.extend(container)
added_items = self.media_items.extend(container)
if len(added_items) > 0:
logger.info("Added %s items", len(added_items))
logger.debug("Done!")

def _get_items_from_overseerr(self, amount: int):
"""Fetch media from overseerr"""
Expand Down
13 changes: 6 additions & 7 deletions backend/program/content/plex_watchlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import json


class Content:
class PlexWatchlist:
"""Class for managing Plex watchlist"""

def __init__(self):
def __init__(self, media_items: MediaItemContainer):
self.initialized = False
self.media_items = media_items
self.watchlist_url = settings.get("plex")["watchlist"]
if not self.watchlist_url or not self._validate_settings():
logger.info(
Expand All @@ -32,17 +33,15 @@ def _validate_settings(self):
except ConnectTimeout:
return False

def update_items(self, media_items: MediaItemContainer):
def run(self):
"""Fetch media from Plex watchlist and add them to media_items attribute
if they are not already there"""
logger.debug("Getting items...")
items = self._get_items_from_plex_watchlist()
new_items = [item for item in items if item not in media_items]
new_items = [item for item in items if item not in self.media_items]
container = self.updater.create_items(new_items)
added_items = media_items.extend(container)
added_items = self.media_items.extend(container)
if len(added_items) > 0:
logger.info("Added %s items", len(added_items))
logger.debug("Done!")

def _get_items_from_plex_watchlist(self) -> list:
"""Fetch media from Plex watchlist"""
Expand Down
29 changes: 24 additions & 5 deletions backend/program/debrid/realdebrid.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Realdebrid module"""
import os
import re
import threading
import time

import requests
from requests import ConnectTimeout
from utils.logger import logger
Expand All @@ -24,14 +24,19 @@ def get_user():
return response.json()


class Debrid: # TODO CHECK TORRENTS LIST BEFORE DOWNLOAD, IF DOWNLOADED AND NOT IN LIBRARY CHOOSE ANOTHER TORRENT
class Debrid(
threading.Thread
): # TODO CHECK TORRENTS LIST BEFORE DOWNLOAD, IF DOWNLOADED AND NOT IN LIBRARY CHOOSE ANOTHER TORRENT
"""Real-Debrid API Wrapper"""

def __init__(self):
def __init__(self, media_items: MediaItemContainer):
super().__init__(name="Debrid")
# Realdebrid class library is a necessity
while True:
self.settings = settings_manager.get("realdebrid")
self.media_items = media_items
self.auth_headers = {"Authorization": f'Bearer {self.settings["api_key"]}'}
self.running = False
if self._validate_settings():
self._torrents = {}
break
Expand All @@ -48,23 +53,37 @@ def _validate_settings(self):
except ConnectTimeout:
return False

def download(self, media_items: MediaItemContainer):
def run(self):
while self.running:
self.download()

def start(self) -> None:
self.running = True
super().start()

def stop(self) -> None:
self.running = False
super().join()

def download(self):
"""Download given media items from real-debrid.com"""
added_files = 0

items = []
for item in media_items:
for item in self.media_items:
if item.state is not MediaItemState.LIBRARY:
if item.type == "movie" and item.state is MediaItemState.SCRAPE:
items.append(item)
if item.type == "show":
item._lock.acquire()
for season in item.seasons:
if season.state is MediaItemState.SCRAPE:
items.append(season)
else:
for episode in season.episodes:
if episode.state is MediaItemState.SCRAPE:
items.append(episode)
item._lock.release()

for item in items:
added_files += self._download(item)
Expand Down
Loading

0 comments on commit 8d54ed5

Please sign in to comment.