Unit3DwebUp 0.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. config/__init__.py +4 -0
  2. config/api_data.py +28 -0
  3. config/constants.py +34 -0
  4. config/host_data.py +47 -0
  5. config/itt.py +154 -0
  6. config/logger.py +37 -0
  7. config/settings.py +212 -0
  8. config/sis.py +137 -0
  9. config/tags.py +395 -0
  10. config/trackers.py +47 -0
  11. external/__init__.py +0 -0
  12. external/async_http_client_service.py +48 -0
  13. external/websocket.py +41 -0
  14. models/__init__.py +0 -0
  15. models/interfaces.py +39 -0
  16. models/keywords.py +13 -0
  17. models/media.py +597 -0
  18. models/media_info.py +142 -0
  19. models/movie.py +287 -0
  20. models/tv.py +266 -0
  21. models/tvdb_search.py +102 -0
  22. models/videos.py +26 -0
  23. repositories/__init__.py +0 -0
  24. repositories/db_online.py +82 -0
  25. repositories/interfaces.py +59 -0
  26. repositories/job_repos.py +166 -0
  27. repositories/media_info_factory.py +28 -0
  28. services/__init__.py +0 -0
  29. services/auto_async_service.py +237 -0
  30. services/create_torrent_service.py +94 -0
  31. services/interfaces.py +72 -0
  32. services/itt_tracker_helper.py +463 -0
  33. services/itt_tracker_service.py +85 -0
  34. services/lifespan_service.py +58 -0
  35. services/media_service.py +114 -0
  36. services/tags_service.py +389 -0
  37. services/tmdb.py +246 -0
  38. services/torrent_client_service.py +92 -0
  39. services/torrent_service.py +107 -0
  40. services/tvdb.py +65 -0
  41. services/utility.py +433 -0
  42. services/video_service.py +356 -0
  43. unit3dwebup-0.0.14.dist-info/METADATA +191 -0
  44. unit3dwebup-0.0.14.dist-info/RECORD +53 -0
  45. unit3dwebup-0.0.14.dist-info/WHEEL +5 -0
  46. unit3dwebup-0.0.14.dist-info/entry_points.txt +2 -0
  47. unit3dwebup-0.0.14.dist-info/top_level.txt +6 -0
  48. use_case/__init__.py +0 -0
  49. use_case/make_torrent_usecase.py +67 -0
  50. use_case/process_all_usecase.py +43 -0
  51. use_case/scan_media_usecase.py +133 -0
  52. use_case/seed_usecase.py +117 -0
  53. use_case/upload_usecase.py +77 -0
@@ -0,0 +1,356 @@
1
+ # -*- coding: utf-8 -*-
2
+ import aiohttp
3
+ import base64
4
+ import os
5
+ import io
6
+
7
+ from pathlib import Path
8
+ from PIL import Image
9
+
10
+ from services.interfaces import VideoServiceInterface, DescriptionBuilderInterface
11
+ from config.host_data import upload_hosts, master_uploaders
12
+ from external.async_http_client_service import AsyncHttpClient
13
+ from config.constants import MediaStatus
14
+ from config.settings import get_settings
15
+ from config.logger import get_logger
16
+ from models.media import Media
17
+
18
+ from pymediainfo import MediaInfo
19
+ from aiohttp import FormData
20
+ from fastapi import FastAPI
21
+ import numpy as np
22
+ import asyncio
23
+
24
+
25
+
26
+ # Based on the old code unit3dup 0.8.21
27
+ class VideoFrame:
28
+ def __init__(self, video_path: str, num_screenshots: int, webp_filepath=None):
29
+ self.video_path = Path(video_path)
30
+ self.webp_filepath = Path(webp_filepath) if webp_filepath else None
31
+ self.num_screenshots = num_screenshots
32
+ self.logger = get_logger(self.__class__.__name__)
33
+
34
+ async def create(self):
35
+ """
36
+ return list of extract screenshots from the video
37
+ """
38
+ frames = await self._extract()
39
+ frames_in_bytes = []
40
+ is_hd = 0
41
+
42
+ for frame in frames:
43
+ img_bytes = self.image_to_bytes(frame)
44
+ if frame.height >= 720:
45
+ is_hd = 1
46
+ frames_in_bytes.append(img_bytes)
47
+
48
+ webp = []
49
+ if self.webp_filepath:
50
+ webp = await self.create_webp_from_video(video_path=self.video_path, output_path=self.webp_filepath)
51
+
52
+ return frames_in_bytes + (webp or []), is_hd
53
+
54
+ @staticmethod
55
+ def image_to_bytes(frame: Image.Image) -> bytes:
56
+ """
57
+ Convert frame bytes to Jpeg with no compression
58
+ """
59
+ buffered = io.BytesIO()
60
+ frame.save(buffered, format="JPEG", optimize=False, compress_level=4)
61
+ return buffered.getvalue()
62
+
63
+ async def _get_video_duration(self) -> float:
64
+ """
65
+ return the video duration
66
+
67
+ -show_entries format: Get video duration
68
+
69
+ -of default=noprint_wrappers=1:nokey=1: output formatted to get only the number ( duration value)
70
+ """
71
+ command = [
72
+ "ffprobe",
73
+ "-v", "error",
74
+ "-show_entries", "format=duration",
75
+ "-of", "default=noprint_wrappers=1:nokey=1",
76
+ str(self.video_path),
77
+ ]
78
+ proc = await asyncio.create_subprocess_exec(
79
+ *command,
80
+ stdout=asyncio.subprocess.PIPE,
81
+ stderr=asyncio.subprocess.PIPE
82
+ )
83
+ stdout, _ = await proc.communicate()
84
+ return float(stdout.decode().strip())
85
+
86
+ async def _extract(self):
87
+ """
88
+ FFPROBE
89
+ - "-v", show the errore only
90
+ - "-select_streams", "v:0", extract only the first stream
91
+ - "-show_entries", "stream=width,height", return only Height and Width value
92
+ - "-of", "default=nw=1:nk=1", print only the numeric value ( WxH value)
93
+
94
+ - FFMPEG
95
+ - "-ss", str(t), seek at time t(secondi)
96
+ - "-i", str(self.video_path), video file path
97
+ - "-vframes", "1", extract only one frame
98
+ - "-f", "rawvideo", output format rawvideo
99
+ - "-pix_fmt", "rgb24", 24-bit rgb - todo: problema con formato png?
100
+ - "-" : output to stdout
101
+
102
+ :return:
103
+ """
104
+
105
+ duration = await self._get_video_duration()
106
+ # todo: Hardcoded values
107
+ start_time = duration * 0.35
108
+ end_time = duration * 0.85
109
+
110
+ times = np.linspace(start_time, end_time, self.num_screenshots, endpoint=True)
111
+
112
+ command = [
113
+ "ffprobe",
114
+ "-v", "error",
115
+ "-select_streams", "v:0",
116
+ "-show_entries", "stream=width,height",
117
+ "-of", "default=nw=1:nk=1",
118
+ str(self.video_path)
119
+
120
+ ]
121
+ proc = await asyncio.create_subprocess_exec(
122
+ *command,
123
+ stdout=asyncio.subprocess.PIPE,
124
+ stderr=asyncio.subprocess.PIPE
125
+ )
126
+ stdout, _ = await proc.communicate()
127
+ w, h = stdout.decode().strip().splitlines()
128
+ width, height = int(w), int(h)
129
+
130
+ frames = []
131
+ for t in times:
132
+ command = [
133
+ "ffmpeg",
134
+ "-ss", str(t),
135
+ "-i", str(self.video_path),
136
+ "-vframes", "1",
137
+ "-f", "rawvideo",
138
+ "-pix_fmt", "rgb24", # ho avuto un problema con png
139
+ "-"
140
+ ]
141
+ proc = await asyncio.create_subprocess_exec(
142
+ *command,
143
+ stdout=asyncio.subprocess.PIPE,
144
+ stderr=asyncio.subprocess.DEVNULL
145
+ )
146
+ stdout, _ = await proc.communicate()
147
+ img = Image.fromarray(np.frombuffer(stdout, dtype=np.uint8).reshape((height, width, 3)))
148
+ frames.append(img)
149
+
150
+ return frames
151
+
152
+
153
+ async def create_webp_from_video(self, video_path, output_path):
154
+ """
155
+ - "-y", overwrite output if it exists
156
+ - "-ss", '70', start time at 70 seconds
157
+ - "-t", '10', extracts 10 seconds
158
+ - "-i", video_path, video file path
159
+ - "-vf", f"fps=7,scale=650:-1:flags=lanczos", set fps and scale to 650 ( aspect ratio ok)
160
+ - "-c:v", "libwebp", encode video as WebP
161
+ - "-quality", "50", quality
162
+ - "-loop", "0", repeat the animation
163
+ - "-speed", "9", encoding speed (1 = slowest, 10 = fastest)
164
+ - "-threads", "8", number of threads
165
+ - "-f", "webp", webp format
166
+ - "output_path" output file path
167
+ """
168
+ command = [
169
+ "ffmpeg",
170
+ "-y",
171
+ "-ss", '70',
172
+ "-t", '10',
173
+ "-i", video_path,
174
+ "-vf", f"fps=7,scale=650:-1:flags=lanczos",
175
+ "-c:v", "libwebp",
176
+ "-quality", "50",
177
+ "-loop", "0",
178
+ "-speed", "9",
179
+ "-threads", "8",
180
+ "-f", "webp",
181
+ output_path
182
+ ]
183
+ proc = await asyncio.create_subprocess_exec(
184
+ *command,
185
+ stdout=asyncio.subprocess.PIPE,
186
+ stderr=asyncio.subprocess.PIPE
187
+ )
188
+ await proc.communicate()
189
+
190
+ if not os.path.isfile(output_path):
191
+ self.logger.error(f"file {output_path} doesn't exist!")
192
+ return None
193
+
194
+ # todo: vorrei non dover scrivere su file ma in memoria ram
195
+ with open(output_path, "rb") as webp_file:
196
+ webp_file_content = webp_file.read()
197
+ os.remove(output_path)
198
+ return [webp_file_content]
199
+
200
+
201
+ class VideoService(VideoServiceInterface):
202
+ """ Build a description for the torrent page: screenshots, mediainfo, trailers, metadata """
203
+
204
+ def __init__(self, media: Media):
205
+ """
206
+ :param media: The Media object
207
+ """
208
+
209
+ self.media = media
210
+ self.file_name: str = media.file_name
211
+ self.display_name: str = media.display_name
212
+ self.webp_filepath: str | None = None
213
+ settings = get_settings()
214
+
215
+ if settings.prefs.WEBP_ENABLED:
216
+ self.webp_filepath = f"{media.display_name}.webp"
217
+
218
+ # Load the video frames
219
+ # if web_enabled is off set the number of screenshots to an even number
220
+ if not settings.prefs.WEBP_ENABLED:
221
+ if settings.prefs.NUMBER_OF_SCREENSHOTS % 2 != 0:
222
+ settings.prefs.NUMBER_OF_SCREENSHOTS += 1
223
+
224
+ samples_n = max(2, min(settings.prefs.NUMBER_OF_SCREENSHOTS, 10))
225
+
226
+ self.video_frames: VideoFrame = VideoFrame(video_path=self.file_name,
227
+ num_screenshots=samples_n, webp_filepath=self.webp_filepath)
228
+
229
+ async def generate(self):
230
+ """Build the information to send to the tracker"""
231
+
232
+ # Extract the frames
233
+ self.media.extracted_frames, self.media.is_hd = await self.video_frames.create()
234
+ # Add Media description
235
+ self.media.media_to_string = await asyncio.to_thread(MediaInfo.parse, self.media.file_name,
236
+ output="STRING", full=False)
237
+ return None
238
+
239
+
240
+ class BuildService(DescriptionBuilderInterface):
241
+ """
242
+ Upload all previously created screenshots to hostimage, get their URLs, build a description for the tracker
243
+ """
244
+
245
+ def __init__(self, media_list: list[Media], app: FastAPI, session: aiohttp.ClientSession | None = None):
246
+ """
247
+ :param media_list: a list of Media objects
248
+ :param session: aiohttp.ClientSession | None
249
+ """
250
+ self.session = session or aiohttp.ClientSession()
251
+ self.http = AsyncHttpClient(self.session)
252
+ self.media_list = media_list
253
+ self.app = app
254
+ self.screenshots = []
255
+ self.logger = get_logger(self.__class__.__name__)
256
+ settings = get_settings()
257
+ self.sign = (
258
+ f"[url=https://github.com/31December99/Unit3DWebUp][code][color=#00BFFF][size=14]Uploaded with Unit3DwebUp"
259
+ f" {settings.unit3DwebUp.VERSION}[/size][/color][/code][/url]")
260
+
261
+ async def description(self):
262
+
263
+ settings = get_settings()
264
+ priority_map = {
265
+ 'ImgBB': settings.prefs.IMGBB_PRIORITY,
266
+ 'PtScreens': settings.prefs.PTSCREENS_PRIORITY,
267
+ 'LensDump': settings.prefs.LENSDUMP_PRIORITY,
268
+ 'ImgFi': settings.prefs.IMGFI_PRIORITY,
269
+ 'PassIMA': settings.prefs.PASSIMA_PRIORITY,
270
+ 'ImaRide': settings.prefs.IMARIDE_PRIORITY,
271
+ 'Freeimage': settings.prefs.FREE_IMAGE_PRIORITY,
272
+ }
273
+
274
+ user_order = sorted(master_uploaders, key=lambda uploader: priority_map[uploader])
275
+
276
+ async def upload_frame(media: Media, index: int, image_bytes: bytes) -> str:
277
+ """
278
+ :param media: the Media object
279
+ :param index: enumeration for the screenshot
280
+ :param image_bytes: the image data
281
+ :return:
282
+
283
+ Upload single frame
284
+ """
285
+
286
+ image_name = f"{media.display_name}.id_{index}"
287
+ image_base64 = base64.b64encode(image_bytes).decode('utf-8')
288
+ timeout = 30
289
+
290
+ host = upload_hosts[user_order[0]]
291
+ form = FormData()
292
+ form.add_field('key', host['data']['key'])
293
+
294
+ # passtheima host
295
+ if host.get('data', {}).get('fieldname', None):
296
+ form.add_field(host['data']['fieldname'], image_name)
297
+ form.add_field('image', image_base64)
298
+
299
+ try:
300
+ response = await self.session.post(host['url'], data=form, timeout=timeout)
301
+ response.raise_for_status()
302
+ result = await response.json()
303
+ if result.get('data', None):
304
+ img_url = result["data"]["image"]["url"]
305
+ else:
306
+ img_url = result["image"]["url"]
307
+ self.logger.info(f"[OK] Uploaded {img_url}")
308
+ self.screenshots.append(img_url)
309
+ media.status = MediaStatus.DESCRIPTION_READY
310
+ return f"[url={img_url}][img=650]{img_url}[/img][/url]"
311
+ except Exception as e:
312
+ self.logger.error(f"[ERROR] Upload frame {image_name}: {e}")
313
+ media.status = MediaStatus.DESCRIPTION_ERROR
314
+ return ""
315
+
316
+ async def upload_media(media: Media) -> str | None:
317
+ """
318
+ :param media: the Media object
319
+ :return:
320
+
321
+ upload media screenshot to hostimage
322
+ """
323
+ tasks = [upload_frame(media, idx, img_bytes) for idx, img_bytes in enumerate(media.extracted_frames)]
324
+ uploaded_frames = await asyncio.gather(*tasks)
325
+
326
+ media.screen_shots = self.screenshots
327
+ if media.status == MediaStatus.DESCRIPTION_READY:
328
+ description = "[center]\n" + "".join(uploaded_frames)
329
+ if media.trailer:
330
+ description += (
331
+ f"\n[/center][b][spoiler=Spoiler:"
332
+ f" PLAY TRAILER][center][youtube]{media.trailer}[/youtube]"
333
+ f"[/center][/spoiler][/b]")
334
+ description+=self.sign
335
+ return description
336
+ return None
337
+
338
+ # Upload
339
+ all_descriptions = await asyncio.gather(*[upload_media(media) for media in self.media_list])
340
+ # Add description
341
+ for media, desc in zip(self.media_list, all_descriptions):
342
+ if media.status == MediaStatus.DESCRIPTION_READY:
343
+ media.description = desc
344
+ else:
345
+ self.logger.warning("Description error")
346
+ # Send a message to the frontend by ws
347
+ await self.app.state.ws_manager.broadcast({
348
+ "type": "log",
349
+ "level": "error",
350
+ "message": f"Failed to build description for {media.file_name}",
351
+ })
352
+
353
+ return None
354
+
355
+ async def close(self) -> None:
356
+ await self.session.close()
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: Unit3DwebUp
3
+ Version: 0.0.14
4
+ Summary: A Unit3D uploader with a web interface
5
+ Author: Parzival
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/31December99/Unit3DupWeb
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: aiohttp==3.13.3
11
+ Requires-Dist: fastapi==0.129.0
12
+ Requires-Dist: guessit==3.8.0
13
+ Requires-Dist: numpy==2.4.2
14
+ Requires-Dist: pathvalidate==3.3.1
15
+ Requires-Dist: pillow==12.1.1
16
+ Requires-Dist: pydantic==2.12.5
17
+ Requires-Dist: pydantic_core==2.41.5
18
+ Requires-Dist: pymediainfo==7.0.1
19
+ Requires-Dist: pydantic-settings==2.12.0
20
+ Requires-Dist: qbittorrent-api==2025.11.1
21
+ Requires-Dist: python-dotenv==1.2.1
22
+ Requires-Dist: redis==7.1.1
23
+ Requires-Dist: thefuzz==0.22.1
24
+ Requires-Dist: torf==4.3.1
25
+ Requires-Dist: watchdog==6.0.0
26
+ Requires-Dist: uvicorn[standard]==0.40.0
27
+
28
+ # Unit3DupWeb
29
+
30
+ ![version](https://img.shields.io/pypi/v/unit3dupWeb.svg)
31
+ ![online](https://img.shields.io/badge/Online-green)
32
+ ![status](https://img.shields.io/badge/Status-Active-brightgreen)
33
+ ![python](https://img.shields.io/badge/Python-3.12+-blue)
34
+ ![ubuntu](https://img.shields.io/badge/Ubuntu-22+-blue)
35
+ ![debian](https://img.shields.io/badge/Debian-12+-blue)
36
+ ![windows](https://img.shields.io/badge/Windows-11-blue)
37
+
38
+ ---
39
+
40
+ # Auto Torrent Generator and Uploader
41
+
42
+ Reworked from the original [Unit3Dup](https://github.com/31December99/Unit3Dup)
43
+
44
+ This code is under testing.
45
+
46
+ ![Home](images/home.png)
47
+
48
+ ---
49
+
50
+ ## Demo video
51
+
52
+ [Here](https://streamable.com/agoizj)
53
+
54
+ ---
55
+
56
+ ## What it does
57
+
58
+ * Scan folder and subfolders
59
+ * Compiles metadata to create a torrent
60
+ * Extracts screenshots from video
61
+ * Adds webp images to torrent page description
62
+ * Searches IDs on TMDB, IMDb, TVDB
63
+ * Adds trailer from TMDB or YouTube
64
+ * Seeds in qBittorrent
65
+ * Generates metadata from video
66
+ * Creates and uploads torrents/pages
67
+
68
+ ---
69
+
70
+ ## NOT YET TESTED
71
+
72
+ * Extracts cover from PDF documents
73
+ * Reseeding multiple torrents
74
+ * Cross-OS seeding
75
+ * Custom season titles
76
+ * MediaInfo-based metadata generation
77
+ * Extract first page of PDFs via xpdf and upload it
78
+ * Windows support improvements
79
+
80
+ ---
81
+
82
+ ## NOT YET IMPLEMENTED
83
+
84
+ * Game metadata generation
85
+ * Transmission / rTorrent seeding
86
+
87
+ ---
88
+
89
+ ## Install from Docker Hub
90
+
91
+ * Complete `.env.example`
92
+ * Rename to `.env`
93
+ * Run:
94
+
95
+ ```bash
96
+ docker-compose pull
97
+ ```
98
+
99
+ ---
100
+
101
+ ## How it works
102
+
103
+ The backend provides FastAPI endpoints.
104
+
105
+ For each video file, a job_id is generated from the hash of its path.
106
+
107
+ * job_ids form a job_list (page view)
108
+ * each page has a job_list_id based on scan path hash
109
+
110
+ WebSocket is used for:
111
+
112
+ * progress updates
113
+ * logs to frontend
114
+
115
+ ### Scan process
116
+
117
+ * Search files/folders
118
+ * Extract title
119
+ * Query TMDB (movie/series)
120
+ * Query TVDB and get IMDb ID
121
+ * Create screenshots
122
+ * Build description with MediaInfo + screenshots
123
+
124
+ ### Editing
125
+
126
+ If poster has issues (TMDB/IMDb mismatch):
127
+
128
+ * click poster
129
+ * edit fields
130
+ * create torrent / upload / seed
131
+
132
+ ---
133
+
134
+ ## Frontend build
135
+
136
+ ```bash
137
+ flutter pub get
138
+ flutter build web --release --wasm
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Backend build
144
+
145
+ ```bash
146
+ docker-compose -f build.yml build --no-cache
147
+ docker-compose up
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Docker Hub
153
+
154
+ ```bash
155
+ docker login
156
+
157
+ docker tag unit3dwebup-backend:latest parzival2025/backend_app:x.y.z
158
+ docker tag unit3dwebup-frontend:latest parzival2025/frontend_app:x.y.z
159
+
160
+ docker push parzival2025/backend_app:x.y.z
161
+ docker push parzival2025/frontend_app:x.y.z
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Trackers
167
+
168
+ The Italian tracker community: people with technical and social backgrounds
169
+ united by torrents.
170
+
171
+ | Tracker | Description |
172
+ | ------- | ---------------------------------------------------- |
173
+ | ITT | [https://itatorrents.xyz/](https://itatorrents.xyz/) |
174
+
175
+ ---
176
+
177
+ ## Discord
178
+
179
+ [Join Discord](https://discord.gg/8RpwN2Khcz)
180
+
181
+ ![Discord](https://img.shields.io/discord/1214696147600408698?label=Discord\&logo=discord\&style=flat)
182
+
183
+ ---
184
+
185
+ ## AstraeLabs
186
+
187
+ [GitHub Project](https://github.com/AstraeLabs/VibraVid)
188
+
189
+ Open-source script for downloading movies, TV shows, and anime.
190
+
191
+ ![AstraeLabs](https://img.shields.io/badge/AstraeLabs-blue.svg)
@@ -0,0 +1,53 @@
1
+ config/__init__.py,sha256=38D2YRz9kUehElDnmAjshs4IQNdI9Ueop1ze-lU8wxQ,114
2
+ config/api_data.py,sha256=WPC334sgcl25sqCiLEm61VKG9Hq4jH6QOxykEtjbuS8,822
3
+ config/constants.py,sha256=EginqIQt_TJh-BDvbddCqPN9HW83JfniOHAOkALA-Qc,773
4
+ config/host_data.py,sha256=H3tIMPbKCtbw-wYOsOr1Tg6ZfvD8ZfJq7c5NtahrDSU,1613
5
+ config/itt.py,sha256=V2tUAqE1F8H9CUcb8-44xCefohdRqltrzW9VUAn5HRI,3317
6
+ config/logger.py,sha256=lucl0ySM6fSchu-YMalYTCrgDzZTLYQhQwYUwfO9Uss,1025
7
+ config/settings.py,sha256=-Dm52cukiE7i_oXI-RNVGKvqrgP8zQNJZsefyxhYkYY,6628
8
+ config/sis.py,sha256=_hp5Z6kLc7QLkm5o0NyKp0lZ1L4l3jYK4xTDNSAJqoI,2562
9
+ config/tags.py,sha256=fF5vTugAMQCNpqcIFLGgNd934ZQKWPRDeNDvUfozKQE,11360
10
+ config/trackers.py,sha256=1jZOt4WfYIbP53F5S9kLVCsGDaPmkwBAjUSKILA4fVI,1458
11
+ external/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ external/async_http_client_service.py,sha256=OCrqLzLyqZW4T1PdEPs6W7np9eG5XgttogBaJGSIRL0,1465
13
+ external/websocket.py,sha256=LP_bPb3hnZbR1XtC-k9BVTELnyc0iXyGh0oGZxHq3mY,1082
14
+ models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ models/interfaces.py,sha256=VYYSJ_FdKTBjiGIdSuhCShjRrDLbzVlsCIQpQXe2e60,751
16
+ models/keywords.py,sha256=GzZ-xPZyYLiZlFNb93793Mf8u4_LVulFvd0RzFrpDQg,247
17
+ models/media.py,sha256=gTNwmKd1irHW1julWCLCr4nzvVE-mXLzfXOsE3IBFnE,19724
18
+ models/media_info.py,sha256=WmYHyFCG-A0RP4fQ5XlGJF8NBvpqQotH_1p7sZPrr0Y,5059
19
+ models/movie.py,sha256=G3GM_WEAsfQCOpeUnvHEtcOc3DN10w1FElZWHs2eQqw,7618
20
+ models/tv.py,sha256=Ibo7C6XzFxHq0pmD-qJDspM10DxSQRV6conNnqzGDKQ,5789
21
+ models/tvdb_search.py,sha256=rqxhp6JX2DnFAWEz-KpmhEF7F138h82Crd2ITuukzrk,3034
22
+ models/videos.py,sha256=TaoPt9zIlMweKFmhw5vyWqNuFFfn3gllbEfCC8X-Xvo,413
23
+ repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ repositories/db_online.py,sha256=jx1tHwekNRH0f6TYi5ITWskMDflfC1RFIEBrwPDP0uY,2890
25
+ repositories/interfaces.py,sha256=r5UonODzBHUMzItJJjgiFsa1Eu5zfqtrRH_h1DjvO6E,1616
26
+ repositories/job_repos.py,sha256=4vU6_Z9ZxwyzyP-0RNIIPzSvHpSPqf0P2P1sXcJOTBg,4854
27
+ repositories/media_info_factory.py,sha256=_QCx2q4Dd9bUpCwmmYyg0uORLMLIlM5hbw1J1H4DiJM,810
28
+ services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ services/auto_async_service.py,sha256=Y6c8eSCBVo4ugm-TUiT1yJLbqqKoSJhy05XtdS2MSGs,8931
30
+ services/create_torrent_service.py,sha256=VN7bKkoayBHl0k8opGTM2VVCLRY8ruzaLBPV_Guvry0,3584
31
+ services/interfaces.py,sha256=CQHTxugH8vscv0GEC5fjJV5Tu6Bjyn8B9eQlW03uIkE,1832
32
+ services/itt_tracker_helper.py,sha256=U-HhkP_F-P4jYzBRrgbSHQL7TzDIZbS98PRXg_gG6ps,16194
33
+ services/itt_tracker_service.py,sha256=X6CxnLt9hJq5Fm5OqKE_TK_yCfFq1m_z8LiknoNpxtU,3358
34
+ services/lifespan_service.py,sha256=4yjFtSa-37nw8TbhDYd1mQxrGrLRDH6QlFWvX390k5c,2657
35
+ services/media_service.py,sha256=fF-u-jyB3KiJFYs-PnkhxTNyMrQQ4fp4RsX-jjbiPbo,5195
36
+ services/tags_service.py,sha256=k2lFCOc1FOfBj058j3_kq1drm2upo7kObiGBS3lOqzQ,14998
37
+ services/tmdb.py,sha256=sMxP3lGQpWCkcbje0SeR9R4wUarb-ebASl_Ou7ympEg,8834
38
+ services/torrent_client_service.py,sha256=6R8xeHHadfbmViddbi9sDHMCJ-yJS7Y-I3znD1mmiAM,2758
39
+ services/torrent_service.py,sha256=Y6yIZb1FL_lkU7sPi7pp5dCuZO_vw2P8Vv4iG2mQBGE,3841
40
+ services/tvdb.py,sha256=4xU7i5kLz5aMA39hsOOMnedH6gbX53mJFcjfpWI9kE8,2105
41
+ services/utility.py,sha256=jis0CiCMuzO6st08a3CN1GsOY86LRS1dc36OSZ0gwtU,13401
42
+ services/video_service.py,sha256=WwMJNIY5AXe7gemF6zeOS8x-k4twt2lUa4eEtSSo_H8,13560
43
+ use_case/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ use_case/make_torrent_usecase.py,sha256=6iwph0waXix90kBfnFJpRj8fpPZenIMuGMSDw1kdeRo,2227
45
+ use_case/process_all_usecase.py,sha256=MstzTeBqy3dSjEDjYzdVB1l7YRV_-ddVRAMtlj6bh40,1618
46
+ use_case/scan_media_usecase.py,sha256=j1zJIqJ1gxfRDiXQlVhAFvRK2QC9297o3X_oe2dF_8Y,4375
47
+ use_case/seed_usecase.py,sha256=5lDulS0MtAN9LVZ8H8loH_pyLsIYXLq8qZf0k3s61lg,4408
48
+ use_case/upload_usecase.py,sha256=8Eu1q8QFsxlodDeCNaBGpFHDQYdJLJ_FDH8a4aCaLQc,2748
49
+ unit3dwebup-0.0.14.dist-info/METADATA,sha256=xoK_SIL2B-mAU9QUKi3kaYbFpkpHbRe6NzskRWgurLk,4060
50
+ unit3dwebup-0.0.14.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
51
+ unit3dwebup-0.0.14.dist-info/entry_points.txt,sha256=09JdfHldw7AOMJA8Dh9Na3pnd6_4bdSKjQsnMQZsFKY,43
52
+ unit3dwebup-0.0.14.dist-info/top_level.txt,sha256=vt_gnbJJYqpYlLBQNYiAh2DtiFVJBh7enVITw-wImV0,54
53
+ unit3dwebup-0.0.14.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ unit3dupweb = start:main
@@ -0,0 +1,6 @@
1
+ config
2
+ external
3
+ models
4
+ repositories
5
+ services
6
+ use_case
use_case/__init__.py ADDED
File without changes
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from services.torrent_service import TorrentService
7
+ from models.media import Media
8
+ from fastapi import FastAPI
9
+
10
+
11
+ class MakeTorrentUseCase:
12
+ def __init__(self, app: FastAPI, job_id: str | None = None):
13
+ """
14
+ Create one or more torrents
15
+
16
+ :param app: the fastapi app
17
+ :param job_id: the job id
18
+ """
19
+ self.media_list = None
20
+ self.app = app
21
+ self.job_id = job_id
22
+
23
+ async def execute(self, media_list: list[Media] | None = None) -> bool:
24
+ """
25
+ Execute the torrent use case: create one or more torrents , notify to frontend
26
+ :param media_list: list of media objects used to create torrents
27
+ :return:
28
+ """
29
+
30
+ # Create the media list for single torrent based on the job_id
31
+ if self.job_id and not media_list:
32
+ results = await self.app.state.job.get_job(job_id=self.job_id)
33
+ self.media_list = [
34
+ Media.from_dict(item)
35
+ for item in [json.loads(results)]
36
+ ]
37
+ else:
38
+ # Receive a list
39
+ self.media_list = media_list
40
+
41
+ # FILTER: filter for existing torrent. Ensures that only new torrents are created
42
+ filtered_torrent_list = []
43
+ for media in self.media_list:
44
+ if Path.exists(media.torrent_file_path):
45
+ # notify the frontend
46
+ await self.send_message(media=media, message=f"Torrent file exists")
47
+ else:
48
+ # Add media objects for the new torrents
49
+ filtered_torrent_list.append(media)
50
+
51
+ if filtered_torrent_list:
52
+ torrent_service = TorrentService(app=self.app)
53
+ await torrent_service.start(media_list=filtered_torrent_list)
54
+ return True
55
+
56
+ return False
57
+
58
+ async def send_message(self, media: Media, message: str):
59
+ """
60
+ :param media: The current Media object
61
+ :param message: Send the message to single poster
62
+ :return:
63
+ """
64
+ await self.app.state.ws_manager.broadcast({
65
+ "type": "posterLogMessage",
66
+ "job_id": media.job_id,
67
+ "message": message})