Files
usk_schedule_downloader/lib/usk.py

1213 lines
58 KiB
Python

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']))]
if title_config['Service'] in ['HMAX']:
cmd += ["--cdm-only"]
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",super_seed=False,seedingTimeLimit=10080)
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())