Unit3DwebUp 0.0.23__tar.gz → 0.0.26__tar.gz
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.
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/PKG-INFO +2 -2
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/README.md +1 -1
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/PKG-INFO +2 -2
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/SOURCES.txt +12 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/pyproject.toml +1 -1
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/__init__.py +1 -1
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/settings.py +23 -14
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/tags.py +5 -0
- unit3dwebup-0.0.26/unit3dwup/lifespan.py +154 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/media.py +51 -31
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/media_info.py +26 -8
- unit3dwebup-0.0.26/unit3dwup/models/subtitles.py +17 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/media_info_factory.py +2 -0
- {unit3dwebup-0.0.23/unit3dwup/services → unit3dwebup-0.0.26/unit3dwup/routers}/__init__.py +0 -0
- unit3dwebup-0.0.26/unit3dwup/routers/jobs.py +54 -0
- unit3dwebup-0.0.26/unit3dwup/routers/posters.py +110 -0
- unit3dwebup-0.0.26/unit3dwup/routers/process.py +90 -0
- unit3dwebup-0.0.26/unit3dwup/routers/scan.py +146 -0
- unit3dwebup-0.0.26/unit3dwup/routers/search.py +47 -0
- unit3dwebup-0.0.26/unit3dwup/routers/settings.py +151 -0
- unit3dwebup-0.0.26/unit3dwup/routers/ws.py +42 -0
- unit3dwebup-0.0.26/unit3dwup/schemas.py +33 -0
- unit3dwebup-0.0.26/unit3dwup/services/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/create_torrent_service.py +1 -1
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_helper.py +2 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_service.py +18 -8
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tags_service.py +57 -40
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tmdb.py +19 -21
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/utility.py +4 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/video_service.py +3 -3
- unit3dwebup-0.0.26/unit3dwup/start.py +37 -0
- unit3dwebup-0.0.23/unit3dwup/start.py → unit3dwebup-0.0.26/unit3dwup/start_backup.py +66 -17
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/upload_usecase.py +2 -1
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/dependency_links.txt +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/entry_points.txt +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/requires.txt +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/top_level.txt +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/setup.cfg +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/api_data.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/constants.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/host_data.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/itt.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/logger.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/sis.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/trackers.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/async_http_client_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/websocket.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/interfaces.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/keywords.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/movie.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/tv.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/tvdb_search.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/videos.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/db_online.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/interfaces.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/job_repos.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/auto_async_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/interfaces.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/lifespan_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/media_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_client_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_service.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tvdb.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/__init__.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/make_torrent_usecase.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/process_all_usecase.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/scan_media_usecase.py +0 -0
- {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/seed_usecase.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Unit3DwebUp
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Summary: A Unit3D uploader with a web interface
|
|
5
5
|
Author: Parzival
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -25,7 +25,7 @@ Requires-Dist: torf==4.3.1
|
|
|
25
25
|
Requires-Dist: watchdog==6.0.0
|
|
26
26
|
Requires-Dist: uvicorn[standard]==0.40.0
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Unit3DwebUp
|
|
29
29
|
|
|
30
30
|

|
|
31
31
|

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Unit3DwebUp
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Summary: A Unit3D uploader with a web interface
|
|
5
5
|
Author: Parzival
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -25,7 +25,7 @@ Requires-Dist: torf==4.3.1
|
|
|
25
25
|
Requires-Dist: watchdog==6.0.0
|
|
26
26
|
Requires-Dist: uvicorn[standard]==0.40.0
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Unit3DwebUp
|
|
29
29
|
|
|
30
30
|

|
|
31
31
|

|
|
@@ -7,7 +7,10 @@ Unit3DwebUp.egg-info/entry_points.txt
|
|
|
7
7
|
Unit3DwebUp.egg-info/requires.txt
|
|
8
8
|
Unit3DwebUp.egg-info/top_level.txt
|
|
9
9
|
unit3dwup/__init__.py
|
|
10
|
+
unit3dwup/lifespan.py
|
|
11
|
+
unit3dwup/schemas.py
|
|
10
12
|
unit3dwup/start.py
|
|
13
|
+
unit3dwup/start_backup.py
|
|
11
14
|
unit3dwup/config/__init__.py
|
|
12
15
|
unit3dwup/config/api_data.py
|
|
13
16
|
unit3dwup/config/constants.py
|
|
@@ -27,6 +30,7 @@ unit3dwup/models/keywords.py
|
|
|
27
30
|
unit3dwup/models/media.py
|
|
28
31
|
unit3dwup/models/media_info.py
|
|
29
32
|
unit3dwup/models/movie.py
|
|
33
|
+
unit3dwup/models/subtitles.py
|
|
30
34
|
unit3dwup/models/tv.py
|
|
31
35
|
unit3dwup/models/tvdb_search.py
|
|
32
36
|
unit3dwup/models/videos.py
|
|
@@ -35,6 +39,14 @@ unit3dwup/repositories/db_online.py
|
|
|
35
39
|
unit3dwup/repositories/interfaces.py
|
|
36
40
|
unit3dwup/repositories/job_repos.py
|
|
37
41
|
unit3dwup/repositories/media_info_factory.py
|
|
42
|
+
unit3dwup/routers/__init__.py
|
|
43
|
+
unit3dwup/routers/jobs.py
|
|
44
|
+
unit3dwup/routers/posters.py
|
|
45
|
+
unit3dwup/routers/process.py
|
|
46
|
+
unit3dwup/routers/scan.py
|
|
47
|
+
unit3dwup/routers/search.py
|
|
48
|
+
unit3dwup/routers/settings.py
|
|
49
|
+
unit3dwup/routers/ws.py
|
|
38
50
|
unit3dwup/services/__init__.py
|
|
39
51
|
unit3dwup/services/auto_async_service.py
|
|
40
52
|
unit3dwup/services/create_torrent_service.py
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from pydantic import BaseModel, field_validator, model_validator, HttpUrl, Field, ValidationError
|
|
8
8
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
9
|
from unit3dwup.config.logger import get_logger
|
|
10
|
+
from unit3dwup.services.utility import ManageTitles
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
# /// Class to help avoid typos...
|
|
@@ -27,11 +28,6 @@ class BaseConfigModel(BaseModel):
|
|
|
27
28
|
return v
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
# /// version
|
|
31
|
-
class Unit3DwebUp(BaseConfigModel):
|
|
32
|
-
VERSION: str = "build"
|
|
33
|
-
|
|
34
|
-
|
|
35
31
|
# /// TRACKER CONFIG
|
|
36
32
|
class TrackerConfig(BaseConfigModel):
|
|
37
33
|
ITT_URL: HttpUrl
|
|
@@ -66,7 +62,7 @@ class TrackerConfig(BaseConfigModel):
|
|
|
66
62
|
def validate_api_keys(cls, v, info):
|
|
67
63
|
if info.field_name.endswith("_KEY") or "APIKEY" in info.field_name:
|
|
68
64
|
if v is not None and len(v) < 5:
|
|
69
|
-
raise ValueError(f"{
|
|
65
|
+
raise ValueError(f"{v} too short")
|
|
70
66
|
return v
|
|
71
67
|
|
|
72
68
|
|
|
@@ -98,7 +94,7 @@ class TorrentClientConfig(BaseConfigModel):
|
|
|
98
94
|
@classmethod
|
|
99
95
|
def validate_ports(cls, v):
|
|
100
96
|
if not 1 <= v <= 65535:
|
|
101
|
-
raise ValueError("invalid port range")
|
|
97
|
+
raise ValueError(f"{v} invalid port range")
|
|
102
98
|
return v
|
|
103
99
|
|
|
104
100
|
@model_validator(mode="after")
|
|
@@ -117,7 +113,7 @@ class TorrentClientConfig(BaseConfigModel):
|
|
|
117
113
|
|
|
118
114
|
# /// USER PREFERENCES
|
|
119
115
|
class UserPreferences(BaseConfigModel):
|
|
120
|
-
RELEASER_SIGN: str =
|
|
116
|
+
RELEASER_SIGN: str | None = None
|
|
121
117
|
TAG_POSITION_MOVIE: list[str]
|
|
122
118
|
TAG_POSITION_SERIE: list[str]
|
|
123
119
|
PTSCREENS_PRIORITY: int = 0
|
|
@@ -141,7 +137,7 @@ class UserPreferences(BaseConfigModel):
|
|
|
141
137
|
SCAN_PATH: str = None
|
|
142
138
|
COMPRESS_SCSHOT: int = 4
|
|
143
139
|
TORRENT_COMMENT: str | None = "no_comment"
|
|
144
|
-
PREFERRED_LANG: str
|
|
140
|
+
PREFERRED_LANG: str = "all"
|
|
145
141
|
ANON: bool = False
|
|
146
142
|
WEBP_ENABLED: bool = False
|
|
147
143
|
PERSONAL_RELEASE: bool = False
|
|
@@ -151,7 +147,7 @@ class UserPreferences(BaseConfigModel):
|
|
|
151
147
|
@classmethod
|
|
152
148
|
def validate_interval(cls, v):
|
|
153
149
|
if v < 5:
|
|
154
|
-
raise ValueError("WATCHER_INTERVAL too low")
|
|
150
|
+
raise ValueError(f"WATCHER_INTERVAL {v} too low")
|
|
155
151
|
return v
|
|
156
152
|
|
|
157
153
|
@field_validator("TAG_POSITION_MOVIE", "TAG_POSITION_SERIE", mode="before")
|
|
@@ -161,13 +157,19 @@ class UserPreferences(BaseConfigModel):
|
|
|
161
157
|
return [x.strip() for x in v.split(",") if x.strip()]
|
|
162
158
|
return v
|
|
163
159
|
|
|
160
|
+
@field_validator("PREFERRED_LANG")
|
|
161
|
+
@classmethod
|
|
162
|
+
def validate_preferred_lang(cls, v):
|
|
163
|
+
if ManageTitles.convert_iso(v) or v.lower() == "all":
|
|
164
|
+
return v
|
|
165
|
+
raise ValueError(f"invalid {v} value for PREFERRED_LANG")
|
|
166
|
+
|
|
164
167
|
|
|
165
168
|
# /// APP SETTINGS
|
|
166
169
|
class Settings(BaseSettings):
|
|
167
170
|
"""
|
|
168
171
|
Set default settings
|
|
169
172
|
"""
|
|
170
|
-
unit3DwebUp: Unit3DwebUp = Field(default_factory=Unit3DwebUp)
|
|
171
173
|
tracker: TrackerConfig = Field(default_factory=TrackerConfig)
|
|
172
174
|
torrent: TorrentClientConfig = Field(default_factory=TorrentClientConfig)
|
|
173
175
|
prefs: UserPreferences = Field(default_factory=UserPreferences)
|
|
@@ -178,7 +180,7 @@ class Settings(BaseSettings):
|
|
|
178
180
|
|
|
179
181
|
model_config = SettingsConfigDict(
|
|
180
182
|
env_nested_delimiter="__",
|
|
181
|
-
env_file=
|
|
183
|
+
env_file=ENV_FILE if not os.getenv("DOCKER") else None,
|
|
182
184
|
extra="ignore"
|
|
183
185
|
)
|
|
184
186
|
|
|
@@ -193,10 +195,17 @@ def get_settings() -> Settings:
|
|
|
193
195
|
try:
|
|
194
196
|
settings = Settings()
|
|
195
197
|
except ValidationError as e:
|
|
198
|
+
"""
|
|
199
|
+
https://pydantic.dev/docs/validation/latest/errors/validation_errors/
|
|
200
|
+
/// Pydantic Validation Error example
|
|
201
|
+
"loc": ("prefs", "PREFERRED_LANG"),
|
|
202
|
+
"msg": "Value error, invalid fr value for PREFERRED_LANG",
|
|
203
|
+
"type": "value_error"
|
|
204
|
+
"""
|
|
196
205
|
for err in e.errors():
|
|
197
206
|
field_path = ".".join(str(loc) for loc in err["loc"])
|
|
198
|
-
logger.warning(f"{field_path}
|
|
199
|
-
|
|
207
|
+
logger.warning(f"{field_path}: {err['msg']}")
|
|
208
|
+
logger.warning("-" * 50)
|
|
200
209
|
|
|
201
210
|
raise SystemExit(1)
|
|
202
211
|
|
|
@@ -250,6 +250,8 @@ SIGNS_LIST = {
|
|
|
250
250
|
"ITT": "releaser",
|
|
251
251
|
"JEDDAK-MIRCREW": "releaser",
|
|
252
252
|
"JOHNSEED": "releaser",
|
|
253
|
+
"K-Z": "releaser",
|
|
254
|
+
"KARIDO": "releaser",
|
|
253
255
|
"KINGOFROME": "releaser",
|
|
254
256
|
"KIN": "releaser",
|
|
255
257
|
"KISSY": "releaser",
|
|
@@ -278,6 +280,9 @@ SIGNS_LIST = {
|
|
|
278
280
|
"MADSKY": "releaser",
|
|
279
281
|
"MADTIA": "releaser",
|
|
280
282
|
"MAX": "releaser",
|
|
283
|
+
"MEM.GP" : "releaser",
|
|
284
|
+
"MEMGP": "releaser",
|
|
285
|
+
"MEM": "releaser",
|
|
281
286
|
"MAX2014": "releaser",
|
|
282
287
|
"ME7ALH": "releaser",
|
|
283
288
|
"MIK0YAN": "releaser",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from unit3dwup.config import get_settings
|
|
9
|
+
from unit3dwup.config import get_logger
|
|
10
|
+
|
|
11
|
+
from unit3dwup.repositories.job_repos import JobRedisRepo
|
|
12
|
+
|
|
13
|
+
from unit3dwup.services.lifespan_service import update_mounted_paths, checking_env_file
|
|
14
|
+
|
|
15
|
+
from unit3dwup.external.websocket import WebSocketManager
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
|
|
19
|
+
from watchdog.observers import Observer
|
|
20
|
+
from watchdog.events import FileSystemEventHandler
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RedisEventHandler(FileSystemEventHandler):
|
|
24
|
+
"""
|
|
25
|
+
Watch only a specific folder
|
|
26
|
+
This event is shared by app.state FastApi
|
|
27
|
+
it uses a queue 'redis_event'
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, app):
|
|
31
|
+
self.app = app
|
|
32
|
+
|
|
33
|
+
def on_created(self, event):
|
|
34
|
+
self.app.state.redis_events.put_nowait({
|
|
35
|
+
"type": "created",
|
|
36
|
+
"path": event.src_path,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
def on_deleted(self, event):
|
|
40
|
+
self.app.state.redis_events.put_nowait({
|
|
41
|
+
"type": "deleted",
|
|
42
|
+
"path": event.src_path,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def redis_event_consumer(app: FastAPI):
|
|
47
|
+
"""
|
|
48
|
+
:param app: it is mr FastApi
|
|
49
|
+
:return: None
|
|
50
|
+
|
|
51
|
+
It is a consumer. Wait for any news from the queue and extract the new path created or deleted
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# > The queue :|
|
|
55
|
+
queue = app.state.redis_events
|
|
56
|
+
|
|
57
|
+
# Logger
|
|
58
|
+
logger = get_logger("settings_logger")
|
|
59
|
+
|
|
60
|
+
while True:
|
|
61
|
+
event = await queue.get()
|
|
62
|
+
try:
|
|
63
|
+
app.state.folder_event = event
|
|
64
|
+
relative = Path(app.state.folder_event['path']).relative_to(Path(app.state.watcher_path))
|
|
65
|
+
new_path = Path(app.state.settings.prefs.WATCHER_PATH) / relative.parts[0]
|
|
66
|
+
|
|
67
|
+
# Send logs to the client
|
|
68
|
+
await app.state.ws_manager.broadcast({
|
|
69
|
+
"type": "log",
|
|
70
|
+
"level": "info",
|
|
71
|
+
"message": f"{app.state.folder_event['type']} {new_path}",
|
|
72
|
+
})
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.debug("Consumer Folder error", e)
|
|
75
|
+
finally:
|
|
76
|
+
queue.task_done()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@asynccontextmanager
|
|
80
|
+
async def lifespan(app: FastAPI):
|
|
81
|
+
"""
|
|
82
|
+
Lifespan initialize DB and app configuration
|
|
83
|
+
We need to shared state
|
|
84
|
+
https://fastapi.tiangolo.com/advanced/events/
|
|
85
|
+
|
|
86
|
+
:param app: FastAPI
|
|
87
|
+
:return: None
|
|
88
|
+
"""
|
|
89
|
+
# Load the configuration file
|
|
90
|
+
settings = get_settings()
|
|
91
|
+
app.state.settings = settings
|
|
92
|
+
|
|
93
|
+
# Check configuration file
|
|
94
|
+
await checking_env_file(app=app)
|
|
95
|
+
|
|
96
|
+
# Create a new state for the watcher queue
|
|
97
|
+
app.state.redis_events = asyncio.Queue()
|
|
98
|
+
|
|
99
|
+
# Environment variabile in the container backend
|
|
100
|
+
redis_host = os.getenv("REDIS_HOST", "localhost")
|
|
101
|
+
redis_port = int(os.getenv("REDIS_PORT", 6379))
|
|
102
|
+
|
|
103
|
+
# Build redis url
|
|
104
|
+
REDIS_URL = f"redis://{redis_host}:{redis_port}"
|
|
105
|
+
|
|
106
|
+
# Connect to redis
|
|
107
|
+
job = JobRedisRepo(url=REDIS_URL)
|
|
108
|
+
await job.connect(app=app)
|
|
109
|
+
|
|
110
|
+
# Store the job reference to app.state
|
|
111
|
+
app.state.job = job
|
|
112
|
+
|
|
113
|
+
# RestartDocker notify
|
|
114
|
+
# Set flag to true when setEnv is called from the frontend
|
|
115
|
+
app.state.restart_docker = False
|
|
116
|
+
|
|
117
|
+
# Create a new profile from user_preferences Job_id is '0'
|
|
118
|
+
# Later will be recalled from the setting endpoint
|
|
119
|
+
await job.create_profile(dict(settings.prefs))
|
|
120
|
+
|
|
121
|
+
# The WebSocket. Send to client progress bar value( Torrent creation) and short log message
|
|
122
|
+
app.state.ws_manager = WebSocketManager()
|
|
123
|
+
|
|
124
|
+
# Update mounted paths string
|
|
125
|
+
await update_mounted_paths(app=app)
|
|
126
|
+
|
|
127
|
+
# Watcher zone
|
|
128
|
+
# Shared event
|
|
129
|
+
app.state.folder_event = None
|
|
130
|
+
|
|
131
|
+
# back to watcher
|
|
132
|
+
observer = Observer()
|
|
133
|
+
|
|
134
|
+
# Callback
|
|
135
|
+
handler = RedisEventHandler(app)
|
|
136
|
+
|
|
137
|
+
# Start to watch
|
|
138
|
+
observer.schedule(handler, app.state.watcher_path, recursive=True)
|
|
139
|
+
if app.state.settings.prefs.WATCHER_DESTINATION_PATH != os.getcwd():
|
|
140
|
+
observer.start()
|
|
141
|
+
|
|
142
|
+
# Create a consumer that works in the background
|
|
143
|
+
consumer_task = asyncio.create_task(redis_event_consumer(app))
|
|
144
|
+
|
|
145
|
+
# Goes..
|
|
146
|
+
yield
|
|
147
|
+
|
|
148
|
+
# Come back to clean and close
|
|
149
|
+
if app.state.settings.prefs.WATCHER_DESTINATION_PATH != os.getcwd():
|
|
150
|
+
observer.stop()
|
|
151
|
+
observer.join()
|
|
152
|
+
|
|
153
|
+
consumer_task.cancel()
|
|
154
|
+
await job.close()
|
|
@@ -9,6 +9,7 @@ from unit3dwup.config.tags import crew_patterns, platform_patterns
|
|
|
9
9
|
from unit3dwup.config.tags import SIGNS_LIST, TAGS_LIST, BAN_LIST
|
|
10
10
|
from unit3dwup.config.constants import MediaStatus
|
|
11
11
|
from unit3dwup.config.settings import get_settings
|
|
12
|
+
from unit3dwup.config.logger import get_logger
|
|
12
13
|
|
|
13
14
|
from unit3dwup.services.utility import ManageTitles, System
|
|
14
15
|
from unit3dwup.services import utility
|
|
@@ -35,6 +36,7 @@ class Media:
|
|
|
35
36
|
self.subfolder: str = subfolder
|
|
36
37
|
self.title: str = (Path(self.folder) / self.subfolder).name
|
|
37
38
|
self._torrent_file_path = Path(torrent_archive_path) / "ITT" / f"{self.title}.torrent"
|
|
39
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
38
40
|
|
|
39
41
|
# // Assign a job id
|
|
40
42
|
path = Path(self.folder) / self.subfolder
|
|
@@ -95,6 +97,7 @@ class Media:
|
|
|
95
97
|
self._screen_shots: list | None = None
|
|
96
98
|
self._job_id_list: str | None = None
|
|
97
99
|
self._imdb_from_tvdb: str | None = None
|
|
100
|
+
self._can_upload: bool = True
|
|
98
101
|
|
|
99
102
|
@property
|
|
100
103
|
def title_sanitized(self) -> str:
|
|
@@ -287,10 +290,13 @@ class Media:
|
|
|
287
290
|
@property
|
|
288
291
|
def guess_episode(self) -> int | None:
|
|
289
292
|
if not self._guess_episode and System.category_list.get(System.TV_SHOW) in self.category:
|
|
290
|
-
if isinstance(self.guess_filename.guessit_episode,list):
|
|
293
|
+
if isinstance(self.guess_filename.guessit_episode, list):
|
|
291
294
|
self._guess_episode = 0
|
|
292
295
|
else:
|
|
293
|
-
|
|
296
|
+
if self.guess_filename.guessit_episode:
|
|
297
|
+
self._guess_episode = int(str(self.guess_filename.guessit_episode))
|
|
298
|
+
else:
|
|
299
|
+
self._guess_episode = 0
|
|
294
300
|
return self._guess_episode
|
|
295
301
|
|
|
296
302
|
@guess_episode.setter
|
|
@@ -471,6 +477,14 @@ class Media:
|
|
|
471
477
|
def is_folder(self, value: bool):
|
|
472
478
|
self._is_folder = value
|
|
473
479
|
|
|
480
|
+
@property
|
|
481
|
+
def can_upload(self) -> bool:
|
|
482
|
+
return self._can_upload
|
|
483
|
+
|
|
484
|
+
@can_upload.setter
|
|
485
|
+
def can_upload(self, value: bool):
|
|
486
|
+
self._can_upload = value
|
|
487
|
+
|
|
474
488
|
@property
|
|
475
489
|
def screen_shots(self) -> list[str]:
|
|
476
490
|
return self._screen_shots
|
|
@@ -505,34 +519,39 @@ class Media:
|
|
|
505
519
|
return self._languages
|
|
506
520
|
|
|
507
521
|
@property
|
|
508
|
-
def
|
|
509
|
-
if not self.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
522
|
+
def detected_resolution(self):
|
|
523
|
+
if not self.mediafile or not self.mediafile.video_width:
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
width = int(self.mediafile.video_width)
|
|
527
|
+
|
|
528
|
+
for limit, label in (
|
|
529
|
+
(3200, "2160p"),
|
|
530
|
+
(1600, "1080p"),
|
|
531
|
+
(1100, "720p"),
|
|
532
|
+
(960, "576p"),
|
|
533
|
+
):
|
|
534
|
+
if width >= limit:
|
|
535
|
+
return label
|
|
536
|
+
|
|
537
|
+
return f"{self.mediafile.video_height}p"
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def resolution(self):
|
|
541
|
+
if self._resolution:
|
|
542
|
+
return self._resolution
|
|
543
|
+
|
|
544
|
+
if not self.mediafile:
|
|
545
|
+
self._resolution = System.NO_RESOLUTION
|
|
546
|
+
return self._resolution
|
|
547
|
+
|
|
548
|
+
detected = self.detected_resolution
|
|
549
|
+
|
|
550
|
+
if detected:
|
|
551
|
+
self._resolution = detected
|
|
552
|
+
else:
|
|
553
|
+
self.logger.warning(f"{self.__class__.__name__}: video resolution not found in {self.file_name}")
|
|
554
|
+
self._resolution = System.NO_RESOLUTION
|
|
536
555
|
return self._resolution
|
|
537
556
|
|
|
538
557
|
@staticmethod
|
|
@@ -586,7 +605,7 @@ class Media:
|
|
|
586
605
|
"platform_list": self.platform_list,
|
|
587
606
|
"screen_shots": self.screen_shots,
|
|
588
607
|
"job_id_list": self.job_id_list,
|
|
589
|
-
|
|
608
|
+
"can_upload": self.can_upload,
|
|
590
609
|
}
|
|
591
610
|
|
|
592
611
|
@classmethod
|
|
@@ -620,4 +639,5 @@ class Media:
|
|
|
620
639
|
m.category = data.get("category")
|
|
621
640
|
m.screen_shots = data.get("screen_shots")
|
|
622
641
|
m.job_id_list = data.get("job_id_list")
|
|
642
|
+
m.can_upload = data.get("can_upload", False)
|
|
623
643
|
return m
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re, os
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Optional, List, Dict, Any
|
|
8
|
+
from unit3dwup.models.subtitles import SubtitleTrack
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass(slots=True)
|
|
@@ -18,6 +19,7 @@ class MediaFile:
|
|
|
18
19
|
video_tracks: List[Dict] = field(default_factory=list)
|
|
19
20
|
audio_tracks: List[Dict] = field(default_factory=list)
|
|
20
21
|
general_track: Dict = field(default_factory=dict)
|
|
22
|
+
text_tracks: List[Dict] = field(default_factory=list)
|
|
21
23
|
|
|
22
24
|
@property
|
|
23
25
|
def media_description(self) -> str:
|
|
@@ -82,16 +84,30 @@ class MediaFile:
|
|
|
82
84
|
return self.audio_tracks[0].get("language", "Unknown") if self.audio_tracks else "Unknown"
|
|
83
85
|
|
|
84
86
|
@property
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
+
def subtitles(self) -> list:
|
|
88
|
+
tracks: list[SubtitleTrack] = []
|
|
89
|
+
|
|
90
|
+
def parse_bool(value: str) -> bool:
|
|
91
|
+
return str(value).lower() in ("yes", "true", "1")
|
|
92
|
+
|
|
93
|
+
for t in self.text_tracks:
|
|
94
|
+
if t.get("track_type") != "Text":
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
tracks.append(
|
|
98
|
+
SubtitleTrack(
|
|
99
|
+
track_id=int(t.get("track_id", 0)),
|
|
100
|
+
language=t.get("language", "und"),
|
|
101
|
+
title=t.get("title", ""),
|
|
102
|
+
default=parse_bool(t.get("default", "No")),
|
|
103
|
+
forced=parse_bool(t.get("forced", "No")),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return tracks
|
|
87
107
|
|
|
88
108
|
@property
|
|
89
|
-
def available_languages(self) -> list[
|
|
90
|
-
langs = {
|
|
91
|
-
t.get("language")
|
|
92
|
-
for t in (self.audio_tracks + self.subtitle_tracks)
|
|
93
|
-
if t.get("language")
|
|
94
|
-
}
|
|
109
|
+
def available_languages(self) -> list[str]:
|
|
110
|
+
langs = {t.language for t in self.subtitles if t.language}
|
|
95
111
|
return list(langs) or ["not found"]
|
|
96
112
|
|
|
97
113
|
@property
|
|
@@ -123,6 +139,7 @@ class MediaFile:
|
|
|
123
139
|
"media_to_string": self.media_to_string,
|
|
124
140
|
"video_tracks": self.video_tracks,
|
|
125
141
|
"audio_tracks": self.audio_tracks,
|
|
142
|
+
"text_tracks": self.text_tracks,
|
|
126
143
|
"general_track": self.general_track,
|
|
127
144
|
}
|
|
128
145
|
|
|
@@ -139,4 +156,5 @@ class MediaFile:
|
|
|
139
156
|
video_tracks=data.get("video_tracks", []),
|
|
140
157
|
audio_tracks=data.get("audio_tracks", []),
|
|
141
158
|
general_track=data.get("general_track", {}),
|
|
159
|
+
text_tracks=data.get("text_tracks", []),
|
|
142
160
|
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SubtitleTrack:
|
|
7
|
+
track_id: int
|
|
8
|
+
language: str
|
|
9
|
+
title: str
|
|
10
|
+
default: bool
|
|
11
|
+
forced: bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SubtitleInfo:
|
|
16
|
+
total: int
|
|
17
|
+
tracks: list[SubtitleTrack]
|
|
@@ -19,10 +19,12 @@ class MediaFileFactory:
|
|
|
19
19
|
video = [track for track in data if track.get("track_type") == "Video"]
|
|
20
20
|
audio = [track for track in data if track.get("track_type") == "Audio"]
|
|
21
21
|
general = next((track for track in data if track.get("track_type") == "General"), {})
|
|
22
|
+
text = [track for track in data if track.get("track_type") == "Text"]
|
|
22
23
|
|
|
23
24
|
return MediaFile(
|
|
24
25
|
file_path=path,
|
|
25
26
|
video_tracks=video,
|
|
26
27
|
audio_tracks=audio,
|
|
27
28
|
general_track=general,
|
|
29
|
+
text_tracks=text,
|
|
28
30
|
)
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request
|
|
6
|
+
|
|
7
|
+
from unit3dwup.config import get_logger
|
|
8
|
+
|
|
9
|
+
from unit3dwup.schemas import ClearJobListRequest
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.post("/cjoblist")
|
|
15
|
+
async def clear_job_list_id(payload: ClearJobListRequest, request: Request):
|
|
16
|
+
"""
|
|
17
|
+
This endpoint deletes a job list and all related posters
|
|
18
|
+
|
|
19
|
+
Required:
|
|
20
|
+
- job_list_id: identifier of the job list
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
- status: operation result
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
app = request.app
|
|
27
|
+
|
|
28
|
+
# Load the job list
|
|
29
|
+
# Carica la joblist per ottenere il percorso e inviarlo a log. Mi sembra troppo per una stampa
|
|
30
|
+
job_list = await app.state.job.get_job_list(job_id=payload.job_list_id)
|
|
31
|
+
results = [json.loads(await app.state.job.get_job(job_id)) for job_id in job_list]
|
|
32
|
+
|
|
33
|
+
# Logger
|
|
34
|
+
frame = inspect.currentframe()
|
|
35
|
+
logger = get_logger(frame.f_code.co_name)
|
|
36
|
+
|
|
37
|
+
# Delete the job list
|
|
38
|
+
# TODO: delete all job ids
|
|
39
|
+
await app.state.job.delete_job_list(job_id=payload.job_list_id)
|
|
40
|
+
|
|
41
|
+
if results:
|
|
42
|
+
await app.state.ws_manager.broadcast({
|
|
43
|
+
"type": "log",
|
|
44
|
+
"level": "success",
|
|
45
|
+
"message": f"Clear JobList {payload.job_list_id} {results[0]['folder']}",
|
|
46
|
+
})
|
|
47
|
+
logger.info(f"-> Clear JobList ID° {payload.job_list_id} {results[0]['folder']}\n")
|
|
48
|
+
else:
|
|
49
|
+
await app.state.ws_manager.broadcast({
|
|
50
|
+
"type": "log",
|
|
51
|
+
"level": "error",
|
|
52
|
+
"message": f"Clear JobList : JobList id not found",
|
|
53
|
+
})
|
|
54
|
+
logger.info(f"-> Clear JobList : JobList ID non trovato\n")
|