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,43 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+
4
+ from use_case.make_torrent_usecase import MakeTorrentUseCase
5
+ from use_case.upload_usecase import UploadUseCase
6
+ from use_case.seed_usecase import SeedUseCase
7
+
8
+ from fastapi import FastAPI
9
+ from models.media import Media
10
+
11
+
12
+ class ProcessAllUseCase:
13
+ def __init__(self, app: FastAPI, job_list_id: str, torrent_client_name: str):
14
+ """
15
+ :param app: the FastAPI app
16
+ :param job_list_id: the job list id
17
+ :param torrent_client_name: the torrent client (qbittorrent etc)
18
+ """
19
+
20
+ self.app = app
21
+ self.job_list_id = job_list_id
22
+
23
+ self.media_list = None
24
+ self.seed_use_case = SeedUseCase(app=app, client=torrent_client_name)
25
+ self.torrent_use_case = MakeTorrentUseCase(app=app)
26
+ self.upload_use_case = UploadUseCase(app=app)
27
+
28
+ async def execute(self):
29
+ # LOAD a list of jobs from the cache based on the job_list_id received from the frontend
30
+ job_list = await self.app.state.job.get_job_list(job_id=self.job_list_id)
31
+ results = [json.loads(await self.app.state.job.get_job(job_id)) for job_id in job_list]
32
+
33
+ # MEDIA: create a media_list
34
+ self.media_list = [Media.from_dict(item) for item in results]
35
+
36
+ # TORRENT: create one or more torrent files
37
+ await self.torrent_use_case.execute(media_list=self.media_list)
38
+
39
+ # UPLOAD: upload to the tracker one or more torrent file
40
+ await self.upload_use_case.execute(media_list=self.media_list)
41
+
42
+ # SEED: seed one or more torrent file
43
+ await self.seed_use_case.execute(media_list=self.media_list)
@@ -0,0 +1,133 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import aiohttp
4
+
5
+ from repositories.interfaces import JobRepositoryInterface
6
+ from config.constants import MediaStatus
7
+ from config.logger import get_logger
8
+
9
+ from services.video_service import VideoService, BuildService
10
+ from services.media_service import MediaService, MediaService2
11
+ from services.auto_async_service import AsyncMediaManager
12
+
13
+
14
+ class ScanMediaUseCase:
15
+ """
16
+ Manage services to search for ID create screenshots and create descriptions sequentially
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ manager: AsyncMediaManager,
22
+ media_service: MediaService,
23
+ media_service2: MediaService2,
24
+ job_repo: JobRepositoryInterface,
25
+ session: aiohttp.ClientSession,
26
+ job_list: list[str],
27
+ ):
28
+ """
29
+ :param manager: instance of the scanner
30
+ :param media_service: instance of external service tmdb
31
+ :param media_service2: instance of the external service tvdb
32
+ :param job_repo: instance of the job repository ( redis)
33
+ :param session: aiohttp session
34
+ :param job_list: list of jobs id for the current list
35
+ """
36
+ self.manager = manager
37
+ self.media_services = [media_service, media_service2]
38
+ self.job_repo = job_repo
39
+ self.session = session
40
+ self.job_list = job_list
41
+ self.logger = get_logger(self.__class__.__name__)
42
+
43
+ async def execute(self):
44
+
45
+ # Scan the local files and create a Media object for each one
46
+ media_list = await self.manager.process_all()
47
+
48
+ # Process all Media objects concurrently
49
+ return await asyncio.gather(
50
+ *(self._process_media(m) for m in media_list)
51
+ )
52
+
53
+ async def _process_media(self, media):
54
+ # job_id already exists in the job_list ?
55
+ # if does -> no description created -> job already exist
56
+ if media.job_id in self.job_list:
57
+ return media
58
+
59
+ # We get a new job_id , search for tmdb and tvdb id !
60
+ await self._identify_media(media)
61
+
62
+ # ops! ( for example ID not found ) #todo split tmdb and tvdb case
63
+ if media.status == MediaStatus.DB_ERROR:
64
+ return media
65
+
66
+ # return if ffmpeg fail
67
+ if not await self._generate_video(media):
68
+ return media
69
+
70
+ # Pass the screenshot to the image host and build a html description for the tracker
71
+ await self._build_description(media)
72
+
73
+ # Save to redis
74
+ await self._save(media)
75
+
76
+ # return the current media object
77
+ return media
78
+
79
+ async def _identify_media(self, media):
80
+ media.status = MediaStatus.DB_NOT_IDENTIFIED
81
+
82
+ try:
83
+ # Search id from tmdb, tvdb
84
+ # and search imdb id in the tvdb remote_ids list
85
+ for service in self.media_services:
86
+ success = await service.fetch(media)
87
+ if success:
88
+ media.status = MediaStatus.DB_IDENTIFIED
89
+ except Exception as e:
90
+ await self._handle_error(media, MediaStatus.DB_ERROR, e)
91
+
92
+ # save to redis
93
+ await self._save(media)
94
+
95
+ async def _generate_video(self, media):
96
+
97
+ try:
98
+ # Extract the screenshots
99
+ video_service = VideoService(media)
100
+ await video_service.generate()
101
+ media.status = MediaStatus.VIDEO_READY
102
+ return True
103
+
104
+ except Exception as e:
105
+ await self._handle_error(media, MediaStatus.VIDEO_ERROR, e)
106
+ return False
107
+
108
+ async def _build_description(self, media):
109
+
110
+ try:
111
+ # Upload the screenshot to imagehost and return a html code for the description
112
+ builder = BuildService(
113
+ media_list=[media],
114
+ session=self.session,
115
+ app= self.manager.app
116
+ )
117
+ await builder.description()
118
+
119
+ except Exception as e:
120
+ await self._handle_error(media, MediaStatus.DESCRIPTION_ERROR, e)
121
+
122
+ async def _handle_error(self, media, status, error):
123
+ media.status = status
124
+ media.error = str(error)
125
+ self.logger.error(f"{status} {media.file_name} {media.error}")
126
+ await self._save(media)
127
+
128
+ async def _save(self, media):
129
+
130
+ await self.job_repo.create_job(
131
+ job_id=media.job_id,
132
+ data=media.to_dict()
133
+ )
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from services.interfaces import TorrentClientServiceInterface
6
+ from services.torrent_client_service import QbittorrentClientService
7
+ from models.media import Media
8
+ from fastapi import FastAPI, status
9
+ from fastapi.responses import JSONResponse
10
+
11
+
12
+ class SeedUseCase:
13
+ def __init__(self, app: FastAPI, client: str, job_id: str | None = None):
14
+ """
15
+ :param app: the fastapi app
16
+ :param job_id: the job id
17
+ :param client: torrent client name
18
+ """
19
+ self.media_list = None
20
+ self.app = app
21
+ self.job_id = job_id
22
+ self.client = client
23
+
24
+ async def execute(self, media_list: list[Media] | None = None) -> JSONResponse:
25
+ """
26
+ Execute the seed use case : load one or more jobs, login to client, verify torrent files,
27
+ add torrents file to the client
28
+
29
+ :param media_list: list of media to upload used when shared with other services
30
+ :return:
31
+ """
32
+
33
+ # Create the media list for single torrent based on the job_id
34
+ if self.job_id and not media_list:
35
+ results = await self.app.state.job.get_job(job_id=self.job_id)
36
+ self.media_list = [
37
+ Media.from_dict(item)
38
+ for item in [json.loads(results)]
39
+ ]
40
+ else:
41
+ # Receive a list
42
+ self.media_list = media_list
43
+
44
+ # Verify whether the *.torrent files still exist and notify the frontend
45
+ filtered_torrent_list = []
46
+ for media in self.media_list:
47
+ if not Path.exists(media.torrent_file_path):
48
+ await self.send_message(media=media, message=f"Torrent file not found !")
49
+ else:
50
+ # discard the invalid torrent file
51
+ filtered_torrent_list.append(media)
52
+
53
+ # Add torrents to the client
54
+ if filtered_torrent_list:
55
+ # Ok - try to login
56
+ torr_client_service: TorrentClientServiceInterface = await self.get_fact_client(name=self.client)
57
+ response = await torr_client_service.login()
58
+
59
+ # Login failed
60
+ if not response:
61
+ await self.broadcast_messages(f"{self.client} Login failed")
62
+ return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={})
63
+
64
+ torrent_path_list = [m.torrent_file_path for m in filtered_torrent_list]
65
+ save_path = self.app.state.settings.prefs.SCAN_PATH
66
+
67
+ # Try to add torrents
68
+ execution = await torr_client_service.add_torrents(
69
+ torrent_paths=torrent_path_list, # Docker path
70
+ save_path=save_path, # Host path
71
+ app=self.app)
72
+
73
+ if execution:
74
+ await self.broadcast_messages(f"Added to {self.client}")
75
+ return JSONResponse(status_code=status.HTTP_200_OK, content={})
76
+ else:
77
+ return JSONResponse(status_code=status.HTTP_409_CONFLICT, content={})
78
+ return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={})
79
+
80
+
81
+
82
+ @staticmethod
83
+ async def get_fact_client(name: str) -> TorrentClientServiceInterface | None:
84
+ """
85
+ :param name: name of the default torrent client
86
+ :return:
87
+ """
88
+
89
+ if name == "qbittorrent":
90
+ return QbittorrentClientService()
91
+ # TODO: aggiungere un altro client mantenendo gli stessi metodi
92
+ # in attesa di essere aggiunti
93
+ # if name == "deluge": return DelugeClientService(...)
94
+ # if name == "transmission": return TransmissionClientService(...)
95
+ return None
96
+
97
+ async def broadcast_messages(self, message: str):
98
+ """
99
+ :param message: Send the message to all posters
100
+ :return:
101
+ """
102
+ for media in self.media_list:
103
+ await self.app.state.ws_manager.broadcast({
104
+ "type": "posterLogMessage",
105
+ "job_id": media.job_id,
106
+ "message": message})
107
+
108
+ async def send_message(self, media: Media, message: str):
109
+ """
110
+ :param media: The current Media object
111
+ :param message: Send the message to single poster
112
+ :return:
113
+ """
114
+ await self.app.state.ws_manager.broadcast({
115
+ "type": "posterLogMessage",
116
+ "job_id": media.job_id,
117
+ "message": message})
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from typing import Any
4
+
5
+ from services.interfaces import TrackerServiceInterface
6
+ from services.itt_tracker_service import ITTtrackerService
7
+ from models.media import Media
8
+
9
+ import asyncio
10
+ import aiohttp
11
+
12
+ from config.logger import get_logger
13
+ from fastapi import FastAPI
14
+
15
+
16
+ class UploadUseCase:
17
+
18
+ def __init__(self, app: FastAPI, job_id: str | None = None):
19
+ """
20
+ :param app: the FastAPI app
21
+ :param job_id: the job id. Used for a single service upload
22
+ """
23
+
24
+ self.media_list: list[Media] | None = None
25
+ self.app = app
26
+ self.job_id = job_id
27
+ self.logger = get_logger(self.__class__.__name__)
28
+
29
+ async def execute(self, media_list: list[Media] | None = None) -> bool:
30
+ """
31
+ Execute the seed use case : load one or more jobs, login to tracker, upload the torrents,
32
+ send message to the frontend
33
+
34
+ :param media_list: list of media to upload used when shared with other services
35
+ :return: true or false
36
+ """
37
+ self.media_list = media_list
38
+ # Create the media list for single torrent
39
+ if self.job_id and not self.media_list:
40
+ results = await self.app.state.job.get_job(job_id=self.job_id)
41
+ self.media_list = [
42
+ Media.from_dict(item)
43
+ for item in [json.loads(results)]
44
+ ]
45
+
46
+ async with aiohttp.ClientSession() as session:
47
+
48
+ # Tracker instance
49
+ tracker_service: TrackerServiceInterface = ITTtrackerService(session, self.app)
50
+
51
+ # Build a list of media
52
+ tasks = [tracker_service.upload(media) for media in self.media_list]
53
+
54
+ # Concurrent execution
55
+ uploaded_torrents = await asyncio.gather(*tasks)
56
+ self.logger.debug(f"Start Uploaded Torrents {uploaded_torrents}")
57
+
58
+ # Send a message to frontend for each uploaded media
59
+ await self.broadcast_messages(uploaded_torrents=uploaded_torrents)
60
+
61
+ for torrent in uploaded_torrents:
62
+ await self.app.state.ws_manager.broadcast({
63
+ "type": "posterLogMessage",
64
+ "job_id": torrent['job_id'],
65
+ "message": f"{torrent['message']}"})
66
+ return True
67
+
68
+ async def broadcast_messages(self, uploaded_torrents: tuple[Any]) -> None:
69
+ """
70
+ :param uploaded_torrents: results message from the uploaded torrents
71
+ :return:
72
+ """
73
+ for torrent in uploaded_torrents:
74
+ await self.app.state.ws_manager.broadcast({
75
+ "type": "posterLogMessage",
76
+ "job_id": torrent['job_id'],
77
+ "message": f"{torrent['message']}"})