Unit3DwebUp 0.0.25__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.25 → unit3dwebup-0.0.26}/PKG-INFO +1 -1
  2. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/PKG-INFO +1 -1
  3. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/SOURCES.txt +11 -0
  4. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/pyproject.toml +1 -1
  5. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/__init__.py +1 -1
  6. unit3dwebup-0.0.26/unit3dwup/lifespan.py +154 -0
  7. {unit3dwebup-0.0.25/unit3dwup/services → unit3dwebup-0.0.26/unit3dwup/routers}/__init__.py +0 -0
  8. unit3dwebup-0.0.26/unit3dwup/routers/jobs.py +54 -0
  9. unit3dwebup-0.0.26/unit3dwup/routers/posters.py +110 -0
  10. unit3dwebup-0.0.26/unit3dwup/routers/process.py +90 -0
  11. unit3dwebup-0.0.26/unit3dwup/routers/scan.py +146 -0
  12. unit3dwebup-0.0.26/unit3dwup/routers/search.py +47 -0
  13. unit3dwebup-0.0.26/unit3dwup/routers/settings.py +151 -0
  14. unit3dwebup-0.0.26/unit3dwup/routers/ws.py +42 -0
  15. unit3dwebup-0.0.26/unit3dwup/schemas.py +33 -0
  16. unit3dwebup-0.0.26/unit3dwup/services/__init__.py +0 -0
  17. unit3dwebup-0.0.26/unit3dwup/start.py +37 -0
  18. unit3dwebup-0.0.25/unit3dwup/start.py → unit3dwebup-0.0.26/unit3dwup/start_backup.py +47 -14
  19. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/README.md +0 -0
  20. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/dependency_links.txt +0 -0
  21. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/entry_points.txt +0 -0
  22. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/requires.txt +0 -0
  23. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/Unit3DwebUp.egg-info/top_level.txt +0 -0
  24. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/setup.cfg +0 -0
  25. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/__init__.py +0 -0
  26. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/api_data.py +0 -0
  27. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/constants.py +0 -0
  28. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/host_data.py +0 -0
  29. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/itt.py +0 -0
  30. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/logger.py +0 -0
  31. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/settings.py +0 -0
  32. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/sis.py +0 -0
  33. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/tags.py +0 -0
  34. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/config/trackers.py +0 -0
  35. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/external/__init__.py +0 -0
  36. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/external/async_http_client_service.py +0 -0
  37. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/external/websocket.py +0 -0
  38. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/__init__.py +0 -0
  39. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/interfaces.py +0 -0
  40. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/keywords.py +0 -0
  41. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/media.py +0 -0
  42. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/media_info.py +0 -0
  43. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/movie.py +0 -0
  44. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/subtitles.py +0 -0
  45. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/tv.py +0 -0
  46. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/tvdb_search.py +0 -0
  47. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/models/videos.py +0 -0
  48. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/repositories/__init__.py +0 -0
  49. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/repositories/db_online.py +0 -0
  50. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/repositories/interfaces.py +0 -0
  51. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/repositories/job_repos.py +0 -0
  52. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/repositories/media_info_factory.py +0 -0
  53. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/auto_async_service.py +0 -0
  54. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/create_torrent_service.py +0 -0
  55. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/interfaces.py +0 -0
  56. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_helper.py +0 -0
  57. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/itt_tracker_service.py +0 -0
  58. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/lifespan_service.py +0 -0
  59. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/media_service.py +0 -0
  60. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/tags_service.py +0 -0
  61. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/tmdb.py +0 -0
  62. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_client_service.py +0 -0
  63. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/torrent_service.py +0 -0
  64. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/tvdb.py +0 -0
  65. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/utility.py +0 -0
  66. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/services/video_service.py +0 -0
  67. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/__init__.py +0 -0
  68. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/make_torrent_usecase.py +0 -0
  69. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/process_all_usecase.py +0 -0
  70. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/scan_media_usecase.py +0 -0
  71. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/seed_usecase.py +0 -0
  72. {unit3dwebup-0.0.25 → unit3dwebup-0.0.26}/unit3dwup/use_case/upload_usecase.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Unit3DwebUp
3
- Version: 0.0.25
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Unit3DwebUp
3
- Version: 0.0.25
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
@@ -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
@@ -36,6 +39,14 @@ unit3dwup/repositories/db_online.py
36
39
  unit3dwup/repositories/interfaces.py
37
40
  unit3dwup/repositories/job_repos.py
38
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
39
50
  unit3dwup/services/__init__.py
40
51
  unit3dwup/services/auto_async_service.py
41
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.25"
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.25"
10
+ __version__ = "0.0.26"
@@ -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()
@@ -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")
@@ -0,0 +1,110 @@
1
+ # -*- coding: utf-8 -*-
2
+ import inspect
3
+
4
+ from fastapi import APIRouter, Request
5
+
6
+ from unit3dwup.config import MediaStatus
7
+ from unit3dwup.config import get_logger
8
+
9
+ from unit3dwup.schemas import UpdatePosterRequest
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ async def update_poster(app, msg: str, job_id: str, field_id: str, new_id: str):
15
+ # Fix the DB ID with the received one
16
+ await app.state.job.update_job(job_id=job_id, new_data={field_id: new_id})
17
+
18
+ # Update the Media status
19
+ await app.state.job.update_job(job_id=job_id, new_data={'status': str(MediaStatus.DB_IDENTIFIED)})
20
+
21
+ # Logger
22
+ frame = inspect.currentframe()
23
+ logger = get_logger(frame.f_code.co_name)
24
+
25
+ # Console message
26
+ logger.info(f"-> Update {msg} JOB_ID: {job_id}\n")
27
+
28
+ # Send log to the client
29
+ await app.state.ws_manager.broadcast({
30
+ "type": "log",
31
+ "level": "success",
32
+ "message": f"Update {msg} JOB_ID {job_id}",
33
+ })
34
+
35
+
36
+ @router.post("/settmdbid")
37
+ async def set_poster_id(payload: UpdatePosterRequest, request: Request):
38
+ """
39
+ Set a Tmdb id for example when tmdb returns an empty result
40
+
41
+ Required
42
+ - job_id identifies each poster corresponds to Media.job_id
43
+
44
+ Return
45
+ - none
46
+ """
47
+
48
+ await update_poster(request.app, msg="TMDB", job_id=payload.job_id, field_id=payload.field_id, new_id=payload.new_id)
49
+
50
+
51
+ @router.post("/settvdbid")
52
+ async def set_tvdb_id(payload: UpdatePosterRequest, request: Request):
53
+ """
54
+ Set a TVdb id for example when tvdb returns an empty result
55
+
56
+ Required
57
+ - job_id identifies each poster corresponds to Media.job_id
58
+
59
+ Return
60
+ - none
61
+ """
62
+
63
+ await update_poster(request.app, msg="TVDB", job_id=payload.job_id, field_id=payload.field_id, new_id=payload.new_id)
64
+
65
+
66
+ @router.post("/setimdbid")
67
+ async def set_imdb_id(payload: UpdatePosterRequest, request: Request):
68
+ """
69
+ Set an Imdb id for example when the remote list of tvdb is empty
70
+
71
+ Required
72
+ - job_id identifies each poster corresponds to Media.job_id
73
+
74
+ Return
75
+ - none
76
+ """
77
+
78
+ await update_poster(request.app, msg="IMDB", job_id=payload.job_id, field_id=payload.field_id, new_id=payload.new_id)
79
+
80
+
81
+ @router.post("/setposterurl")
82
+ async def set_poster_url(payload: UpdatePosterRequest, request: Request):
83
+ """
84
+ Set a poster url for example when tmdb returns an empty result only for frontend
85
+
86
+ Required
87
+ - job_id identifies each poster corresponds to Media.job_id
88
+
89
+ Return
90
+ - none
91
+ """
92
+
93
+ await update_poster(request.app, msg="TMDB Poster Url", job_id=payload.job_id, field_id=payload.field_id, new_id=payload.new_id)
94
+
95
+
96
+ @router.post("/setposterdname")
97
+ async def set_poster_dname(payload: UpdatePosterRequest, request: Request):
98
+ """
99
+ Set a poster display name for example if you dont like it
100
+ Display name is the name shown on the dedicated torrent page
101
+
102
+ Required
103
+ - job_id identifies each poster corresponds to Media.job_id
104
+
105
+ Return
106
+ - none
107
+ """
108
+
109
+ await update_poster(request.app, msg="Update DisplayName", job_id=payload.job_id, field_id=payload.field_id,
110
+ new_id=payload.new_id)
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ from fastapi import APIRouter, Request
3
+ from fastapi.responses import JSONResponse
4
+
5
+ from unit3dwup.use_case.process_all_usecase import ProcessAllUseCase
6
+ from unit3dwup.use_case.upload_usecase import UploadUseCase
7
+ from unit3dwup.use_case.seed_usecase import SeedUseCase
8
+ from unit3dwup.use_case.make_torrent_usecase import MakeTorrentUseCase
9
+
10
+ from unit3dwup.schemas import ProcessAllRequest, JobRequest
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post("/processall")
16
+ async def process_all(payload: ProcessAllRequest, request: Request):
17
+ """
18
+ Start a chain load the joblist filter for existing torrent create torrent upload the complete joblist
19
+
20
+ Required
21
+ - job_list_id identifies the list created by the scan endpoint
22
+
23
+ Prerequisite
24
+ - valid description status media class attribute
25
+
26
+ Return
27
+ - none
28
+ """
29
+
30
+ app = request.app
31
+
32
+ use_case = ProcessAllUseCase(app=app, job_list_id=payload.job_list_id,
33
+ torrent_client_name=app.state.settings.torrent.TORRENT_CLIENT)
34
+ await use_case.execute()
35
+
36
+
37
+ @router.post("/maketorrent")
38
+ async def make(payload: JobRequest, request: Request):
39
+ """
40
+ Create one or more torrent files
41
+
42
+ Required
43
+ - job_id identifies each poster corresponds to Media.job_id
44
+
45
+ Return
46
+ - none
47
+ """
48
+ app = request.app
49
+
50
+ torrent_service = MakeTorrentUseCase(app=app, job_id=payload.job_id)
51
+ await torrent_service.execute()
52
+
53
+
54
+ @router.post("/upload")
55
+ async def upload(payload: JobRequest, request: Request):
56
+ """
57
+ Upload a single torrent file
58
+
59
+ Required
60
+ - job_id identifies each poster corresponds to Media.job_id
61
+
62
+ Return
63
+ - none
64
+
65
+ WebSocket events emitted
66
+ - posterLogMessage: sent for each uploaded torrent
67
+ - job_id: media identifier
68
+ - message: upload result message
69
+ """
70
+
71
+ app = request.app
72
+
73
+ upload_service = UploadUseCase(app=app, job_id=payload.job_id)
74
+ await upload_service.execute()
75
+
76
+
77
+ @router.post("/seed")
78
+ async def seed(payload: JobRequest, request: Request) -> JSONResponse:
79
+ """
80
+ Required
81
+ - job_id identifies each poster corresponds to Media.job_id
82
+
83
+ Return
84
+ - none
85
+ """
86
+
87
+ app = request.app
88
+
89
+ use_case = SeedUseCase(app=app, client=app.state.settings.torrent.TORRENT_CLIENT, job_id=payload.job_id)
90
+ return await use_case.execute()
@@ -0,0 +1,146 @@
1
+ # -*- coding: utf-8 -*-
2
+ import hashlib
3
+ import inspect
4
+ import json
5
+ import time
6
+
7
+ import aiohttp
8
+
9
+ from fastapi import APIRouter, Request, status
10
+ from fastapi.responses import JSONResponse
11
+
12
+ from unit3dwup.config import get_logger
13
+
14
+ from unit3dwup.repositories.db_online import Tmdb, Tvdb
15
+
16
+ from unit3dwup.services.media_service import MediaService, MediaService2
17
+ from unit3dwup.services.auto_async_service import AsyncMediaManager
18
+
19
+ from unit3dwup.use_case.scan_media_usecase import ScanMediaUseCase
20
+
21
+ from unit3dwup.schemas import ScanRequest
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ @router.post("/scan")
27
+ async def scan(payload: ScanRequest, request: Request) -> JSONResponse:
28
+ """
29
+ This endpoint scans the local files and creates a Media object for each associating it with its description
30
+
31
+ Required
32
+ - path: user path for the scan process, filesystem path on the hdd
33
+
34
+ Prerequisite
35
+ - path must be valid and accessible
36
+
37
+ Http response
38
+ - status: http status code
39
+ - source: indicates source of the posters local hdd or remote tracker
40
+ - results: dictionary containing source and list of Media objects
41
+
42
+ Websocket events
43
+ - type: log type for frontend display
44
+ - level: result of the process success or error
45
+ - message: message displayed in the frontend console
46
+ """
47
+
48
+ app = request.app
49
+
50
+ frame = inspect.currentframe()
51
+ logger = get_logger(frame.f_code.co_name)
52
+
53
+ if app.state.restart_docker:
54
+ return JSONResponse(
55
+ status_code=status.HTTP_403_FORBIDDEN,
56
+ content={
57
+ "source": "local",
58
+ "message": "Please restart the Docker container",
59
+ }
60
+ )
61
+
62
+ start_time = time.perf_counter()
63
+
64
+ # Get the id for the current path
65
+ job_list_id = hashlib.sha256(app.state.settings.prefs.SCAN_PATH.encode()).hexdigest()
66
+ logger.info(f"Current joblist_id {job_list_id} {app.state.settings.prefs.SCAN_PATH}")
67
+
68
+ # Load the jobs list using the previous id
69
+ job_list = await app.state.job.get_job_list(job_id=job_list_id)
70
+
71
+ # Load Media for each job id from the job_list
72
+ job_list_results = []
73
+ for job_id in job_list:
74
+ job = await app.state.job.get_job(job_id)
75
+ if job:
76
+ job_list_results.append(json.loads(job))
77
+
78
+ # New session
79
+ async with aiohttp.ClientSession() as session:
80
+
81
+ manager = AsyncMediaManager(
82
+ path=app.state.scan_path,
83
+ app=app,
84
+ job_id_list=job_list_id
85
+ )
86
+
87
+ # Instance repo ( or gateway..?) for each db online (TVDB, TMDB). Read imdb id from the tvdb remote_ids list
88
+ tvdb_repo = Tvdb(session=session)
89
+ tmdb_repo = Tmdb(session=session)
90
+
91
+ # Pass the repository to the MediaService class for async task purposes
92
+ media_service = MediaService(tmdb_repo)
93
+ media_service2 = MediaService2(tvdb_repo)
94
+
95
+ # Create a use_case
96
+ use_case = ScanMediaUseCase(
97
+ manager=manager,
98
+ media_service=media_service,
99
+ media_service2=media_service2,
100
+ job_repo=app.state.job,
101
+ session=session,
102
+ job_list=job_list
103
+ )
104
+
105
+ # Run all
106
+ results = await use_case.execute()
107
+
108
+ # Send a message to the frontend by ws
109
+ await app.state.ws_manager.broadcast({
110
+ "type": "log",
111
+ "level": "success",
112
+ "message": f"Scan completato in {time.perf_counter() - start_time:.2f} secondi",
113
+ })
114
+
115
+ # Analyze the results and build a new job_list with the new and old posters
116
+ build_results = []
117
+ new_job_list = []
118
+
119
+ for media in results:
120
+ # The current media object has no description because the job_id is already in the job_list
121
+ if not media.description:
122
+ for job in job_list_results:
123
+ if job['job_id'] == media.job_id:
124
+ build_results.append(job)
125
+ # append the old poster
126
+ new_job_list.append(job['job_id'])
127
+ else:
128
+ # new description. This is a new job_id
129
+ build_results.append(media.to_dict())
130
+ new_job_list.append(media.job_id)
131
+
132
+ if new_job_list:
133
+ # Save the new job_list
134
+ await app.state.job.create_job_list(
135
+ job_id=job_list_id,
136
+ job_list=new_job_list
137
+ )
138
+
139
+ # return to frontend the new posters
140
+ return JSONResponse(
141
+ status_code=status.HTTP_200_OK,
142
+ content={
143
+ "source": "local",
144
+ "results": build_results,
145
+ }
146
+ )
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+ import aiohttp
3
+
4
+ from fastapi import APIRouter, Request, status
5
+ from fastapi.responses import JSONResponse
6
+
7
+ from unit3dwup.services.itt_tracker_service import ITTtrackerService
8
+ from unit3dwup.services.interfaces import TrackerServiceInterface
9
+
10
+ from unit3dwup.schemas import FilterRequest
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post("/filter")
16
+ async def filter_search(payload: FilterRequest, request: Request):
17
+ """
18
+ Search words or title in the tracker
19
+
20
+ Required
21
+ - title: title or part of it
22
+
23
+ Return
24
+ - none
25
+ """
26
+
27
+ app = request.app
28
+
29
+ # Search for a title in the tracker
30
+ async with aiohttp.ClientSession() as session:
31
+ # A new ITT tracker instance with interface
32
+ tracker_service: TrackerServiceInterface = ITTtrackerService(session, app=app)
33
+
34
+ # Search
35
+ data = await tracker_service.search(payload.title)
36
+
37
+ # Return the source as remote ( change the bottom line color on the poster)
38
+ # job_id not applicable
39
+ # Extract attributes field for each data found
40
+ return JSONResponse(
41
+ status_code=status.HTTP_200_OK,
42
+ content={
43
+ "source": "remote",
44
+ "job_id": '-1',
45
+ "results": [c['attributes'] for c in data['data']],
46
+ }
47
+ )