├── .gitattributes ├── .github └── workflows │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── app ├── app.py ├── requirements.txt ├── static │ ├── css │ │ ├── search.css │ │ ├── status.css │ │ └── style.css │ └── images │ │ └── default_cover.jpg └── templates │ ├── base.html │ ├── search.html │ └── status.html └── docker-compose.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Multi-Architecture Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | jobs: 13 | build: 14 | name: Build and Push Multi-Architecture Image to GHCR 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v2 23 | with: 24 | platforms: linux/amd64,linux/arm64 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Set Repository Name to Lowercase 30 | run: echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 31 | 32 | - name: Log in to GitHub Container Registry 33 | uses: docker/login-action@v2 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build and Push Multi-Architecture Image 40 | uses: docker/build-push-action@v5 41 | with: 42 | context: . 43 | push: true 44 | tags: | 45 | ghcr.io/${{ env.REPO_NAME }}:latest 46 | platforms: linux/amd64,linux/arm64 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | 163 | #Mac DS_Store 164 | .DS_Store 165 | 166 | 167 | .vscode/* 168 | !.vscode/settings.json 169 | !.vscode/tasks.json 170 | !.vscode/launch.json 171 | !.vscode/extensions.json 172 | !.vscode/*.code-snippets 173 | 174 | # Local History for Visual Studio Code 175 | .history/ 176 | 177 | # Built Visual Studio Code Extensions 178 | *.vsix 179 | .vscode/settings.json 180 | .python-version 181 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.10-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the app directory contents into the container 8 | COPY /app /app 9 | 10 | # Install any necessary dependencies 11 | RUN pip install --no-cache-dir -r /app/requirements.txt 12 | 13 | # Expose the port the app runs on 14 | EXPOSE 5078 15 | 16 | # Define the command to run the application 17 | CMD ["python", "app.py"] 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | source app/.env/bin/activate && python3.10 app/app.py 3 | 4 | venv: 5 | python3.10 -m venv app/.env 6 | 7 | requirements: 8 | source app/.env/bin/activate && python3.10 -m pip install -r app/requirements.txt 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # AudiobookBay Automated 3 | 4 | AudiobookBay Automated is a lightweight web application designed to simplify audiobook management. It allows users to search [**AudioBook Bay**](https://audiobookbay.lu/) for audiobooks and send magnet links directly to a designated **Deludge, qBittorrent or Transmission** client. 5 | 6 | ## How It Works 7 | - **Search Results**: Users search for audiobooks. The app grabs results from AudioBook Bay and displays results with the **title** and **cover image**, along with two action links: 8 | 1. **More Details**: Opens the audiobook's page on AudioBook Bay for additional information. 9 | 2. **Download to Server**: Sends the audiobook to your configured torrent client for downloading. 10 | 11 | - **Magnet Link Generation**: When a user selects "Download to Server," the app generates a magnet link from the infohash displayed on AudioBook Bay and sends it to the torrent client. Along with the magnet link, the app assigns: 12 | - A **category label** for organizational purposes. 13 | - A **save location** for downloaded files. 14 | 15 | 16 | > **Note**: This app does not download or move any material itself (including torrent files). It only searches AudioBook Bay and facilitates magnet link generation for torrent. 17 | 18 | 19 | ## Features 20 | - **Search Audiobook Bay**: Easily search for audiobooks by title or keywords. 21 | - **View Details**: Displays book titles and covers with quickly links to the full details on AudioBook Bay. 22 | - **Basic Download Status Page**: Monitor the download status of items in your torrent client that share the specified category assigned. 23 | - **No AudioBook Bay Account Needed**: The app automatically generates magnet links from the displayed infohashes and push them to your torrent client for downloading. 24 | - **Automatic Folder Organization**: Once the download is complete, torrent will automatically move the downloaded audiobook files to your save location. Audiobooks are organized into subfolders named after the AudioBook Bay title, making it easy for [**Audiobookshelf**](https://www.audiobookshelf.org/) to automatically add completed downloads to its library. 25 | 26 | 27 | 28 | ## Why Use This? 29 | AudiobookBay Downloader provides a simple and user-friendly interface for users to download audiobooks without on their own and import them into your libary. 30 | 31 | --- 32 | 33 | ## Installation 34 | 35 | ### Prerequisites 36 | - **Deluge, qBittorrent or Transmission** (with the WebUI enabled) 37 | - **Docker** (optional, for containerized deployments) 38 | 39 | ### Environment Variables 40 | The app uses environment variables to configure its behavior. Below are the required variables: 41 | 42 | ```env 43 | DL_SCHEME=http 44 | DL_HOST=192.168.xxx.xxx # IP or hostname of your qBittorrent or Transmission instance 45 | DL_PORT=8080 # torrent WebUI port 46 | DL_USERNAME=YOUR_USER # torrent username 47 | DL_PASSWORD=YOUR_PASSWORD # torrent password 48 | DL_CATEGORY=abb-downloader # torrent category for downloads 49 | SAVE_PATH_BASE=/audiobooks # Root path for audiobook downloads (relative to torrent) 50 | ABB_HOSTNAME='audiobookbay.is' #Default 51 | ``` 52 | The following optional variables add an additional entry to the navigation bar. This is useful for linking to your audiobook player or another related service: 53 | 54 | ``` 55 | NAV_LINK_NAME=Open Audiobook Player 56 | NAV_LINK_URL=https://audiobooks.yourdomain.com/ 57 | ``` 58 | 59 | ### Using Docker 60 | 61 | 1. Use `docker-compose` for quick deployment. Example `docker-compose.yml`: 62 | 63 | ```yaml 64 | version: '3.8' 65 | 66 | services: 67 | audiobookbay-downloader: 68 | image: ghcr.io/jamesry96/audiobookbay-automated:latest 69 | ports: 70 | - "5078:5078" 71 | container_name: audiobookbay-downloader 72 | environment: 73 | - DOWNLOAD_CLIENT=qbittorrent 74 | - DL_SCHEME=http 75 | - DL_HOST=192.168.1.123 76 | - DL_PORT=8080 77 | - DL_USERNAME=admin 78 | - DL_PASSWORD=pass 79 | - DL_CATEGORY=abb-downloader 80 | - SAVE_PATH_BASE=/audiobooks 81 | - ABB_HOSTNAME='audiobookbay.is' #Default 82 | - NAV_LINK_NAME=Open Audiobook Player #Optional 83 | - NAV_LINK_URL=https://audiobooks.yourdomain.com/ #Optional 84 | ``` 85 | 86 | 2. **Start the Application**: 87 | ```bash 88 | docker-compose up -d 89 | ``` 90 | 91 | ### Running Locally 92 | 1. **Install Dependencies**: 93 | Ensure you have Python installed, then install the required dependencies: 94 | ```bash 95 | pip install -r requirements.txt 96 | 97 | 2. Create a .env file in the project directory to configure your application. Below is an example of the required variables: 98 | ``` 99 | # Torrent Client Configuration 100 | DOWNLOAD_CLIENT=transmission # Change to delugeweb, transmission or qbittorrent 101 | DL_SCHEME=http 102 | DL_HOST=192.168.1.123 103 | DL_PORT=8080 104 | DL_USERNAME=admin 105 | DL_PASSWORD=pass 106 | DL_CATEGORY=abb-downloader 107 | SAVE_PATH_BASE=/audiobooks 108 | 109 | # AudiobookBar Hostname 110 | ABB_HOSTNAME='audiobookbay.is' #Default 111 | # ABB_HOSTNAME='audiobookbay.lu' #Alternative 112 | 113 | # Optional Navigation Bar Entry 114 | NAV_LINK_NAME=Open Audiobook Player 115 | NAV_LINK_URL=https://audiobooks.yourdomain.com/ 116 | ``` 117 | 118 | 3. Start the app: 119 | ```bash 120 | python app.py 121 | ``` 122 | 123 | --- 124 | 125 | ## Notes 126 | - **This app does NOT download any material**: It simply generates magnet links and sends them to your qBittorrent client for handling. 127 | 128 | - **Folder Mapping**: __The `SAVE_PATH_BASE` is based on the perspective of your torrent client__, not this app. This app does not move any files; all file handling and organization are managed by the torrent client. Ensure that the `SAVE_PATH_BASE` in your torrent client aligns with your audiobook library (e.g., for Audiobookshelf). Using a path relative to where this app is running, instead of the torrent client, will cause issues. 129 | 130 | 131 | --- 132 | 133 | ## Feedback and Contributions 134 | This project is a work in progress, and your feedback is welcome! Feel free to open issues or contribute by submitting pull requests. 135 | 136 | --- 137 | 138 | ## Screenshots 139 | ### Search Results 140 |  141 | 142 | ### Download Status 143 |  144 | 145 | --- 146 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import os, re, requests 2 | from flask import Flask, request, render_template, jsonify 3 | from bs4 import BeautifulSoup 4 | from qbittorrentapi import Client 5 | from transmission_rpc import Client as transmissionrpc 6 | from deluge_web_client import DelugeWebClient as delugewebclient 7 | from dotenv import load_dotenv 8 | from urllib.parse import urlparse 9 | app = Flask(__name__) 10 | 11 | #Load environment variables 12 | load_dotenv() 13 | 14 | ABB_HOSTNAME = os.getenv("ABB_HOSTNAME", "audiobookbay.lu") 15 | 16 | DOWNLOAD_CLIENT = os.getenv("DOWNLOAD_CLIENT") 17 | DL_URL = os.getenv("DL_URL") 18 | if DL_URL: 19 | parsed_url = urlparse(DL_URL) 20 | DL_SCHEME = parsed_url.scheme 21 | DL_HOST = parsed_url.hostname 22 | DL_PORT = parsed_url.port 23 | else: 24 | DL_SCHEME = os.getenv("DL_SCHEME", "http") 25 | DL_HOST = os.getenv("DL_HOST") 26 | DL_PORT = os.getenv("DL_PORT") 27 | 28 | # Make a DL_URL for Deluge if one was not specified 29 | if DL_HOST and DL_PORT: 30 | DL_URL = f"{DL_SCHEME}://{DL_HOST}:{DL_PORT}" 31 | 32 | DL_USERNAME = os.getenv("DL_USERNAME") 33 | DL_PASSWORD = os.getenv("DL_PASSWORD") 34 | DL_CATEGORY = os.getenv("DL_CATEGORY", "Audiobookbay-Audiobooks") 35 | SAVE_PATH_BASE = os.getenv("SAVE_PATH_BASE") 36 | 37 | # Custom Nav Link Variables 38 | NAV_LINK_NAME = os.getenv("NAV_LINK_NAME") 39 | NAV_LINK_URL = os.getenv("NAV_LINK_URL") 40 | 41 | #Print configuration 42 | print(f"ABB_HOSTNAME: {ABB_HOSTNAME}") 43 | print(f"DOWNLOAD_CLIENT: {DOWNLOAD_CLIENT}") 44 | print(f"DL_HOST: {DL_HOST}") 45 | print(f"DL_PORT: {DL_PORT}") 46 | print(f"DL_URL: {DL_URL}") 47 | print(f"DL_USERNAME: {DL_USERNAME}") 48 | print(f"DL_CATEGORY: {DL_CATEGORY}") 49 | print(f"SAVE_PATH_BASE: {SAVE_PATH_BASE}") 50 | print(f"NAV_LINK_NAME: {NAV_LINK_NAME}") 51 | print(f"NAV_LINK_URL: {NAV_LINK_URL}") 52 | 53 | 54 | @app.context_processor 55 | def inject_nav_link(): 56 | return { 57 | 'nav_link_name': os.getenv('NAV_LINK_NAME'), 58 | 'nav_link_url': os.getenv('NAV_LINK_URL') 59 | } 60 | 61 | 62 | 63 | # Helper function to search AudiobookBay 64 | def search_audiobookbay(query, max_pages=5): 65 | headers = { 66 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' 67 | } 68 | results = [] 69 | for page in range(1, max_pages + 1): 70 | url = f"https://{ABB_HOSTNAME}/page/{page}/?s={query.replace(' ', '+')}&cat=undefined%2Cundefined" 71 | response = requests.get(url, headers=headers) 72 | if response.status_code != 200: 73 | print(f"[ERROR] Failed to fetch page {page}. Status Code: {response.status_code}") 74 | break 75 | 76 | soup = BeautifulSoup(response.text, 'html.parser') 77 | for post in soup.select('.post'): 78 | try: 79 | title = post.select_one('.postTitle > h2 > a').text.strip() 80 | link = f"https://{ABB_HOSTNAME}{post.select_one('.postTitle > h2 > a')['href']}" 81 | cover = post.select_one('img')['src'] if post.select_one('img') else "/static/images/default-cover.jpg" 82 | results.append({'title': title, 'link': link, 'cover': cover}) 83 | except Exception as e: 84 | print(f"[ERROR] Skipping post due to error: {e}") 85 | continue 86 | return results 87 | 88 | # Helper function to extract magnet link from details page 89 | def extract_magnet_link(details_url): 90 | headers = { 91 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' 92 | } 93 | try: 94 | response = requests.get(details_url, headers=headers) 95 | if response.status_code != 200: 96 | print(f"[ERROR] Failed to fetch details page. Status Code: {response.status_code}") 97 | return None 98 | 99 | soup = BeautifulSoup(response.text, 'html.parser') 100 | 101 | # Extract Info Hash 102 | info_hash_row = soup.find('td', string=re.compile(r'Info Hash', re.IGNORECASE)) 103 | if not info_hash_row: 104 | print("[ERROR] Info Hash not found on the page.") 105 | return None 106 | info_hash = info_hash_row.find_next_sibling('td').text.strip() 107 | 108 | # Extract Trackers 109 | tracker_rows = soup.find_all('td', string=re.compile(r'udp://|http://', re.IGNORECASE)) 110 | trackers = [row.text.strip() for row in tracker_rows] 111 | 112 | if not trackers: 113 | print("[WARNING] No trackers found on the page. Using default trackers.") 114 | trackers = [ 115 | "udp://tracker.openbittorrent.com:80", 116 | "udp://opentor.org:2710", 117 | "udp://tracker.ccc.de:80", 118 | "udp://tracker.blackunicorn.xyz:6969", 119 | "udp://tracker.coppersurfer.tk:6969", 120 | "udp://tracker.leechers-paradise.org:6969" 121 | ] 122 | 123 | # Construct the magnet link 124 | trackers_query = "&".join(f"tr={requests.utils.quote(tracker)}" for tracker in trackers) 125 | magnet_link = f"magnet:?xt=urn:btih:{info_hash}&{trackers_query}" 126 | 127 | print(f"[DEBUG] Generated Magnet Link: {magnet_link}") 128 | return magnet_link 129 | 130 | except Exception as e: 131 | print(f"[ERROR] Failed to extract magnet link: {e}") 132 | return None 133 | 134 | # Helper function to sanitize titles 135 | def sanitize_title(title): 136 | return re.sub(r'[<>:"/\\|?*]', '', title).strip() 137 | 138 | # Endpoint for search page 139 | @app.route('/', methods=['GET', 'POST']) 140 | def search(): 141 | books = [] 142 | try: 143 | if request.method == 'POST': # Form submitted 144 | query = request.form['query'] 145 | #Convert to all lowercase 146 | query = query.lower() 147 | if query: # Only search if the query is not empty 148 | books = search_audiobookbay(query) 149 | return render_template('search.html', books=books) 150 | except Exception as e: 151 | print(f"[ERROR] Failed to search: {e}") 152 | return render_template('search.html', books=books, error=f"Failed to search. { str(e) }") 153 | 154 | 155 | 156 | 157 | # Endpoint to send magnet link to qBittorrent 158 | @app.route('/send', methods=['POST']) 159 | def send(): 160 | data = request.json 161 | details_url = data.get('link') 162 | title = data.get('title') 163 | if not details_url or not title: 164 | return jsonify({'message': 'Invalid request'}), 400 165 | 166 | try: 167 | magnet_link = extract_magnet_link(details_url) 168 | if not magnet_link: 169 | return jsonify({'message': 'Failed to extract magnet link'}), 500 170 | 171 | save_path = f"{SAVE_PATH_BASE}/{sanitize_title(title)}" 172 | 173 | if DOWNLOAD_CLIENT == 'qbittorrent': 174 | qb = Client(host=DL_HOST, port=DL_PORT, username=DL_USERNAME, password=DL_PASSWORD) 175 | qb.auth_log_in() 176 | qb.torrents_add(urls=magnet_link, save_path=save_path, category=DL_CATEGORY) 177 | elif DOWNLOAD_CLIENT == 'transmission': 178 | transmission = transmissionrpc(host=DL_HOST, port=DL_PORT, protocol=DL_SCHEME, username=DL_USERNAME, password=DL_PASSWORD) 179 | transmission.add_torrent(magnet_link, download_dir=save_path) 180 | elif DOWNLOAD_CLIENT == "delugeweb": 181 | delugeweb = delugewebclient(url=DL_URL, password=DL_PASSWORD) 182 | delugeweb.login() 183 | delugeweb.add_torrent_magnet(magnet_link, save_directory=save_path, label=DL_CATEGORY) 184 | else: 185 | return jsonify({'message': 'Unsupported download client'}), 400 186 | 187 | return jsonify({'message': f'Download added successfully! This may take some time, the download will show in Audiobookshelf when completed.'}) 188 | except Exception as e: 189 | return jsonify({'message': str(e)}), 500 190 | @app.route('/status') 191 | def status(): 192 | try: 193 | if DOWNLOAD_CLIENT == 'transmission': 194 | transmission = transmissionrpc(host=DL_HOST, port=DL_PORT, username=DL_USERNAME, password=DL_PASSWORD) 195 | torrents = transmission.get_torrents() 196 | torrent_list = [ 197 | { 198 | 'name': torrent.name, 199 | 'progress': round(torrent.progress, 2), 200 | 'state': torrent.status, 201 | 'size': f"{torrent.total_size / (1024 * 1024):.2f} MB" 202 | } 203 | for torrent in torrents 204 | ] 205 | return render_template('status.html', torrents=torrent_list) 206 | elif DOWNLOAD_CLIENT == 'qbittorrent': 207 | qb = Client(host=DL_HOST, port=DL_PORT, username=DL_USERNAME, password=DL_PASSWORD) 208 | qb.auth_log_in() 209 | torrents = qb.torrents_info(category=DL_CATEGORY) 210 | torrent_list = [ 211 | { 212 | 'name': torrent.name, 213 | 'progress': round(torrent.progress * 100, 2), 214 | 'state': torrent.state, 215 | 'size': f"{torrent.total_size / (1024 * 1024):.2f} MB" 216 | } 217 | for torrent in torrents 218 | ] 219 | elif DOWNLOAD_CLIENT == "delugeweb": 220 | delugeweb = delugewebclient(url=DL_URL, password=DL_PASSWORD) 221 | delugeweb.login() 222 | torrents = delugeweb.get_torrents_status( 223 | filter_dict={"label": DL_CATEGORY}, 224 | keys=["name", "state", "progress", "total_size"], 225 | ) 226 | torrent_list = [ 227 | { 228 | "name": torrent["name"], 229 | "progress": round(torrent["progress"], 2), 230 | "state": torrent["state"], 231 | "size": f"{torrent['total_size'] / (1024 * 1024):.2f} MB", 232 | } 233 | for k, torrent in torrents.result.items() 234 | ] 235 | else: 236 | return jsonify({'message': 'Unsupported download client'}), 400 237 | return render_template('status.html', torrents=torrent_list) 238 | except Exception as e: 239 | return jsonify({'message': f"Failed to fetch torrent status: {e}"}), 500 240 | 241 | 242 | 243 | if __name__ == '__main__': 244 | app.run(host='0.0.0.0', port=5078) 245 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | requests 3 | beautifulsoup4 4 | qbittorrent-api 5 | python-dotenv 6 | transmission-rpc 7 | deluge-web-client 8 | -------------------------------------------------------------------------------- /app/static/css/search.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background: linear-gradient(135deg, #6a1b9a, #8e24aa, #9c27b0); 6 | color: white; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: flex-start; 12 | } 13 | h1 { 14 | margin-top: 20px; 15 | font-size: 2.5rem; 16 | text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); 17 | } 18 | form { 19 | margin: 20px 0; 20 | display: flex; 21 | justify-content: center; /* Center the form content */ 22 | align-items: center; /* Center the form content vertically */ 23 | } 24 | input[type="text"] { 25 | padding: 10px; 26 | border: none; 27 | border-radius: 5px; 28 | width: 300px; 29 | font-size: 1rem; 30 | } 31 | button { 32 | padding: 10px 20px; 33 | width: 100%; 34 | background-color: #512da8; 35 | border: none; 36 | border-radius: 5px; 37 | color: white; 38 | font-size: 1rem; 39 | cursor: pointer; 40 | transition: background-color 0.3s; 41 | margin-bottom: 10px; 42 | } 43 | button:hover { 44 | background-color: #673ab7; 45 | } 46 | table { 47 | width: 90%; 48 | max-width: 1000px; 49 | margin-top: 20px; 50 | border-collapse: collapse; 51 | background-color: rgba(255, 255, 255, 0.1); 52 | border-radius: 10px; 53 | overflow: hidden; 54 | } 55 | td { 56 | padding: 15px; 57 | text-align: left; 58 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 59 | } 60 | img.cover { 61 | border-radius: 5px; 62 | } 63 | .search-container { 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | gap: 10px; /* Optional: Add some space between the input and button */ 68 | } 69 | .search-bar { 70 | width: 100%; /* Make the search bar full width */ 71 | padding: 10px; /* Adjust padding as needed */ 72 | box-sizing: border-box; /* Ensure padding is included in the width */ 73 | margin-bottom: 10px; /* Add some space between the input and button */ 74 | } 75 | .search-button { 76 | width: auto; /* Set the button width to auto */ 77 | padding: 10px 20px; /* Add padding to the button */ 78 | background-color: #512da8; 79 | border: none; 80 | border-radius: 5px; 81 | color: white; 82 | font-size: 1rem; 83 | cursor: pointer; 84 | transition: background-color 0.3s; 85 | } 86 | 87 | /* Loading spinner styles */ 88 | .loading-spinner { 89 | display: none; /* Hidden by default */ 90 | flex-direction: column; 91 | align-items: center; 92 | justify-content: center; 93 | margin-top: 20px; 94 | } 95 | 96 | .spinner { 97 | width: 50px; 98 | height: 50px; 99 | border: 5px solid rgba(255, 255, 255, 0.3); 100 | border-top: 5px solid white; 101 | border-radius: 50%; 102 | animation: spin 1s linear infinite; 103 | } 104 | 105 | .loading-spinner p { 106 | margin-top: 10px; 107 | font-size: 1rem; 108 | color: white; 109 | } 110 | 111 | .button-spinner { 112 | display: inline-block; 113 | vertical-align: middle; 114 | margin-left: 5px; 115 | } 116 | 117 | .button-spinner .spinner { 118 | width: 16px; 119 | height: 16px; 120 | border: 2px solid #f3f3f3; 121 | border-top: 2px solid #3498db; 122 | border-radius: 50%; 123 | animation: spin 1s linear infinite; 124 | } 125 | 126 | .message-scroller { 127 | display: none; 128 | text-align: center; 129 | margin: 0 auto; 130 | width: fit-content; 131 | } 132 | 133 | .error-message { 134 | color: red; 135 | } 136 | 137 | /* Spinner animation */ 138 | @keyframes spin { 139 | 0% { 140 | transform: rotate(0deg); 141 | } 142 | 100% { 143 | transform: rotate(360deg); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/static/css/status.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background: linear-gradient(135deg, #6a1b9a, #8e24aa, #9c27b0); 6 | color: white; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: flex-start; 12 | } 13 | h1 { 14 | margin-top: 20px; 15 | font-size: 2.5rem; 16 | text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); 17 | } 18 | table { 19 | width: 90%; 20 | max-width: 1000px; 21 | margin-top: 20px; 22 | border-collapse: collapse; 23 | background-color: rgba(255, 255, 255, 0.1); 24 | border-radius: 10px; 25 | overflow: hidden; 26 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 27 | } 28 | th, td { 29 | padding: 15px; 30 | text-align: left; 31 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 32 | color: white; 33 | } 34 | th { 35 | background-color: rgba(0, 0, 0, 0.3); 36 | font-weight: bold; 37 | text-transform: uppercase; 38 | } 39 | td { 40 | background-color: rgba(0, 0, 0, 0.1); 41 | } 42 | tr:hover td { 43 | background-color: rgba(255, 255, 255, 0.2); 44 | } 45 | -------------------------------------------------------------------------------- /app/static/css/style.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | width: 100%; 3 | background-color: rgba(0, 0, 0, 0.3); 4 | display: flex; 5 | justify-content: center; 6 | padding: 10px 0; 7 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 8 | position: sticky; 9 | top: 0; 10 | z-index: 1000; 11 | } 12 | 13 | .navbar a { 14 | text-decoration: none; 15 | color: white; 16 | margin: 0 20px; 17 | font-size: 1.2rem; 18 | font-weight: bold; 19 | transition: color 0.3s; 20 | } 21 | 22 | .navbar a:hover { 23 | color: #ffcc00; 24 | } 25 | 26 | .navbar a.active { 27 | color: #ffcc00; 28 | border-bottom: 2px solid #ffcc00; 29 | } 30 | .title-container { 31 | text-align: center; 32 | width: 100%; 33 | margin-bottom: 20px; 34 | } 35 | 36 | h1 { 37 | font-size: 2.5rem; 38 | text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); 39 | margin: 0 auto; 40 | width: fit-content; 41 | } 42 | -------------------------------------------------------------------------------- /app/static/images/default_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesRy96/audiobookbay-automated/46e7725ee165176bd96c83d917e6505caa7c3434/app/static/images/default_cover.jpg -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |Searching...
31 |{{ book.title }} | 44 |45 | 46 | 47 | | 48 |
Title | 17 |Progress | 18 |State | 19 |Size | 20 |
---|---|---|---|
{{ torrent.name }} | 26 |{{ torrent.progress }}% | 27 |{{ torrent.state }} | 28 |{{ torrent.size }} | 29 |