Skip to content

Commit dfb76d1

Browse files
authored
feat: delete tg post video & update FinalRip & store relative path (#10)
* feat: tg post * feat: new api * feat: json store use relative path * refactor: .....
1 parent cb13ab4 commit dfb76d1

22 files changed

+102
-168
lines changed

.gitignore

+2-3
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ cython_debug/
166166

167167
/deploy/docker/
168168

169-
/tests/task.json
170-
/task.json
171-
/conf/store.json
172169
/store.json
170+
/conf/store.json
171+
/tests/store.json

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ or you can use docker to run the project, see [docker-compose.yml](./deploy/dock
3939

4040
#### RSS Config:
4141

42-
supports hot reloading, which means you can update your config without needing to restart the service.
42+
supports hot reloading, which means you can update your config without needing to restart the service.
4343

4444
you should provide the compatible params and scripts in the [params](./conf/params) and [scripts](./conf/scripts) folder.
4545

animepipeline/bt/qb.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def get_downloaded_path(self, torrent_hash: str) -> Optional[Path]:
7878
if torrent[0].state in self.COMPLETE_STATES:
7979
file_list: List[Tuple[str, int]] = [(file["name"], file["size"]) for file in torrent[0].files]
8080
file_list.sort(key=lambda x: x[1], reverse=True)
81-
return self.download_path / Path(file_list[0][0])
81+
return Path(file_list[0][0])
8282

8383
else:
8484
return None

animepipeline/config/rss.py

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class BaseConfig(BaseModel):
1010
uploader: str
1111
script: str
1212
param: str
13+
slice: Optional[bool] = True
14+
timeout: Optional[int] = 20
1315

1416

1517
class NyaaConfig(BaseModel):
@@ -21,6 +23,8 @@ class NyaaConfig(BaseModel):
2123
uploader: Optional[str] = None
2224
script: Optional[str] = None
2325
param: Optional[str] = None
26+
slice: Optional[bool] = None
27+
timeout: Optional[int] = None
2428

2529

2630
class RSSConfig(BaseModel):
@@ -108,6 +112,12 @@ def _gen_dict(folder_path: Union[Path, str]) -> Dict[str, str]:
108112
if item.uploader is None:
109113
item.uploader = config.base.uploader
110114

115+
if item.slice is None:
116+
item.slice = config.base.slice
117+
118+
if item.timeout is None:
119+
item.timeout = config.base.timeout
120+
111121
if item.script is None:
112122
item.script = config.base.script
113123
else:

animepipeline/config/server.py

-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class FinalRipConfig(BaseModel):
2525

2626
class TelegramConfig(BaseModel):
2727
enable: bool
28-
local_mode: bool
29-
base_url: AnyUrl
30-
base_file_url: AnyUrl
3128
bot_token: str
3229
channel_id: Union[str, int]
3330

animepipeline/encode/finalrip.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import mimetypes
44
import time
55
from pathlib import Path
6-
from typing import Union
6+
from typing import Optional, Union
77

88
import httpx
99
from httpx import AsyncClient
@@ -30,7 +30,7 @@
3030

3131
class FinalRipClient:
3232
def __init__(self, config: FinalRipConfig):
33-
self.client = AsyncClient(base_url=str(config.url), headers={"token": str(config.token)})
33+
self.client = AsyncClient(base_url=str(config.url), headers={"token": str(config.token)}, timeout=30)
3434

3535
async def ping(self) -> PingResponse:
3636
try:
@@ -184,15 +184,23 @@ def _upload_file() -> None:
184184
if not new_task_response.success:
185185
logger.error(f"Error creating task: {new_task_response.error.message}") # type: ignore
186186

187-
async def start_task(self, video_key: str, encode_param: str, script: str) -> None:
187+
async def start_task(
188+
self, video_key: str, encode_param: str, script: str, slice: Optional[bool] = True, timeout: Optional[int] = 20
189+
) -> None:
188190
"""
189191
start encode task
190192
191193
:param video_key: video_key of the task
192194
:param encode_param: encode param
193195
:param script: encode script
196+
:param slice: cut video into clips or not
197+
:param timeout: clip timeout, default 20 minutes
194198
"""
195-
resp = await self._start_task(StartTaskRequest(video_key=video_key, encode_param=encode_param, script=script))
199+
resp = await self._start_task(
200+
StartTaskRequest(
201+
video_key=video_key, encode_param=encode_param, script=script, slice=slice, timeout=timeout
202+
)
203+
)
196204
if not resp.success:
197205
logger.warning(f"Failed to start finalrip task: {resp.error.message}") # type: ignore
198206

animepipeline/encode/type.py

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class StartTaskRequest(BaseModel):
3535
encode_param: str
3636
script: str
3737
video_key: str
38+
slice: Optional[bool] = None
39+
timeout: Optional[int] = None
3840

3941

4042
class StartTaskResponse(BaseModel):

animepipeline/loop.py

+41-39
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import asyncio
22
from pathlib import Path
3-
from typing import Any, Callable, Coroutine, List
3+
from typing import Any, Callable, Coroutine, List, Optional
44

55
from loguru import logger
6+
from pydantic import DirectoryPath
67

78
from animepipeline.bt import QBittorrentManager
89
from animepipeline.config import NyaaConfig, RSSConfig, ServerConfig
@@ -15,33 +16,42 @@
1516

1617

1718
class TaskInfo(TorrentInfo):
19+
download_path: DirectoryPath
1820
uploader: str
19-
script: str
20-
param: str
21+
script_content: str
22+
param_content: str
23+
slice: Optional[bool] = True
24+
timeout: Optional[int] = 20
2125

2226

23-
def build_task_info(torrent_info: TorrentInfo, nyaa_config: NyaaConfig, rss_config: RSSConfig) -> TaskInfo:
27+
def build_task_info(
28+
torrent_info: TorrentInfo, nyaa_config: NyaaConfig, rss_config: RSSConfig, server_config: ServerConfig
29+
) -> TaskInfo:
2430
"""
2531
Build TaskInfo from TorrentInfo, NyaaConfig and RSSConfig
2632
2733
:param torrent_info: TorrentInfo
2834
:param nyaa_config: NyaaConfig
2935
:param rss_config: RSSConfig
36+
:param server_config: ServerConfig
3037
:return: TaskInfo
3138
"""
3239
if nyaa_config.script not in rss_config.scripts:
3340
raise ValueError(f"script not found: {nyaa_config.script}")
3441
if nyaa_config.param not in rss_config.params:
3542
raise ValueError(f"param not found: {nyaa_config.param}")
3643

37-
script = rss_config.scripts[nyaa_config.script]
38-
param = rss_config.params[nyaa_config.param]
44+
script_content = rss_config.scripts[nyaa_config.script]
45+
param_content = rss_config.params[nyaa_config.param]
3946

4047
return TaskInfo(
4148
**torrent_info.model_dump(),
49+
download_path=server_config.qbittorrent.download_path,
4250
uploader=nyaa_config.uploader,
43-
script=script,
44-
param=param,
51+
script_content=script_content,
52+
param_content=param_content,
53+
slice=nyaa_config.slice,
54+
timeout=nyaa_config.timeout,
4555
)
4656

4757

@@ -94,7 +104,12 @@ async def start(self) -> None:
94104
torrent_info_list = parse_nyaa(cfg)
95105

96106
for torrent_info in torrent_info_list:
97-
task_info = build_task_info(torrent_info, cfg, self.rss_config)
107+
task_info = build_task_info(
108+
torrent_info=torrent_info,
109+
nyaa_config=cfg,
110+
rss_config=self.rss_config,
111+
server_config=self.server_config,
112+
)
98113

99114
await self.task_executor.submit_task(torrent_info.hash, self.pipeline, task_info)
100115

@@ -107,7 +122,7 @@ def add_pipeline_task(self) -> None:
107122
"""
108123
self.pipeline_tasks.append(self.pipeline_bt)
109124
self.pipeline_tasks.append(self.pipeline_finalrip)
110-
self.pipeline_tasks.append(self.pipeline_tg)
125+
self.pipeline_tasks.append(self.pipeline_post)
111126

112127
async def pipeline(self, task_info: TaskInfo) -> None:
113128
# init task status
@@ -168,7 +183,7 @@ async def pipeline_finalrip(self, task_info: TaskInfo) -> None:
168183
logger.info(f'Start FinalRip Encode for "{task_info.name}" EP {task_info.episode}')
169184
# start finalrip task
170185

171-
bt_downloaded_path = Path(task_status.bt_downloaded_path)
186+
bt_downloaded_path = Path(task_info.download_path) / task_status.bt_downloaded_path
172187

173188
while not await self.finalrip_client.check_task_exist(bt_downloaded_path.name):
174189
try:
@@ -181,33 +196,21 @@ async def pipeline_finalrip(self, task_info: TaskInfo) -> None:
181196
try:
182197
await self.finalrip_client.start_task(
183198
video_key=bt_downloaded_path.name,
184-
encode_param=task_info.param,
185-
script=task_info.script,
199+
encode_param=task_info.param_content,
200+
script=task_info.script_content,
201+
slice=task_info.slice,
202+
timeout=task_info.timeout,
186203
)
187204
logger.info(f'FinalRip Task Started for "{task_info.name}" EP {task_info.episode}')
188205
except Exception as e:
189206
logger.error(f"Failed to start finalrip task: {e}")
190207

191208
# wait video cut done
192-
await asyncio.sleep(10)
209+
await asyncio.sleep(30)
193210

194211
# check task progress
195212
while not await self.finalrip_client.check_task_completed(bt_downloaded_path.name):
196-
# retry merge if all clips are done but merge failed?
197-
if await self.finalrip_client.check_task_all_clips_done(bt_downloaded_path.name):
198-
# wait 30s before retry merge
199-
await asyncio.sleep(30)
200-
# check again
201-
if await self.finalrip_client.check_task_completed(bt_downloaded_path.name):
202-
break
203-
204-
try:
205-
await self.finalrip_client.retry_merge(bt_downloaded_path.name)
206-
logger.info(f'Retry Merge Clips for "{task_info.name}" EP {task_info.episode}')
207-
except Exception as e:
208-
logger.error(f'Failed to retry merge clips for "{task_info.name}" EP {task_info.episode}: {e}')
209-
210-
await asyncio.sleep(10)
213+
await asyncio.sleep(30)
211214

212215
# download temp file to bt_downloaded_path's parent directory
213216
temp_saved_path: Path = bt_downloaded_path.parent / (bt_downloaded_path.name + "-encoded.mkv")
@@ -220,7 +223,7 @@ async def pipeline_finalrip(self, task_info: TaskInfo) -> None:
220223
path=temp_saved_path, episode=task_info.episode, name=task_info.name, uploader=task_info.uploader
221224
)
222225
)
223-
finalrip_downloaded_path = bt_downloaded_path.parent / gen_name
226+
finalrip_downloaded_path = Path(task_info.download_path) / gen_name
224227
except Exception as e:
225228
logger.error(f"Failed to generate file name: {e}")
226229
raise e
@@ -234,32 +237,31 @@ async def pipeline_finalrip(self, task_info: TaskInfo) -> None:
234237
logger.info(f'FinalRip Encode Done for "{finalrip_downloaded_path.name}"')
235238

236239
# update task status
237-
task_status.finalrip_downloaded_path = str(finalrip_downloaded_path)
240+
task_status.finalrip_downloaded_path = gen_name
238241
await self.json_store.update_task(task_info.hash, task_status)
239242

240-
async def pipeline_tg(self, task_info: TaskInfo) -> None:
243+
async def pipeline_post(self, task_info: TaskInfo) -> None:
241244
task_status = await self.json_store.get_task(task_info.hash)
242245

243246
if self.tg_channel_sender is None:
244247
logger.info("Telegram Channel Sender is not enabled. Skip upload.")
245248
return
246249

247250
# check tg
248-
if task_status.tg_uploaded:
251+
if task_status.tg_posted:
249252
return
250253

251254
if task_status.finalrip_downloaded_path is None:
252255
logger.error("FinalRip download path is None! finalrip download task not finished?")
253256
raise ValueError("FinalRip download path is None! finalrip download task not finished?")
254257

255-
logger.info(f'Start Telegram Channel Upload for "{task_info.name}" EP {task_info.episode}')
258+
logger.info(f'Post to Telegram Channel for "{task_info.name}" EP {task_info.episode}')
256259

257-
finalrip_downloaded_path = Path(task_status.finalrip_downloaded_path)
260+
finalrip_downloaded_path = Path(task_info.download_path) / task_status.finalrip_downloaded_path
258261

259-
await self.tg_channel_sender.send_video(
260-
video_path=finalrip_downloaded_path,
261-
caption=f"{task_info.translation} | EP {task_info.episode} | {finalrip_downloaded_path.name}",
262+
await self.tg_channel_sender.send_text(
263+
text=f"{task_info.translation} | EP {task_info.episode} | {finalrip_downloaded_path.name}",
262264
)
263265

264-
task_status.tg_uploaded = True
266+
task_status.tg_posted = True
265267
await self.json_store.update_task(task_info.hash, task_status)

animepipeline/post/tg.py

+11-40
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from pathlib import Path
2-
from typing import Optional, Union
3-
4-
import telegram.error
1+
import telegram
52
from loguru import logger
63
from telegram import Bot
4+
from tenacity import retry, stop_after_attempt, wait_random
75

86
from animepipeline.config import TelegramConfig
97

@@ -16,47 +14,20 @@ class TGChannelSender:
1614
"""
1715

1816
def __init__(self, config: TelegramConfig) -> None:
19-
if config.local_mode:
20-
self.bot = Bot(
21-
token=config.bot_token,
22-
base_url=str(config.base_url),
23-
base_file_url=str(config.base_file_url),
24-
local_mode=True,
25-
)
26-
else:
27-
self.bot = Bot(token=config.bot_token)
28-
17+
self.bot = Bot(token=config.bot_token)
2918
self.channel_id = config.channel_id
3019

31-
async def send_video(self, video_path: Union[Path, str], caption: Optional[str] = None) -> None:
20+
@retry(wait=wait_random(min=3, max=5), stop=stop_after_attempt(5))
21+
async def send_text(self, text: str) -> None:
3222
"""
33-
Send video to the channel.
23+
Send text to the channel.
3424
35-
:param video_path:
36-
:param caption: the caption of the video
25+
:param text: The text to send.
3726
"""
38-
video_path = Path(video_path)
39-
video_name = video_path.name
40-
if not video_path.exists():
41-
raise FileNotFoundError(f"Video file not found: {video_path}")
42-
43-
if caption is None:
44-
caption = video_name
45-
46-
with open(video_path, "rb") as f:
47-
video_file = f.read()
48-
4927
try:
50-
await self.bot.send_video(
51-
chat_id=self.channel_id,
52-
video=video_file,
53-
filename=video_name,
54-
caption=caption,
55-
read_timeout=6000,
56-
write_timeout=6000,
57-
pool_timeout=6000,
58-
)
28+
await self.bot.send_message(chat_id=self.channel_id, text=text)
5929
except telegram.error.NetworkError as e:
60-
logger.error(f"Network error: {e}, video path: {video_path}, video_caption: {caption}")
30+
logger.error(f"Network error: {e}, text: {text}")
31+
raise e
6132
except Exception as e:
62-
logger.error(f"Unknown Error sending video: {e}, video path: {video_path}, video_caption: {caption}")
33+
logger.error(f"Unknown Error sending text: {e}, text: {text}")

animepipeline/store/task.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class TaskStatus(BaseModel):
1212
done: bool = False
1313
bt_downloaded_path: Optional[str] = None
1414
finalrip_downloaded_path: Optional[str] = None
15-
tg_uploaded: bool = False
15+
tg_posted: bool = False
1616
ex_status_dict: Optional[Dict[str, Any]] = None
1717

1818

conf/params/default.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ffmpeg -i - -vcodec libx265 -crf 16
1+
ffmpeg -i - -vcodec libx264 -preset ultrafast

0 commit comments

Comments
 (0)