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.
- config/__init__.py +4 -0
- config/api_data.py +28 -0
- config/constants.py +34 -0
- config/host_data.py +47 -0
- config/itt.py +154 -0
- config/logger.py +37 -0
- config/settings.py +212 -0
- config/sis.py +137 -0
- config/tags.py +395 -0
- config/trackers.py +47 -0
- external/__init__.py +0 -0
- external/async_http_client_service.py +48 -0
- external/websocket.py +41 -0
- models/__init__.py +0 -0
- models/interfaces.py +39 -0
- models/keywords.py +13 -0
- models/media.py +597 -0
- models/media_info.py +142 -0
- models/movie.py +287 -0
- models/tv.py +266 -0
- models/tvdb_search.py +102 -0
- models/videos.py +26 -0
- repositories/__init__.py +0 -0
- repositories/db_online.py +82 -0
- repositories/interfaces.py +59 -0
- repositories/job_repos.py +166 -0
- repositories/media_info_factory.py +28 -0
- services/__init__.py +0 -0
- services/auto_async_service.py +237 -0
- services/create_torrent_service.py +94 -0
- services/interfaces.py +72 -0
- services/itt_tracker_helper.py +463 -0
- services/itt_tracker_service.py +85 -0
- services/lifespan_service.py +58 -0
- services/media_service.py +114 -0
- services/tags_service.py +389 -0
- services/tmdb.py +246 -0
- services/torrent_client_service.py +92 -0
- services/torrent_service.py +107 -0
- services/tvdb.py +65 -0
- services/utility.py +433 -0
- services/video_service.py +356 -0
- unit3dwebup-0.0.14.dist-info/METADATA +191 -0
- unit3dwebup-0.0.14.dist-info/RECORD +53 -0
- unit3dwebup-0.0.14.dist-info/WHEEL +5 -0
- unit3dwebup-0.0.14.dist-info/entry_points.txt +2 -0
- unit3dwebup-0.0.14.dist-info/top_level.txt +6 -0
- use_case/__init__.py +0 -0
- use_case/make_torrent_usecase.py +67 -0
- use_case/process_all_usecase.py +43 -0
- use_case/scan_media_usecase.py +133 -0
- use_case/seed_usecase.py +117 -0
- 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
|
+
)
|
use_case/seed_usecase.py
ADDED
|
@@ -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']}"})
|