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.
Files changed (72) hide show
  1. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/PKG-INFO +2 -2
  2. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/README.md +1 -1
  3. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/PKG-INFO +2 -2
  4. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/SOURCES.txt +12 -0
  5. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/pyproject.toml +1 -1
  6. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/__init__.py +1 -1
  7. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/settings.py +23 -14
  8. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/tags.py +5 -0
  9. unit3dwebup-0.0.26/unit3dwup/lifespan.py +154 -0
  10. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/media.py +51 -31
  11. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/media_info.py +26 -8
  12. unit3dwebup-0.0.26/unit3dwup/models/subtitles.py +17 -0
  13. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/media_info_factory.py +2 -0
  14. {unit3dwebup-0.0.23/unit3dwup/services → unit3dwebup-0.0.26/unit3dwup/routers}/__init__.py +0 -0
  15. unit3dwebup-0.0.26/unit3dwup/routers/jobs.py +54 -0
  16. unit3dwebup-0.0.26/unit3dwup/routers/posters.py +110 -0
  17. unit3dwebup-0.0.26/unit3dwup/routers/process.py +90 -0
  18. unit3dwebup-0.0.26/unit3dwup/routers/scan.py +146 -0
  19. unit3dwebup-0.0.26/unit3dwup/routers/search.py +47 -0
  20. unit3dwebup-0.0.26/unit3dwup/routers/settings.py +151 -0
  21. unit3dwebup-0.0.26/unit3dwup/routers/ws.py +42 -0
  22. unit3dwebup-0.0.26/unit3dwup/schemas.py +33 -0
  23. unit3dwebup-0.0.26/unit3dwup/services/__init__.py +0 -0
  24. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/create_torrent_service.py +1 -1
  25. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_helper.py +2 -0
  26. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_service.py +18 -8
  27. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tags_service.py +57 -40
  28. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tmdb.py +19 -21
  29. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/utility.py +4 -0
  30. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/video_service.py +3 -3
  31. unit3dwebup-0.0.26/unit3dwup/start.py +37 -0
  32. unit3dwebup-0.0.23/unit3dwup/start.py → unit3dwebup-0.0.26/unit3dwup/start_backup.py +66 -17
  33. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/upload_usecase.py +2 -1
  34. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/dependency_links.txt +0 -0
  35. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/entry_points.txt +0 -0
  36. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/requires.txt +0 -0
  37. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/top_level.txt +0 -0
  38. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/setup.cfg +0 -0
  39. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/__init__.py +0 -0
  40. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/api_data.py +0 -0
  41. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/constants.py +0 -0
  42. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/host_data.py +0 -0
  43. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/itt.py +0 -0
  44. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/logger.py +0 -0
  45. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/sis.py +0 -0
  46. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/config/trackers.py +0 -0
  47. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/__init__.py +0 -0
  48. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/async_http_client_service.py +0 -0
  49. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/external/websocket.py +0 -0
  50. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/__init__.py +0 -0
  51. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/interfaces.py +0 -0
  52. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/keywords.py +0 -0
  53. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/movie.py +0 -0
  54. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/tv.py +0 -0
  55. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/tvdb_search.py +0 -0
  56. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/models/videos.py +0 -0
  57. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/__init__.py +0 -0
  58. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/db_online.py +0 -0
  59. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/interfaces.py +0 -0
  60. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/repositories/job_repos.py +0 -0
  61. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/auto_async_service.py +0 -0
  62. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/interfaces.py +0 -0
  63. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/lifespan_service.py +0 -0
  64. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/media_service.py +0 -0
  65. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_client_service.py +0 -0
  66. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_service.py +0 -0
  67. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/services/tvdb.py +0 -0
  68. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/__init__.py +0 -0
  69. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/make_torrent_usecase.py +0 -0
  70. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/process_all_usecase.py +0 -0
  71. {unit3dwebup-0.0.23 → unit3dwebup-0.0.26}/unit3dwup/use_case/scan_media_usecase.py +0 -0
  72. {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.23
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
- # Unit3DupWeb
28
+ # Unit3DwebUp
29
29
 
30
30
  ![version](https://img.shields.io/pypi/v/unit3dupWeb.svg)
31
31
  ![online](https://img.shields.io/badge/Online-green)
@@ -1,4 +1,4 @@
1
- # Unit3DupWeb
1
+ # Unit3DwebUp
2
2
 
3
3
  ![version](https://img.shields.io/pypi/v/unit3dupWeb.svg)
4
4
  ![online](https://img.shields.io/badge/Online-green)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Unit3DwebUp
3
- Version: 0.0.23
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
- # Unit3DupWeb
28
+ # Unit3DwebUp
29
29
 
30
30
  ![version](https://img.shields.io/pypi/v/unit3dupWeb.svg)
31
31
  ![online](https://img.shields.io/badge/Online-green)
@@ -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
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "uvicorn[standard]==0.40.0"
25
25
  ]
26
26
  name = "Unit3DwebUp"
27
- version = "0.0.23"
27
+ version = "0.0.26"
28
28
 
29
29
  description = "A Unit3D uploader with a web interface"
30
30
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -7,4 +7,4 @@ from .settings import get_settings
7
7
  from .constants import MediaStatus
8
8
  from .logger import get_logger
9
9
 
10
- __version__ = "0.0.23"
10
+ __version__ = "0.0.26"
@@ -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"{info.field_name} too short")
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 | None = "all"
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= ENV_FILE if not os.getenv("DOCKER") else None,
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} value not set or invalid")
199
- logger.warning("-" * 50)
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
- self._guess_episode = int(str(self.guess_filename.guessit_episode))
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 resolution(self) -> str | None:
509
- if not self._resolution:
510
- if self.mediafile and self.mediafile.video_height:
511
- # Pick the highest standard resolution whose height fits the
512
- # video. The previous `min(abs())` heuristic mis-classified
513
- # 1080p widescreen content as 720p (a 1080p movie in 2.39:1
514
- # has a video_height of ~800, which is closer to 720 than to
515
- # 1080). A 50px tolerance rounds up when the height is just
516
- # below a step (cropped masters, encoder padding).
517
- tolerance = 50
518
- ladder = sorted(
519
- (int(r) for r in System.RESOLUTIONS), reverse=True
520
- )
521
- height = int(self.mediafile.video_height)
522
- chosen = ladder[-1]
523
- for step in ladder:
524
- if height >= step - tolerance:
525
- chosen = step
526
- break
527
- closest_resolution = str(chosen)
528
- scan_type = self.mediafile.video_scan_type
529
- if scan_type:
530
- closest_resolution = f"{closest_resolution}p" if scan_type.lower() == "progressive" else f"{closest_resolution}i"
531
- else:
532
- closest_resolution = f"{closest_resolution}i" if self.mediafile.is_interlaced else f"{closest_resolution}p"
533
- self._resolution = closest_resolution
534
- else:
535
- self._resolution = System.NO_RESOLUTION
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 subtitle_tracks(self) -> List[Dict]:
86
- return [t for t in self.video_tracks if t.get("track_type") == "Text"]
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[Any | None] | list[str]:
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
  )
@@ -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")