from lib.sonarr import Sonarr_API from lib.db import sqlite_db from lib.db import Today_Queue_Status from lib.discord_bot import ScheduleBot from lib.logging_data import logger from lib.check_track_detail import check_langs_with_langcodes,extract_file_path from lib.torrent_creator import TorrentUpload from dotenv import load_dotenv import pytz from datetime import datetime import discord import asyncio import requests import os # from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler from typing import Callable, Awaitable class USK_Status: def __init__(self): self.title="" self.check_status = False self.check_found = False self.check_lang_found = False self.download_status = False self.import_status = False self.import_episode: requests.Response | None = None self.torrent_status = False self.message = "" self.other_data="" class USK(ScheduleBot): CODEC_MAP = { "265": 'h.265', "264": 'h.264', "AV1": 'av1' } QUALITY_MAP = { 1080: "1080p", 720: "720p", 480: "480p", 360: "360p", 240: "240p", } AUDIO_CHANNEL_MAP = { '20': "2.0", '51': "5.1" } def __init__(self,console:logger=None): super().__init__(console=console) # print(Path(__file__).resolve() / ".env") load_dotenv(".env") self.tz = pytz.timezone("Asia/Bangkok") self.console = console self.usk_path = os.getenv("usk_path") # print(self.usk_path) os.chdir(self.usk_path) self.path = os.getenv("db_path") self.sonarr_ip = os.getenv("sonarr_ip") self.sonarr_key = os.getenv("sonarr_key") self.download_path = os.getenv("download_path") self.channel = None self.scheduler = self.scheduler if isinstance( self.scheduler, AsyncIOScheduler) else AsyncIOScheduler() self.sonarr = Sonarr_API(self.sonarr_ip, self.sonarr_key) self.db = sqlite_db(self.path) def download_command(self,entry,title_config,check=False): cmd = [self.usk_path+".venv/bin/unshackle", 'dl',"--no-cache"] if check: # cmd += ['--skip-dl'] cmd += ['--list'] else: if title_config.get("tmdb_id"): cmd += ['--tmdb',title_config["tmdb_id"],"--enrich"] if title_config['absolute']: cmd += ['--episode-overwrite',str(int(title_config['absolute'])+int(entry['episode']))] if title_config['absolute_season']: cmd += ['--season-overwrite',str(int(title_config['absolute_season'])+int(title_config['season']))] cmd += ['--quality', str(title_config['quality'])] cmd += ['--range', title_config['range']] cmd += ['--vcodec', self.CODEC_MAP[str(title_config['codec'])]] if title_config['proxy']: cmd += ['--proxy', title_config['proxy']] else: cmd += ['--no-proxy'] cmd += ['--v-lang',"all"] cmd += ['--a-lang', f"{title_config['audio_lang'] if title_config['Service'] not in ["TID","MMAX"] else 'all'}"] cmd += ['--s-lang', f"{title_config['sub_lang'] if title_config['Service'] not in ["TID","MMAX"] else 'all'}"] if title_config['audio_channel']: cmd += ['--channels', self.AUDIO_CHANNEL_MAP[title_config['audio_channel']]] if bool(title_config['worst']): cmd += ['--worst'] if title_config['video_bitrate']: cmd += ['--vbitrate', int(title_config['video_bitrate'])] if entry['season'] or entry['episode']: cmd += ['--wanted'] if entry['season']: wanted = f's{entry['season']:02}' else: wanted = "s01" if entry['episode']: if wanted: wanted += f'e{entry['episode']:02}' else: wanted = f'e{entry['episode']:02}' cmd += [wanted] cmd += [title_config['Service']] if title_config['Service'] in ['AMZN']: cmd += ["https://www.primevideo.com/detail/"+title_config['url']] else: cmd += [title_config['url']] if title_config['Service'] in ['TID']: cmd += ['--drm','wv'] if title_config['Service'] in ['TID','BLBL']: if title_config['season']: cmd += ['--season', str(entry['season'])] if title_config['Service'] in ['OND','LT','BLBL','TID']: if title_config['title_lang']: cmd += ['--title_lang', title_config['title_lang']] if title_config['Service'] in ['BLBL']: # if entry['season']: # cmd += ['--season', str(entry['season'])] # if title_config['title_lang']: # cmd += ['--title_lang', str(title_config['title_lang'])] if title_config['url_org']: cmd += ['--original_url', title_config['url_org']] if title_config['org_lang']: cmd += ['--original_lang', str(title_config['org_lang'])] return cmd async def checking_process(self,entry,title_config,title_schedule,discord_notification=False) -> USK_Status: cmd = self.download_command(entry, title_config,check=True) self.console.log(" ".join(cmd)) status=USK_Status() status.title=title_config['Title'] stdout, stderr = await self.run_cmd(cmd) # Available Tracks # if f"S{entry['season']:02}E{entry['episode']:02}" not in stdout: if "Available Tracks" not in stdout: # self.console.warn(f"Not found in the list: {title_config['Title']} Episode {entry['season']}x{entry['episode']}") status.check_found=False status.message=f"Failed to find: {title_config['Title']} Episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Failed to find {title_config['Title']} episode {entry['season']}x{entry['episode']} after multiple attempts.", color=discord.Color.red() ) self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) else: status.check_found=True lang_result=check_langs_with_langcodes(stdout, title_config['audio_lang'].split(','), title_config['sub_lang'].split(',')) self.console.debug("lang_result :",lang_result) if ( lang_result['audio']['exists_all'] or title_config['Service'] in ["IQ"] ) \ and \ (lang_result['subtitle']['exists_all'] or ### Skip checking Thai subtitle that is Thai original (title_config['audio_lang']=="th" and title_config['sub_lang']=="th")): status.check_lang_found=True # embed = discord.Embed( # title=f"Found in the list: {title_config['Title']} Episode {entry['season']}x{entry['episode']}", # description=f"Starting download for {title_config['Title']} - {entry['season']}x{entry['episode']}", # color=discord.Color.green() # ) # # embed.add_field( # # name=f"Episode {entry['season']}x{entry['episode']} found in the list", # # value=f"Starting download for {title_config['Title']} - {entry['season']}x{entry['episode']}", # # inline=False # # ) # # if self.channel: # # await self.channel.send(embed=embed) # status.message=f"{title_config['Title']} Episode {entry['season']}x{entry['episode']} found in the list" # self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) else: status.message=f"Found but language requirements not met: {title_config['Title']} Episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Found episode but language requirements not met", color=discord.Color.orange() ) embed.add_field( name="Audio Language Check", value=f"Configured: {', '.join(lang_result['audio']['configured'])}\nFound: {', '.join(lang_result['audio']['found_langs'])}", inline=False ) embed.add_field( name="Subtitle Language Check", value=f"Configured: {', '.join(lang_result['subtitle']['configured'])}\nFound: {', '.join(lang_result['subtitle']['found_langs'])}", inline=False ) status.check_lang_found=False self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) status.check_status = True if status.check_found and status.check_lang_found else False self.console.debug("Checking status:", status.__dict__) return status async def download_process(self,entry,waiting_minute:int=15,discord_notification:bool=True,retry_count=40) -> USK_Status: title_config= self.db.find_title_config_db(entry['title']) title_schedule=self.db.get_show_by_title(entry['title'])[0] self.console.debug("Entry:", entry) self.console.debug("Title Config:", title_config) self.console.debug("Title Schedule:", title_schedule) status=USK_Status() today_queue_status = Today_Queue_Status() status.message=f"Checking: {title_config['Title']} - {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.blue() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) embed.add_field( name="Command", value=" ".join(self.download_command(entry, title_config,check=True)), inline=False ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) count=0 ############ CHECKING ############ # self.console.debug("Checking status:", status.__dict__) while not status.check_status : status = await self.checking_process(entry,title_config,title_schedule,discord_notification) if status.check_status or count>=retry_count: break status.message=f"Waiting {waiting_minute} minute before checking again for {entry['title']} episode {entry['season']}x{entry['episode']} to be available in the list" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.yellow() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.debug("Checking status:", status.__dict__) self.console.warn(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.waiting) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.waiting) count+=1 await asyncio.sleep(waiting_minute * 60) if status.check_status: status.message=f"Found in the list: {title_config['Title']} Episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Starting download for {title_config['Title']} - {entry['season']}x{entry['episode']}", color=discord.Color.green() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.search_found) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.search_found) # return status elif not status.check_status: status.message=f"Finish searching but not found: {title_config['Title']} Episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Starting download for {title_config['Title']} - {entry['season']}x{entry['episode']}", color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_search) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_search) return status self.console.debug("Checked results:", status.__dict__) ############ CHECKED ############ ############ DOWNLOADING ############ # self.console.debug("Downloading status:", status.__dict__) self.db.update_download_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.downloading) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.downloading) cmd=self.download_command(entry, title_config) status.message=f"Downloading: {title_config['Title']} - {entry['season']}x{entry['episode']}" embed.clear_fields() embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.blue() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) embed.add_field( name="Command", value=" ".join(cmd), inline=False ) self.console.log( status.message, is_gotify=False , is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.console.debug("Download command:", " ".join(cmd)) stdout, stderr = await self.run_cmd(cmd) final_file_path=extract_file_path(stdout) if any(x in stdout for x in ['[E]']) or "Processed all titles" not in stdout or not final_file_path: self.db.update_download_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_download) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_download) status.message=f"Download failed: {title_config['Title']} - {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) # await self.channel.send(embed=embed) status.download_status=False status.message+=f"\n{stdout}" self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) return status else: self.db.update_schedule_episode(title_config['Title'], entry['episode']) status.download_status=True status.message=f"Download completed: {title_config['Title']} - {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.green() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {} ) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.downloaded) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.downloaded) self.console.debug("Download status:", status.__dict__) ############ DOWNLOADED ############ ############ IMPORTING ############ # self.console.debug(status.__dict__) try: self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.importing) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.importing) ep_info=self.sonarr.get_episodes(final_file_path) ep_import = self.sonarr.import_episodes(entry,title_config,ep_info,mode="move") self.console.debug("Import results:", ep_import) if len(ep_import) == 0 : status.import_status=False status.message=f"Failed to import: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.failed_import) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_import) return status for ep in ep_import: if ep.status_code == 201: status.import_status=True status.message=f"Imported successfully: Episode {entry['title']} {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, color=discord.Color.green() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.imported) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.imported) status.import_episode=ep break else: status.import_status=False status.message=f"Failed to import: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.failed_import) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_import) return status except Exception as e: status.import_status=False status.message=f"Failed to import: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) status.message+=f"\n{e}" self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.failed_import) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.failed_import) ############ IMPORTED ############ ############ CHECK IF LAST EPISODE ############ self.console.debug("Checking status:", status.__dict__) self.console.debug("Show details:", self.db.get_show_by_title(title_config['Title'])) if int(entry['episode']) == int(self.db.get_show_by_title(title_config['Title'])[0]["end_ep"]): status.message=f"Last EPISODE of SEASON: {title_config['Title']} - {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.pink() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {} ) ############ CHECK IF LAST EPISODE ############ ############ TORRENTING ############ # self.console.debug(status.__dict__) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.downloaded) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.downloaded) torrent_detail=self.db.get_torrent_detail(entry['title']) if not torrent_detail or torrent_detail['enable']==0 or not status.import_episode : ### if not torrenting return to exit self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.completed) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.completed) return status status.message=f"Creating torrent: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.blue() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.log(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) await asyncio.sleep(5) self.console.debug("Import results:", status.import_episode.json()) for imported_ep in status.import_episode.json()['body']['files']: sonarr_import_count=0 sonarr_found=False # sonarr_seriesId=imported_ep['seriesId'] sonarr_episodeIds=imported_ep['episodeIds'][0] while not sonarr_found : sonarr_import_count+=1 try: sonarr_ep_detail=self.sonarr.get_episode_detail(sonarr_episodeIds) self.console.debug("Sonarr episode details:", sonarr_ep_detail) sonarr_ep_path=sonarr_ep_detail['episodeFile']['path'] sonarr_found=True break except KeyError as e: if sonarr_import_count < 10: self.console.warn("Sonarr not complete import yet. Wait for 10 seconds and try again.") await asyncio.sleep(10) else: status.message=f"Failed to Create torrent: {entry['title']} episode {entry['episode']}" embed = discord.Embed( title=status.message, # description=f"Air time: {title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) embed.add_field( name="Sonarr error", value="Sonarr not found file path. Try to inspect file in Sonarr and try again", inline=False ) status.message+=f"\n{e}" self.console.error(status.message,is_gotify=False,is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) # if not sonarr_found and sonarr_import_count >= 10: self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.fail_upload) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.fail_upload) return status if int( torrent_detail['last_ep'] or 0)==int(sonarr_episodeIds) or torrent_detail['enable']==0: continue if len(status.import_episode.json()['body']['files'])>1: status.message=f"Failed to Create torrent: {entry['title']} episode {entry['season']}x{entry['episode']} : Found more than 1 file for episode." embed = discord.Embed( title=status.message, color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.fail_upload) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.fail_upload) self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) return status upload_entry={ 'file_path':sonarr_ep_path, 'imdb_id':torrent_detail['imdb'], 'tmdb_id':torrent_detail['tmdb'], 'category':torrent_detail['category'], 'source_type':torrent_detail['source_type'], 'source':torrent_detail['source'], 'country':torrent_detail['country'], 'original_platform':torrent_detail['original_platform'], 'is_subdir':False, 'bearbit':True, 'torrentdd':True, 'is_movie':False, 'pack':False } try: """ Stop previous episode before adding a new one Some releases are updated later (e.g., Thai dub added). Example: Old: S01E10 | Audio: JPN New: S01E10 | Audio: JPN,THA The new torrent replaces the old one. To avoid qBittorrent file conflicts or missing file issues, we must stop the existing torrent (same episode) before adding the new one. Additionally, stop the previous episode (e.g., S01E09), since seeding is only required for about one week. """ torrentupload=TorrentUpload(console=self.console) await torrentupload.qbit.login() stop_previous_episode=await torrentupload.qbit.stop_previous_episode_torrent(tmdb_id=torrent_detail['tmdb'],entry=entry,title_config=title_config,prv_count=1) if not stop_previous_episode: status.message= f"Some of Episode has problem when stopping torrent: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, color=discord.Color.orange() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) self.console.warn(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) # await torrentupload.qbit.stop_torrent(search_string=torrent_detail["qbit_name"]) ################################################ upload_status: dict[str,list] = await torrentupload.upload_torrent(upload_entry,qbit_category="SeFree-Automate") self.console.debug("Upload status:", upload_status) if not (any(item['status'] is False for item in upload_status['bearbit']) or any(item['status'] is False for item in upload_status['torrentdd'])) and \ len(upload_status['bearbit'] ) > 0 and len(upload_status['torrentdd']) >0: status.message=f"Torrent Upload Completed: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title="Torrent Upload Completed", description="All torrents have been created and uploaded successfully.", color=discord.Color.green() ) bb_text="" for staBB in upload_status['bearbit']: bb_text+=f"{"Uploaded" if staBB["status"] else "Fail Upload"} : [{staBB["name"]}]({staBB["url"]})" bb_text+="\n" # bb_text+=staBB["url"] # bb_text+="\n" embed.add_field(name="BearBit", value=bb_text, inline=False) dd_text="" for staDD in upload_status['torrentdd']: dd_text+=f"{"Uploaded" if staDD["status"] else "Fail Upload"} : [{staDD["name"]}]({staDD["url"]})" dd_text+="\n" # dd_text+=staDD["url"] # dd_text+="\n" embed.add_field(name="TorrentDD", value=dd_text, inline=False) embed.set_footer(text="Thank you for visiting!") # self.console.debug("Upload status:", upload_status) self.console.log(status.message,is_discord={"channel": self.channel,"embed": embed,"web_hook_urls":os.getenv['torrent_update_webhook'].split(",")}) qbit_name=next( (item["name"] for items in upload_status.values() for item in items if item.get("name")), None ) self.db.update_torrent_detail(entry['title'],qbit_name,sonarr_episodeIds) self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.uploaded) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.uploaded) # return status else: raise Exception("Error during create torrent") except Exception as e: status.message=f"Fail to Create torrent: {entry['title']} episode {entry['season']}x{entry['episode']}" embed = discord.Embed( title=status.message, color=discord.Color.red() ) embed.add_field( name="Air time:", value=f"{title_schedule["day_of_week"]} {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%H:%M:%S')}", inline=False ) embed.add_field( name="Timestamp", value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), inline=True ) status.message+=f"\n{e}" self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.fail_upload) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.fail_upload) self.console.error(status.message, is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if discord_notification else {}) return status self.db.update_download_status(title_config['Title'],entry['season'], entry['episode'], today_queue_status.completed) self.db.update_download_history_status(title_config['Title'], entry['season'], entry['episode'], today_queue_status.completed) return status @staticmethod async def run_cmd(cmd): process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() return stdout.decode(errors='replace') if stdout else None , stderr.decode(errors='replace') if stderr else None # @staticmethod # async def already_in_queue(entrys:list[dict],func: Callable[..., Any]) : # # with open('entrys.json','w') as f: # # json.dump(entrys,f,indent=4) # # exit() # for entry in entrys: # await func(entry,discord_notification=True) # # await VT.download_process(entry,discord_notification=True) @staticmethod async def already_in_queue( entries: list[dict], func: Callable[..., Awaitable[bool]], *, retry_delay: float = 15*60, #900 max_retry: int | None = 40 ): pending = entries.copy() attempt = 0 while pending: attempt += 1 # print(f"Attempt {attempt}, pending = {len(pending)}") next_round = [] for entry in pending: ok = False # try: status:USK_Status = await func(entry,waiting_minute=0,retry_count=0, discord_notification=True) # print(status.check_status) ok = status.check_status and status.download_status # if not ok: # ok=False # else: # ok=True # except Exception as e: # print("Exception:", e) # ok = False if not ok: next_round.append(entry) if not next_round: # print("All entries succeeded") return True if max_retry and attempt >= max_retry: # print("Retry limit reached") return False pending = next_round await asyncio.sleep(retry_delay) def add_shows_jobs(self) -> None: entrys=[] today_queue_status=Today_Queue_Status() for i,entry in enumerate(self.db.get_today_queue()): if entry['status'] in [today_queue_status.downloaded, # today_queue_status.failed_download, today_queue_status.importing, today_queue_status.failed_import, today_queue_status.imported, today_queue_status.completed, today_queue_status.uploading, today_queue_status.uploaded, today_queue_status.fail_upload ]: continue timestamp = entry['start_timestamp'] #+(60*5) if datetime.now(self.tz).timestamp() > entry['start_timestamp']: timestamp = datetime.now(self.tz).timestamp() + (60*i) entrys.append(entry) continue self.console.log(f"Scheduling download for {entry['title']} - {entry['season']}x{entry['episode']} at {datetime.fromtimestamp(timestamp)}", is_gotify=False) # embed = discord.Embed( # title=f"Scheduling download for {entry['title']} - {entry['season']}x{entry['episode']}", # description=f"Start Time: {datetime.fromtimestamp(timestamp, self.tz).strftime('%Y-%m-%d %H:%M:%S')}", # color=discord.Color.blue() # ) # if self.channel: # self.channel.send(embed=embed) self.scheduler.add_job( self.download_process, # args=(entry,waiting_minute,discord_notification,retry_count), kwargs={ "entry":entry, "discord_notification":True, "retry_count":20 }, trigger='date', run_date=datetime.fromtimestamp(timestamp), id=f"{entry['title']}_{entry['season']}_{entry['episode']}", replace_existing=True ) if len(entrys)<1: return self.scheduler.add_job( self.already_in_queue, # args=(entry,self.download_process), kwargs={ "entries":entrys, "func":self.download_process, # "retry_delay":15*60 if entry['status'] is not today_queue_status.failed_download else 0, "max_retry":20 #if entry['status'] is not today_queue_status.failed_download else 0 }, trigger='date', run_date=datetime.fromtimestamp(datetime.now(self.tz).timestamp()+60), id="Already Aired", replace_existing=True ) async def daily_job(self,is_clear_queue:bool=False,overwrite:bool=True) -> discord.Embed: self.db.add_today_queue([],True) if is_clear_queue else None # self.scheduler.shutdown(wait=False) # self.scheduler.remove_all_jobs() embed = discord.Embed( title="Daily Job", description="Starting daily job to fetch today's schedule and add it to the queue.", color=discord.Color.blue() ) today_schedule = self.db.get_today_schedule() today_queue_db=self.db.get_today_queue() for entry in today_schedule: if not (any(entry['title'] == x['title'] for x in today_queue_db) and any(entry['episode'] == x['episode'] for x in today_queue_db) and any(entry['season'] == x['season'] for x in today_queue_db)): self.db.add_today_queue([entry],False) self.db.add_download_history([entry],False) self.console.log(f"Adding season {entry['season']} episode {entry['episode']} of {entry['title']} to queue", is_gotify=False) embed.add_field( name=f"Adding season {entry['season']} episode {entry['episode']} of {entry['title']} to queue", value=f"Air time: {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%Y-%m-%d %H:%M:%S')}", inline=False ) # embed.add_field( # name="Timestamp", # value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), # inline=False # ) else: # if self.get_download_status(entry['title'], entry['season']) == 'completed': # if any(x["status"] =='completed' or 'import' in x["status"] for x in self.get_download_status(entry['title'], entry['season'])): if self.db.get_download_status(entry['title'], entry['season'], entry['episode']) in ['completed','uploaded']: self.console.log(f"Episode {entry['episode']} season {entry['season']} of {entry['title']} already completed, skipping", is_gotify=False) embed.add_field( name=f"Episode {entry['episode']} season {entry['season']} of {entry['title']} already completed, skipping", value=f"Air time: {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%Y-%m-%d %H:%M:%S')}", inline=False ) # embed.add_field( # name="Timestamp", # value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), # inline=False # ) else: self.console.warn(f"Already in queue but not complete. Adding season {entry['season']} episode {entry['episode']} of {entry['title']} to queue", is_gotify=False) embed.add_field( name=f"Already in queue but not complete. Adding season {entry['season']} episode {entry['episode']} of {entry['title']} to queue", value=f"Air time: {datetime.fromtimestamp(entry['start_timestamp'], self.tz).strftime('%Y-%m-%d %H:%M:%S')}", inline=False ) # embed.add_field( # name="Timestamp", # value=datetime.now(self.tz).strftime('%Y-%m-%d %H:%M:%S'), # inline=False # ) self.db.add_today_queue([entry],False,True) self.db.add_download_history([entry],False) # if self.channel: # await self.channel.send(embed=embed) self.add_shows_jobs() if self.scheduler.state != 1: # If the scheduler is not running self.scheduler.start() self.console.log("Daily job completed", is_gotify=False, is_discord={"channel": self.channel,"embed": embed} if self.channel else {}) return embed async def recheck_day(self,weekday:int,interaction: discord.Interaction) -> None: weekday_map = { 'Monday': 1, 'Tuesday': 2, 'Wednesday': 3, 'Thursday': 4, 'Friday': 5, 'Saturday': 6, 'Sunday': 7, 'All':8 } self.channel = self.get_channel(interaction.channel_id) # watchlist=self.db.get_watchlist() recheck_list=[] if weekday != 8: for entry in self.db.get_show_by_date(weekday): result = self.db.find_title_config_db(entry['title']) if entry['last_ep'] >= entry['end_ep']: continue for dow in entry['day_of_week'].split(','): if weekday != weekday_map[dow.strip()]: continue timestamp = int(datetime.now().replace(hour=int(entry['air_time'][:2]), minute=int(entry['air_time'][2:]), second=0, microsecond=0).timestamp()) for i in range(entry['multi_release']): detail ={ "title": result['Title'], "season": int(result['season']) if isinstance(result['season'], int) else 1, "episode": ((int(entry['last_ep'])) if entry['last_ep'] is not None else 0) +i+1, "sonarr_id": result['ID'], "air_time": entry['air_time'], "day_of_week": entry['day_of_week'], "offset": entry['offset'], "start_timestamp":timestamp + (60*(i*5)) } # print(detail) recheck_list.append(detail) else: for k,v in weekday_map.items(): if k == "All": continue for entry in self.db.get_show_by_date(v): result = self.db.find_title_config_db(entry['title']) if entry['last_ep'] >= entry['end_ep']: continue for dow in entry['day_of_week'].split(','): if v != weekday_map[dow.strip()]: continue timestamp = int(datetime.now().replace(hour=int(entry['air_time'][:2]), minute=int(entry['air_time'][2:]), second=0, microsecond=0).timestamp()) for i in range(entry['multi_release']): detail ={ "title": result['Title'], "season": int(result['season']) if isinstance(result['season'], int) else 1, "episode": ((int(entry['last_ep'])) if entry['last_ep'] is not None else 0) +i+1, "sonarr_id": result['ID'], "air_time": entry['air_time'], "day_of_week": entry['day_of_week'], "offset": entry['offset'], "start_timestamp":timestamp + (60*(i*5)) } # print(detail) recheck_list.append(detail) if len(recheck_list)<1: return async def recheck_queue(entrys:list[dict]) : for entry in entrys: await self.download_process(entry,0,discord_notification=True,retry_count=0) # self.scheduler.add_job( # recheck_queue, # args=(recheck_list,), # trigger='date', # run_date=datetime.fromtimestamp(datetime.now(self.tz).timestamp()+60), # id=f"Recheck_{weekday_map[weekday]}", # replace_existing=True # ) # if self.scheduler.state != 1: # If the scheduler is not running # self.scheduler.start() await recheck_queue(recheck_list) async def recheck_Title(self,title:str,interaction: discord.Interaction,season=None,episode=None) -> None: self.channel = self.get_channel(interaction.channel_id) # watchlist=self.db.get_watchlist() recheck_list=[] for entry in self.db.get_show_by_title(title): result = self.db.find_title_config_db(entry['title']) if entry['last_ep'] > entry['end_ep']: continue timestamp = int(datetime.now().replace(hour=int(entry['air_time'][:2]), minute=int(entry['air_time'][2:]), second=0, microsecond=0).timestamp()) for i in range(entry['multi_release']): detail ={ "title": result['Title'], "season": season or int(result['season']) if isinstance(result['season'], int) else 1, "episode": (episode or 0)+i or (((int(entry['last_ep'])) if entry['last_ep'] is not None else 0) +i+1), "sonarr_id": result['ID'], "air_time": entry['air_time'], "day_of_week": entry['day_of_week'], "offset": entry['offset'], "start_timestamp":timestamp + (60*(i*5)) } recheck_list.append(detail) if len(recheck_list)<1: return async def recheck_queue(entrys:list[dict]) : for entry in entrys: await self.download_process(entry,0,discord_notification=True,retry_count=0) # self.scheduler.add_job( # recheck_queue, # args=(recheck_list,), # trigger='date', # run_date=datetime.fromtimestamp(datetime.now(self.tz).timestamp()+60), # id=f"Recheck_{weekday_map[weekday]}", # replace_existing=True # ) # if self.scheduler.state != 1: # If the scheduler is not running # self.scheduler.start() await recheck_queue(recheck_list) async def set_today_schedule(self,interaction: discord.Interaction) -> None: # tomorrow = datetime.now(self.tz) + timedelta(days=1) # tomorrow = tomorrow.strftime('%Y_%m_%d') # hour_now = int(datetime.now(self.tz).strftime('%H')) minute_now = int(datetime.now(self.tz).strftime('%M')) self.channel = self.get_channel(interaction.channel_id) await self.daily_job(False) # if self.channel: # await self.channel.send(embed=embed) self.scheduler.add_job( self.daily_job,args=(True,),trigger='cron', hour=0, minute=0 if not minute_now > 59 else 0, id="daily_midnight_job", replace_existing=True) # embed = discord.Embed( # title="Today's Schedule", # description="Today's schedule has been set and the download jobs have been added.", # color=discord.Color.green() # ) # for job in self.scheduler.get_jobs(): # if job.id == 'today_queue': # continue # embed.add_field(name=job.id, value=f"Scheduled for {job.next_run_time.astimezone(self.tz).strftime('%Y-%m-%d %H:%M:%S')}", inline=False) # channel = bot.get_channel(interaction.channel_id) # await channel.send(embed=embed) if __name__ == "__main__":# # import time import logging # current_epoch_time = int(time.time()) console = logger(app_name="scheduler",log_dir="./log",discord_config=os.getenv('DISCORD_CHANNEL_ID'),level=logging.DEBUG) # console.log("Starting VT Schedule Bot...") usk_scheduler = USK(console) console.client=usk_scheduler async def main(): Entry= {'title': 'Fire Force', 'season': 3, 'episode': 25, 'sonarr_id': 1326, 'air_time': '1235', 'day_of_week': 'Saturday', 'offset': 0, 'start_timestamp': 1774762500} # Entry= {'title': 'Fate/strange Fake', 'season': 1, 'episode': 14, 'sonarr_id': 1107, 'air_time': '2330', 'day_of_week': 'Saturday', 'offset': 0, 'start_timestamp': 1774801800} # Entry={'title': 'Dead Account', 'season': 1, 'episode': 13, 'sonarr_id': 1078, 'air_time': '2200', 'day_of_week': 'Saturday', 'offset': 0, 'start_timestamp': 1774796400} await usk_scheduler.download_process(Entry,discord_notification=True) asyncio.run(main())