diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..235c925 Binary files /dev/null and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile index e69de29..549fc83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Use official Python image +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Set work directory +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy project files +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the app with Uvicorn +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..4065509 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/__pycache__/download.cpython-313.pyc b/app/__pycache__/download.cpython-313.pyc new file mode 100644 index 0000000..bd6a6c8 Binary files /dev/null and b/app/__pycache__/download.cpython-313.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 73b097b..27149bd 100644 Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/data/dropout.json b/app/data/dropout.json new file mode 100644 index 0000000..b10088c --- /dev/null +++ b/app/data/dropout.json @@ -0,0 +1,531 @@ +[ + { + "SHOW": "Adventuring Academy", + "URL": "https://www.dropout.tv/adventuring-academy", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5" + ] + }, + { + "SHOW": "Bad Internet", + "URL": "https://www.dropout.tv/bad-internet", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Breaking News No Laugh Newsroom", + "URL": "https://www.dropout.tv/breaking-news-no-laugh-newsroom", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ] + }, + { + "SHOW": "Cartoon Hell", + "URL": "https://www.dropout.tv/cartoon-hell", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "Dimension 20", + "URL": "https://www.dropout.tv/dimension-20", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25" + ] + }, + { + "SHOW": "Dimension 20 A Court of Fey Flowers", + "URL": "https://www.dropout.tv/dimension-20-a-court-of-fey-flowers", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 A Crown of Candy", + "URL": "https://www.dropout.tv/dimension-20-a-crown-of-candy", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 A Starstruck Odyssey", + "URL": "https://www.dropout.tv/dimension-20-a-starstruck-odyssey", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Animated", + "URL": "https://www.dropout.tv/dimension-20-animated", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Audio Only", + "URL": "https://www.dropout.tv/dimension-20-audio-only", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25" + ] + }, + { + "SHOW": "Dimension 20 Burrow's End", + "URL": "https://www.dropout.tv/dimension-20-burrow-s-end", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Coffin Run", + "URL": "https://www.dropout.tv/dimension-20-coffin-run", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Dungeons And Drag Queens", + "URL": "https://www.dropout.tv/dimension-20-dungeons-and-drag-queens", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "Dimension 20 Escape From The Bloodkeep", + "URL": "https://www.dropout.tv/dimension-20-escape-from-the-bloodkeep", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Fantasy High", + "URL": "https://www.dropout.tv/dimension-20-fantasy-high", + "SEASONS": [ + "1", + "2", + "3" + ] + }, + { + "SHOW": "Dimension 20 Fantasy High Junior Year", + "URL": "https://www.dropout.tv/dimension-20-fantasy-high-junior-year", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Leviathan", + "URL": "https://www.dropout.tv/dimension-20-leviathan", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Mentopolis", + "URL": "https://www.dropout.tv/dimension-20-mentopolis", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Misfits And Magic", + "URL": "https://www.dropout.tv/dimension-20-misfits-and-magic", + "SEASONS": [ + "1", + "2", + "3" + ] + }, + { + "SHOW": "Dimension 20 Never Stop Blowing Up", + "URL": "https://www.dropout.tv/dimension-20-never-stop-blowing-up", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Neverafter", + "URL": "https://www.dropout.tv/dimension-20-neverafter", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Shriek Week", + "URL": "https://www.dropout.tv/dimension-20-shriek-week", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 The Ravening War", + "URL": "https://www.dropout.tv/dimension-20-the-ravening-war", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 The Seven", + "URL": "https://www.dropout.tv/dimension-20-the-seven", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 The Unsleeping City", + "URL": "https://www.dropout.tv/dimension-20-the-unsleeping-city", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "Dimension 20 Time Quangle", + "URL": "https://www.dropout.tv/dimension-20-time-quangle", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Tiny Heist", + "URL": "https://www.dropout.tv/dimension-20-tiny-heist", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20 Titan Takedown", + "URL": "https://www.dropout.tv/dimension-20-titan-takedown", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Dimension 20's Adventuring Party", + "URL": "https://www.dropout.tv/dimension-20-s-adventuring-party", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20" + ] + }, + { + "SHOW": "Dirty Laundry", + "URL": "https://www.dropout.tv/dirty-laundry", + "SEASONS": [ + "1", + "2", + "3", + "4", + "69" + ] + }, + { + "SHOW": "Dropout Music", + "URL": "https://www.dropout.tv/dropout-music", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Erotic Clubhouse", + "URL": "https://www.dropout.tv/erotic-clubhouse", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Fantasy High Extra Credit", + "URL": "https://www.dropout.tv/fantasy-high-extra-credit", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Game Changer", + "URL": "https://www.dropout.tv/game-changer", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ] + }, + { + "SHOW": "Game Changer Animated", + "URL": "https://www.dropout.tv/game-changer-animated", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Game Changer Battle Royale", + "URL": "https://www.dropout.tv/game-changer-battle-royale", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Gastronauts", + "URL": "https://www.dropout.tv/gastronauts", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Gods of Food", + "URL": "https://www.dropout.tv/gods-of-food", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Kingpin Katie", + "URL": "https://www.dropout.tv/kingpin-katie", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Kingpin Katie The Web Series", + "URL": "https://www.dropout.tv/kingpin-katie-the-web-series", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Make Some Noise", + "URL": "https://www.dropout.tv/make-some-noise", + "SEASONS": [ + "1", + "2", + "3" + ] + }, + { + "SHOW": "Mice Murder", + "URL": "https://www.dropout.tv/mice-murder", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Monet's Slumber Party", + "URL": "https://www.dropout.tv/monet-s-slumber-party", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Nobody Asked", + "URL": "https://www.dropout.tv/nobody-asked", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Paranoia", + "URL": "https://www.dropout.tv/paranoia", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "Parlor Room", + "URL": "https://www.dropout.tv/parlor-room", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Play It By Ear", + "URL": "https://www.dropout.tv/play-it-by-ear", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "See Plum Run", + "URL": "https://www.dropout.tv/see-plum-run", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Smartypants", + "URL": "https://www.dropout.tv/smartypants", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "Thousandaires", + "URL": "https://www.dropout.tv/thousandaires", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Total Forgiveness", + "URL": "https://www.dropout.tv/total-forgiveness", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Total Forgiveness Early Payoff", + "URL": "https://www.dropout.tv/total-forgiveness-early-payoff", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Troopers 1", + "URL": "https://www.dropout.tv/troopers-1", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Troopers Animated", + "URL": "https://www.dropout.tv/troopers-animated", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Ultramechatron Team Go 1", + "URL": "https://www.dropout.tv/ultramechatron-team-go-1", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Um Actually", + "URL": "https://www.dropout.tv/um-actually", + "SEASONS": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + }, + { + "SHOW": "Very Important People", + "URL": "https://www.dropout.tv/very-important-people", + "SEASONS": [ + "1", + "2" + ] + }, + { + "SHOW": "What The Facts 101", + "URL": "https://www.dropout.tv/what-the-facts-101", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Where In The Eff Is Sarah Cincinnati", + "URL": "https://www.dropout.tv/where-in-the-eff-is-sarah-cincinnati", + "SEASONS": [ + "1" + ] + }, + { + "SHOW": "Wtf 101", + "URL": "https://www.dropout.tv/wtf-101", + "SEASONS": [ + "1" + ] + } +] \ No newline at end of file diff --git a/app/download.py b/app/download.py new file mode 100644 index 0000000..1787ce0 --- /dev/null +++ b/app/download.py @@ -0,0 +1,131 @@ +import os, yt_dlp, json, requests +from bs4 import BeautifulSoup + +class grab(): + def season(url): + page_html=requests.get(url) + soup = BeautifulSoup(page_html.text, 'html.parser') + select_element = soup.find('select', class_='js-switch-season') + options = select_element.find_all('option') + option_values = [option['value'] for option in options if option.has_attr('value')] + seasons = [item.replace(url+'/season:', '') for item in option_values] + return seasons + +class dropout(): + def show(show,season,episode): + directory='/mnt/plex/tv/'+show+'/Season '+season+'/' + with open('data/dropout.json', 'r') as json_file: + url_mapping = json.load(json_file) + url = next((item['URL'] for item in url_mapping if item['SHOW'] == show), None) + if url is not None: + url = f'{url}/season:{season}' + else: + raise ValueError(f"Show '{show}' not found in the JSON data.") + if not os.path.exists(directory): + os.makedirs(directory) + if episode is None or episode == '': + episode = '%(playlist_index)02d' + else: + try: + dl_ops['playliststart'] = int(episode) + except ValueError: + # Handle the error, e.g., log it or set a default value + dl_ops['playliststart'] = 0 # or some appropriate default value + dl_ops = { + 'format': 'bestvideo+bestaudio/best', + 'audio_quality': '256K', + 'paths': { + 'temp': '/mnt/data/downloader/temp', + 'home': directory + }, + 'cookiefile': 'cookies.txt', + 'reject_title': [ + r'(?i).*behind.?the.?scenes.*', # Reject titles with "behind the scenes" (case-insensitive) + r'(?i).*trailer.*', # Reject titles with "trailer" (case-insensitive) + r'(?i).*recap.*', # Reject titles with "recap" (case-insensitive) + r'(?i).*last.looks.*' # Reject titles with "last looks" (case-insensitive) + ], + 'outtmpl': show + ' - S'+f"{int(season):02}"+'E'+episode+' - %(title)s.%(ext)s', + 'noplaylist': True, + # Additional options for downloading subtitles + 'writesubtitles': True, # Download subtitles + 'subtitleslangs': ['en'] # Specify the language for subtitles (e.g., 'en' for English) + } + with yt_dlp.YoutubeDL(dl_ops) as ydl: + ydl.download([url]) + + def series(): + json_data=[] + page_html=requests.get('https://www.dropout.tv/series') + # If you want to parse the HTML + soup = BeautifulSoup(page_html.text, 'html.parser') + # Example: Find all elements with a specific class + elements = soup.find_all(class_='browse-item-link') + # Extract URLs from href attributes + urls = [element['href'] for element in elements if 'href' in element.attrs] + + for url in urls: + info_data={} + name=url.replace('https://www.dropout.tv/','').replace('-s-',"'s-").replace('-',' ').title().replace('Of','of').replace("'S","'s") + info_data['SHOW'] = name + info_data['URL'] = url + info_data['SEASONS'] = grab.season(url) + json_data.append(info_data) + + # Sort the json_data by the 'SHOW' key + sorted_json_data = sorted(json_data, key=lambda x: x['SHOW']) + + with open('data/dropout.json', 'w') as json_file: + json.dump(sorted_json_data, json_file, indent=4) + return sorted_json_data + +def ydl(url, location): + dl_ops = {'paths': {'temp': '/mnt/data/downloader/temp', 'home': location}, 'outtmpl': '%(uploader)s/%(title)s.%(ext)s'} + if location == "/mnt/plex/podcasts": + dl_ops['format'] = 'bestaudio/best[ext=mp3]' + dl_ops['postprocessors'] = [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }, { + 'key': 'FFmpegMetadata', + 'add_metadata': True, + }] + elif location == "/mnt/plex/asmr": + dl_ops['format'] = 'bestaudio/best[ext=mp3]' + dl_ops['postprocessors'] = [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }, { + 'key': 'FFmpegMetadata', + 'add_metadata': True, + }] + elif location == "/mnt/nsfw": + dl_ops['format'] = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best' + + else: + dl_ops = { + 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', + 'cookiefile': 'yt-cookies.txt' + } + + + with yt_dlp.YoutubeDL(dl_ops) as ydl: + ydl.download(url) + + # Extracting video information + video_info = ydl.extract_info(url, download=False) + thumbnail_url = video_info.get('thumbnail') + + # Download the thumbnail image + if thumbnail_url: + try: + thumbnail_filename = os.path.join(location, f"{video_info['id']}.jpg") + with open(thumbnail_filename, 'wb') as thumbnail_file: + thumbnail_file.write(requests.get(thumbnail_url).content) + print("Downloaded MP4 and downloaded thumbnail successfully!") + except Exception as e: + print(f"Error downloading thumbnail: {str(e)}") + else: + print("Downloaded MP4 but no thumbnail found.") \ No newline at end of file diff --git a/app/main.py b/app/main.py index 8d5f969..a5325c0 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,10 @@ from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles +from pathlib import Path +import download +import json app = FastAPI() @@ -11,6 +14,26 @@ app.mount("/static", StaticFiles(directory="static"), name="static") # Jinja2 template directory templates = Jinja2Templates(directory="templates") + +# api +@app.get("/dropoutUpdate") +async def grab(): + return download.dropout.series() + +@app.get("/dropoutSeries", response_class=JSONResponse) +async def read(): + file_path = Path("data/dropout.json") + if file_path.exists(): + with file_path.open("r", encoding="utf-8") as f: + data = json.load(f) + return JSONResponse(content=data) + return JSONResponse(content={"error": "File not found"}, status_code=404) + +@app.get("/dropoutDownload") +async def download(show: str, season: str, episode: str): + download.dropout.show(show,season,episode) + +# html @app.get("/", response_class=HTMLResponse) async def read_root(request: Request): return templates.TemplateResponse("index.html", {"request": request, "message": "Hello from FastAPI + Jinja2!"})