yta-fastapi-docker-motion-generator 0.0.1__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 (16) hide show
  1. yta_fastapi_docker_motion_generator-0.0.1/LICENSE +19 -0
  2. yta_fastapi_docker_motion_generator-0.0.1/PKG-INFO +47 -0
  3. yta_fastapi_docker_motion_generator-0.0.1/README.md +26 -0
  4. yta_fastapi_docker_motion_generator-0.0.1/pyproject.toml +37 -0
  5. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/__init__.py +6 -0
  6. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/__init__.py +0 -0
  7. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/main.py +12 -0
  8. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/routers/__init__.py +24 -0
  9. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/routers/motion_resources/__init__.py +0 -0
  10. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/routers/motion_resources/images.py +59 -0
  11. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/routers/motion_resources/utils.py +58 -0
  12. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/routers/motion_resources/videos.py +40 -0
  13. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/services/__init__.py +0 -0
  14. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/services/motion_resources/__init__.py +438 -0
  15. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/services/motion_resources/custom.py +106 -0
  16. yta_fastapi_docker_motion_generator-0.0.1/src/yta_fastapi_docker_motion_generator/app/services/utils.py +65 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: yta-fastapi-docker-motion-generator
3
+ Version: 0.0.1
4
+ Summary: Youtube Autonomous FastAPI Docker Motion Generator Module
5
+ License-File: LICENSE
6
+ Author: danialcala94
7
+ Author-email: danielalcalavalera@gmail.com
8
+ Requires-Python: >=3.10,<3.14
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: fastapi (>=0.137.0,<9999.0.0)
15
+ Requires-Dist: uvicorn (>=0.49.0,<9999.0.0)
16
+ Requires-Dist: yta_fastapi_docker_pydantic_models (>=0.0.8,<1.0.0)
17
+ Requires-Dist: yta_general_utils (>=0.5.1,<1.0.0)
18
+ Requires-Dist: yta_programming_env (>=0.3.2,<1.0.0)
19
+ Requires-Dist: yta_web_scraper (>=0.3.7,<1.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Youtube Autonomous FastAPI Docker Motion Generator Module
23
+
24
+ The module that is providing the functionality related to the Motion Generator Web that are customized and generated to be downloaded using a scraper, to be lately provided through a FastAPI that is included and isolated in a Docker container but exposed to the internal and general FastAPI.
25
+
26
+ This project will use the `motion-generator-web` project to scrap it.
27
+
28
+ ### Endpoints
29
+
30
+ #### GET
31
+ No endpoints by now
32
+
33
+ #### POST
34
+ - `/motion-resources/images/booking-review`
35
+ - `name` - The name of the user that is creating the review
36
+ - `message` - The message we want to show in the review
37
+
38
+ - `/motion-resources/images/simple-progress-bar`
39
+ - `stripes_speed` - The speed of the stripes when loading.
40
+ - `duration` - The duration of the animation
41
+ - `fps` - The fps of the animation
42
+ - `frame` - The frame of the animation we want to download
43
+
44
+ - `/motion-resources/videos/simple-progress-bar`
45
+ - `stripes_speed` - The speed of the stripes when loading.
46
+ - `duration` - The duration of the animation
47
+ - `fps` - The fps of the animation
@@ -0,0 +1,26 @@
1
+ # Youtube Autonomous FastAPI Docker Motion Generator Module
2
+
3
+ The module that is providing the functionality related to the Motion Generator Web that are customized and generated to be downloaded using a scraper, to be lately provided through a FastAPI that is included and isolated in a Docker container but exposed to the internal and general FastAPI.
4
+
5
+ This project will use the `motion-generator-web` project to scrap it.
6
+
7
+ ### Endpoints
8
+
9
+ #### GET
10
+ No endpoints by now
11
+
12
+ #### POST
13
+ - `/motion-resources/images/booking-review`
14
+ - `name` - The name of the user that is creating the review
15
+ - `message` - The message we want to show in the review
16
+
17
+ - `/motion-resources/images/simple-progress-bar`
18
+ - `stripes_speed` - The speed of the stripes when loading.
19
+ - `duration` - The duration of the animation
20
+ - `fps` - The fps of the animation
21
+ - `frame` - The frame of the animation we want to download
22
+
23
+ - `/motion-resources/videos/simple-progress-bar`
24
+ - `stripes_speed` - The speed of the stripes when loading.
25
+ - `duration` - The duration of the animation
26
+ - `fps` - The fps of the animation
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "yta-fastapi-docker-motion-generator"
3
+ version = "0.0.1"
4
+ description = "Youtube Autonomous FastAPI Docker Motion Generator Module"
5
+ authors = [
6
+ {name = "danialcala94", email = "danielalcalavalera@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10,<3.14"
10
+
11
+ [tool.poetry.dependencies]
12
+ # Mandatory
13
+ yta_web_scraper = { version = ">=0.3.7,<1.0.0", optional = false }
14
+ yta_programming_env = { version = ">=0.3.2,<1.0.0", optional = false }
15
+ yta_general_utils = { version = ">=0.5.1,<1.0.0", optional = false }
16
+ fastapi = { version = ">=0.137.0,<9999.0.0", optional = false }
17
+ uvicorn = { version = ">=0.49.0,<9999.0.0", optional = false }
18
+ yta_fastapi_docker_pydantic_models = { version = ">=0.0.8,<1.0.0", optional = false }
19
+ # Optionalº
20
+
21
+ [tool.poetry]
22
+ packages = [{include = "yta_fastapi_docker_motion_generator", from = "src"}]
23
+
24
+ [tool.poetry.group.dev.dependencies]
25
+ pytest = "^8.3.5"
26
+ httpx = ">=0.28.1"
27
+ yta_testing = ">=0.3.6"
28
+
29
+ [tool.pytest.ini_options]
30
+ markers = [
31
+ "mandatory: mandatory tests for release",
32
+ "additional: exhaustive and demanding tests"
33
+ ]
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,6 @@
1
+ """
2
+ The magic use of html, css and javascript to
3
+ dynamically create resources that are stored
4
+ as images, with transparency, ready to be used
5
+ in our compositions and/or videos.
6
+ """
@@ -0,0 +1,12 @@
1
+ from yta_fastapi_docker_motion_generator.app.routers import router as general_router
2
+ from yta_fastapi_docker_motion_generator.app.routers.motion_resources.images import router as motion_resources_images_router
3
+ from yta_fastapi_docker_motion_generator.app.routers.motion_resources.videos import router as motion_resources_videos_router
4
+ from fastapi import FastAPI
5
+
6
+
7
+ app = FastAPI()
8
+
9
+ # Include all the routers we have
10
+ app.include_router(general_router)
11
+ app.include_router(motion_resources_images_router)
12
+ app.include_router(motion_resources_videos_router)
@@ -0,0 +1,24 @@
1
+ """
2
+ Check this project to get inspiration about FastAPI:
3
+ - https://github.com/Implosiv3/render-fastapi/blob/master/render-fastapi/routers/download/__init__.py
4
+ """
5
+ from fastapi import APIRouter
6
+ from fastapi.responses import JSONResponse
7
+
8
+
9
+ PREFIX = f''
10
+
11
+ router = APIRouter(
12
+ prefix = PREFIX
13
+ )
14
+
15
+ @router.get('/check-status')
16
+ def route_check_status(
17
+ ) -> JSONResponse:
18
+ return JSONResponse(
19
+ {
20
+ 'error': False,
21
+ 'message': 'Hello World!'
22
+ },
23
+ status_code = 200
24
+ )
@@ -0,0 +1,59 @@
1
+ """
2
+ This module is based on the `motion-generator-web`
3
+ project that is performing a React web page with
4
+ resources that can be static, downloadable as
5
+ images, or animated, downloadable as images or
6
+ videos.
7
+ """
8
+ from yta_fastapi_docker_motion_generator.app.routers.motion_resources.utils import create_temp_file_response, get_animation_kwargs_from_request
9
+ from yta_fastapi_docker_motion_generator.app.services.motion_resources.custom import BookingReviewMotionResource, SimpleProgressBarAnimatedMotionResource
10
+ from yta_fastapi_docker_pydantic_models.motion_resources.images import CreateBookingReview, CreateSimpleProgressBarImage
11
+ from fastapi.responses import FileResponse
12
+ from fastapi import APIRouter, BackgroundTasks
13
+
14
+
15
+ PREFIX = f'/motion-resources/images'
16
+
17
+ router = APIRouter(
18
+ prefix = PREFIX
19
+ )
20
+
21
+ MEDIA_TYPE = 'image/png'
22
+
23
+ @router.post('/booking-review')
24
+ async def post_booking_review(
25
+ request: CreateBookingReview,
26
+ background_tasks: BackgroundTasks
27
+ ) -> FileResponse:
28
+ return create_temp_file_response(
29
+ background_tasks = background_tasks,
30
+ download_callable = lambda temp_dir: BookingReviewMotionResource(
31
+ download_folder = temp_dir
32
+ ).download_image(
33
+ name = request.name,
34
+ text = request.message
35
+ ),
36
+ output_filename = 'booking_review.png',
37
+ media_type = MEDIA_TYPE
38
+ )
39
+
40
+
41
+ @router.post('/simple-progress-bar')
42
+ async def post_simple_progress_bar(
43
+ request: CreateSimpleProgressBarImage,
44
+ background_tasks: BackgroundTasks
45
+ ) -> FileResponse:
46
+ animation_kwars = get_animation_kwargs_from_request(request)
47
+
48
+ return create_temp_file_response(
49
+ background_tasks = background_tasks,
50
+ download_callable = lambda temp_dir: SimpleProgressBarAnimatedMotionResource(
51
+ download_folder = temp_dir
52
+ ).download_image(
53
+ stripes_speed = request.stripes_speed,
54
+ **animation_kwars,
55
+ frame = request.frame,
56
+ ),
57
+ output_filename = 'simple_progress_bar.png',
58
+ media_type = MEDIA_TYPE
59
+ )
@@ -0,0 +1,58 @@
1
+ from fastapi.responses import FileResponse
2
+
3
+ import tempfile
4
+ import shutil
5
+
6
+
7
+ def create_temp_file_response(
8
+ background_tasks: 'BackgroundTasks',
9
+ download_callable: callable,
10
+ output_filename: str,
11
+ media_type: str,
12
+ ) -> FileResponse:
13
+ """
14
+ Create a temporary directory to download the
15
+ file by calling the `download_callable` function
16
+ and return a `FileResponse` including it with
17
+ the `media_type` provided.
18
+
19
+ A background task will be added to remove the
20
+ temporary folder once the `FileResponse` has
21
+ been completely sent.
22
+ """
23
+ temp_dir = tempfile.mkdtemp()
24
+
25
+ background_tasks.add_task(
26
+ shutil.rmtree,
27
+ temp_dir,
28
+ ignore_errors = True
29
+ )
30
+
31
+ generated_file = download_callable(temp_dir)
32
+
33
+ return FileResponse(
34
+ generated_file,
35
+ media_type = media_type,
36
+ filename = output_filename
37
+ )
38
+
39
+
40
+ def get_animation_kwargs_from_request(
41
+ request
42
+ ) -> dict:
43
+ """
44
+ Get the animation kwargs that include the
45
+ `duration` and `fps` field if provided in the
46
+ request.
47
+
48
+ This is meant to be sent as `**kwargs` to the
49
+ model method in order to download with that
50
+ specific animation configuration.
51
+ """
52
+ animation_kwars = {}
53
+ if request.duration is not None:
54
+ animation_kwars['duration'] = request.duration
55
+ if request.fps is not None:
56
+ animation_kwars['fps'] = request.fps
57
+
58
+ return animation_kwars
@@ -0,0 +1,40 @@
1
+ """
2
+ This module is based on the `motion-generator-web`
3
+ project that is performing a React web page with
4
+ resources that can be static, downloadable as
5
+ images, or animated, downloadable as images or
6
+ videos.
7
+ """
8
+ from yta_fastapi_docker_motion_generator.app.routers.motion_resources.utils import create_temp_file_response, get_animation_kwargs_from_request
9
+ from yta_fastapi_docker_motion_generator.app.services.motion_resources.custom import SimpleProgressBarAnimatedMotionResource
10
+ from yta_fastapi_docker_pydantic_models.motion_resources.videos import CreateSimpleProgressBarVideo
11
+ from fastapi.responses import FileResponse
12
+ from fastapi import APIRouter, BackgroundTasks
13
+
14
+
15
+ PREFIX = f'/motion-resources/videos'
16
+
17
+ router = APIRouter(
18
+ prefix = PREFIX
19
+ )
20
+
21
+ MEDIA_TYPE = 'video/quicktime'
22
+
23
+ @router.post('/simple-progress-bar')
24
+ async def post_simple_progress_bar(
25
+ request: CreateSimpleProgressBarVideo,
26
+ background_tasks: BackgroundTasks
27
+ ) -> FileResponse:
28
+ animation_kwars = get_animation_kwargs_from_request(request)
29
+
30
+ return create_temp_file_response(
31
+ background_tasks = background_tasks,
32
+ download_callable = lambda temp_dir: SimpleProgressBarAnimatedMotionResource(
33
+ download_folder = temp_dir
34
+ ).download_video(
35
+ stripes_speed = request.stripes_speed,
36
+ **animation_kwars
37
+ ),
38
+ output_filename = 'simple_progress_bar.mov',
39
+ media_type = MEDIA_TYPE
40
+ )
@@ -0,0 +1,438 @@
1
+ from yta_fastapi_docker_motion_generator.app.services.utils import press_button_and_wait_downloaded
2
+ from yta_web_scraper.chrome import ChromeScraper
3
+ from yta_web_scraper.chrome.dataclasses.options_argument import NoSandboxChromeOptionsArgument, DisableDevShmUsageChromeOptionsArgument
4
+ from yta_web_scraper.chrome.dataclasses.experimental_options import SetDefaultDownloadDirectoryChromeExperimentalOption, SetDefaultSavefileDirectoryChromeExperimentalOption, DownloadDirectoryUpgradeChromeExperimentalOption, PromptForDownloadChromeExperimentalOption, SafebrowsingEnabledChromeExperimentalOption
5
+ from yta_general_utils.url.dataclasses import UrlParameters, UrlParameter
6
+ from yta_programming_env import Environment
7
+ from yta_validation.parameter import ParameterValidator
8
+ from typing import Union
9
+
10
+ import os
11
+ import shutil
12
+
13
+
14
+ """
15
+ TODO: This code could be refactored because the
16
+ `download_image` and `download_video` have a lot
17
+ of code that is duplicated. Please, do it.
18
+ """
19
+ class MotionResource:
20
+ """
21
+ A resource from the 'motion-generator-web' project
22
+ that can be scrapped to download it as an image.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ component_name: str,
28
+ do_use_gui: bool = False,
29
+ download_image_button_id: str = 'download-png',
30
+ # TODO: This hardcoded has to be changed
31
+ download_folder: str = 'C:/Users/dania/Downloads/',
32
+ timeout: float = 60.0
33
+ ):
34
+ # self.url: str = 'http://localhost:5173/editor-playground'
35
+ # This below is for docker
36
+ self.url: str = Environment.get_env('MOTION_RESOURCES_DOCKER_ENDPOINT')
37
+ # This below is for local
38
+ # self.url: str = Environment.get_current_project_env('MOTION_RESOURCES_DOCKER_ENDPOINT')
39
+ """
40
+ The url in which the resource is located.
41
+ """
42
+ self.component_name: str = component_name
43
+ """
44
+ The name of the component that identifies this
45
+ resource in the website and is able to load it
46
+ in order to modify and download it.
47
+ """
48
+ self.do_use_gui: bool = do_use_gui
49
+ """
50
+ Internal boolean variable to indicate if the
51
+ scraper will show or not the GUI when scrapping.
52
+ """
53
+ self._download_image_button_id: str = download_image_button_id
54
+ """
55
+ *For internal use only*
56
+
57
+ The id of the button to download the resource as
58
+ an image.
59
+ """
60
+ self._download_folder: str = download_folder
61
+ """
62
+ *For internal use only*
63
+
64
+ The name of the folder in which the scraper will
65
+ download the files.
66
+ """
67
+ self.timeout: float = timeout
68
+ """
69
+ The maximum amount of seconds that the scraper
70
+ will wait until the file is downloaded before
71
+ exiting with an error.
72
+ """
73
+
74
+ download_path = os.path.abspath(download_folder)
75
+
76
+ additional_options = [
77
+ NoSandboxChromeOptionsArgument(),
78
+ DisableDevShmUsageChromeOptionsArgument(),
79
+ # The ones below are to control the downloads
80
+ SetDefaultDownloadDirectoryChromeExperimentalOption(download_path),
81
+ # SetDefaultSavefileDirectoryChromeExperimentalOption(download_path),
82
+ PromptForDownloadChromeExperimentalOption(False),
83
+ DownloadDirectoryUpgradeChromeExperimentalOption(True),
84
+ SafebrowsingEnabledChromeExperimentalOption(True)
85
+ ]
86
+
87
+ self.scraper: ChromeScraper = ChromeScraper.init(
88
+ do_use_gui = do_use_gui,
89
+ additional_options = additional_options
90
+ )
91
+
92
+ def _get_url(
93
+ self,
94
+ parameters: Union[UrlParameters, None]
95
+ ) -> str:
96
+ """
97
+ *For internal use only*
98
+
99
+ Get the url but including the `parameters`
100
+ provided.
101
+ """
102
+ ParameterValidator.validate_instance_of('parameters', parameters, UrlParameters)
103
+
104
+ parameters = (
105
+ ''
106
+ if parameters is None else
107
+ f'?{parameters.encoded}'
108
+ )
109
+
110
+ return f'{self.url}{parameters}'
111
+
112
+ def _load(
113
+ self,
114
+ parameters: Union[UrlParameters, None] = None
115
+ ) -> None:
116
+ """
117
+ *For internal use only*
118
+
119
+ Navigates to the web page when not yet on it.
120
+ """
121
+ url = self._get_url(parameters)
122
+
123
+ if self.scraper.current_url != url:
124
+ self.scraper.go_to_web_and_wait_until_loaded(url)
125
+
126
+ def reload(
127
+ self
128
+ ) -> bool:
129
+ """
130
+ Force a refresh in the web page to reload it.
131
+ """
132
+ # TODO: It was previously like this below:
133
+ # return self.scraper.go_to_web_and_wait_until_loaded(self.url)
134
+ # Force to wait until completely loaded
135
+ return self.scraper.go_to_web_and_wait_until_loaded(self.scraper.current_url)
136
+
137
+ def _set_fields(
138
+ self,
139
+ fields: dict
140
+ ) -> None:
141
+ """
142
+ *For internal use only*
143
+
144
+ Set the `fields` provided for the web resource
145
+ by using the `.setField(name, value)` method
146
+ exposed in the web.
147
+ """
148
+ # # Wait until 'setField' is available
149
+ # from yta_web_scraper.chrome import WebDriverWait
150
+
151
+ # WebDriverWait(self.scraper.driver, 30).until(
152
+ # lambda d: d.execute_script(
153
+ # "return typeof window.setField === 'function'"
154
+ # )
155
+ # )
156
+
157
+ for field_key in fields:
158
+ self.scraper.execute_script(
159
+ f'window.setField(arguments[0], arguments[1])',
160
+ field_key,
161
+ fields[field_key]
162
+ )
163
+
164
+ def _set_animation_properties(
165
+ self,
166
+ duration: float = 1.0,
167
+ fps: int = 60
168
+ ) -> None:
169
+ """
170
+ *For internal use only*
171
+
172
+ Configure the video `duration` and `fps`
173
+ to download it later with these properties.
174
+ """
175
+ pass
176
+
177
+ def _set_frame(
178
+ self,
179
+ frame: int
180
+ ) -> None:
181
+ """
182
+ *For internal use only*
183
+
184
+ Configure the video `frame` in which we want
185
+ to position the resource. Useful when we want
186
+ to obtain an image of a specific frame.
187
+ """
188
+ pass
189
+
190
+ def _download(
191
+ self,
192
+ fields: dict,
193
+ download_button_id: str,
194
+ duration: float = 1.0,
195
+ fps: int = 60,
196
+ frame: int = 0,
197
+ output_filename: Union[str, None] = None
198
+ ) -> 'Path':
199
+ """
200
+ *For internal use only*
201
+
202
+ Download the resource as an image or as a video
203
+ depending on the `download_button_id` provided.
204
+ """
205
+ # 1st. Go to the webpage
206
+ self._load(
207
+ parameters = UrlParameters([
208
+ UrlParameter(
209
+ name = 'component',
210
+ value = self.component_name
211
+ )
212
+ ])
213
+ )
214
+
215
+ # 2nd. Set fields if needed
216
+ self._set_fields(fields)
217
+
218
+ # 3rd. Set animation properties
219
+ self._set_animation_properties(
220
+ duration = duration,
221
+ fps = fps
222
+ )
223
+
224
+ # 4th. Set frame if needed
225
+ self._set_frame(frame)
226
+
227
+ # 5th. Download the resource
228
+ button = self.scraper.find_element_by_id_waiting(download_button_id)
229
+ downloaded_file_path: 'Path' = press_button_and_wait_downloaded(
230
+ button = button,
231
+ download_dir = self._download_folder,
232
+ timeout = self.timeout
233
+ )
234
+
235
+ # 6th. Move to the 'output_filename' if provided
236
+ output_path = (
237
+ shutil.move(
238
+ downloaded_file_path,
239
+ # TODO: What if the extension is wrong (?)
240
+ output_filename
241
+ )
242
+ if output_filename else
243
+ downloaded_file_path
244
+ )
245
+
246
+ return output_path
247
+
248
+ def download_image(
249
+ self,
250
+ fields: dict,
251
+ output_filename: Union[str, None] = None
252
+ ) -> str:
253
+ """
254
+ Download the resource as an image with the
255
+ given `fields` and stored as the
256
+ `output_filename` provided.
257
+ """
258
+ return self._download(
259
+ fields = fields,
260
+ download_button_id = self._download_image_button_id,
261
+ duration = None,
262
+ fps = None,
263
+ output_filename = output_filename
264
+ )
265
+ # 1st. Go to the webpage
266
+ self._load(
267
+ parameters = UrlParameters([
268
+ UrlParameter(
269
+ name = 'component',
270
+ value = self.component_name
271
+ )
272
+ ])
273
+ )
274
+
275
+ # 2nd. Set fields if needed
276
+ self._set_fields(fields)
277
+
278
+ # 3rd. Ask for the download
279
+ button = self.scraper.find_element_by_id_waiting(self._download_image_button_id)
280
+ downloaded_file_path: 'Path' = press_button_and_wait_downloaded(
281
+ button = button,
282
+ download_dir = self._download_folder,
283
+ timeout = self.timeout
284
+ )
285
+
286
+ # 4th. Move to the 'output_filename' if provided
287
+ output_path = (
288
+ shutil.move(
289
+ downloaded_file_path,
290
+ # TODO: What if the extension is wrong (?)
291
+ output_filename
292
+ )
293
+ if output_filename else
294
+ downloaded_file_path
295
+ )
296
+
297
+ return output_path
298
+
299
+
300
+ class AnimatedMotionResource(MotionResource):
301
+ """
302
+ A resource from the 'motion-generator-web' project
303
+ that can be scrapped to download it as an image or
304
+ a video.
305
+ """
306
+
307
+ def __init__(
308
+ self,
309
+ component_name: str,
310
+ do_use_gui: bool = False,
311
+ download_image_button_id: str = 'download-png',
312
+ download_video_button_id: str = 'download-video',
313
+ # TODO: This hardcoded has to be changed
314
+ download_folder: str = 'C:/Users/dania/Downloads/',
315
+ timeout: float = 60.0
316
+ ):
317
+ super().__init__(
318
+ component_name = component_name,
319
+ do_use_gui = do_use_gui,
320
+ download_image_button_id = download_image_button_id,
321
+ download_folder = download_folder,
322
+ timeout = timeout
323
+ )
324
+
325
+ self._download_video_button_id: str = download_video_button_id
326
+ """
327
+ *For internal use only*
328
+
329
+ The id of the button to download the resource as
330
+ a video.
331
+ """
332
+
333
+ def _set_animation_properties(
334
+ self,
335
+ duration: float = 1.0,
336
+ fps: int = 60
337
+ ) -> None:
338
+ """
339
+ *For internal use only*
340
+
341
+ Configure the video `duration` and `fps`
342
+ to download it later with these properties.
343
+ """
344
+ self.scraper.execute_script(f'window.configureAnimation({{fps: {fps}, duration: {duration}}})')
345
+
346
+ def _set_frame(
347
+ self,
348
+ frame: int
349
+ ) -> None:
350
+ """
351
+ *For internal use only*
352
+
353
+ Configure the video `frame` in which we want
354
+ to position the resource. Useful when we want
355
+ to obtain an image of a specific frame.
356
+ """
357
+ self.scraper.execute_script(
358
+ f'window.setFrame(arguments[0])',
359
+ frame
360
+ )
361
+
362
+ def download_image(
363
+ self,
364
+ fields: dict,
365
+ duration: float = 1.0,
366
+ fps: int = 60,
367
+ frame: int = 0,
368
+ output_filename: Union[str, None] = None
369
+ ):
370
+ return super()._download(
371
+ fields = fields,
372
+ download_button_id = self._download_image_button_id,
373
+ duration = duration,
374
+ fps = fps,
375
+ frame = frame,
376
+ output_filename = output_filename
377
+ )
378
+
379
+ def download_video(
380
+ self,
381
+ fields: dict,
382
+ duration: float = 1.0,
383
+ fps: int = 60,
384
+ output_filename: Union[str, None] = None
385
+ ) -> str:
386
+ """
387
+ Download the resource as a video of
388
+ `duration` seconds, with `fps`, the
389
+ given `fields` and stored as the
390
+ `output_filename` provided.
391
+ """
392
+ return super()._download(
393
+ fields = fields,
394
+ download_button_id = self._download_video_button_id,
395
+ duration = duration,
396
+ fps = fps,
397
+ frame = 0,
398
+ output_filename = output_filename
399
+ )
400
+ # 1st. Go to the webpage
401
+ self._load(
402
+ parameters = UrlParameters([
403
+ UrlParameter(
404
+ name = 'component',
405
+ value = self.component_name
406
+ )
407
+ ])
408
+ )
409
+
410
+ # 2nd. Set fields if needed
411
+ self._set_fields(fields)
412
+
413
+ # 3rd. Set animation properties
414
+ self._set_animation_properties(
415
+ duration = duration,
416
+ fps = fps
417
+ )
418
+
419
+ # 3rd. Ask for the download
420
+ button = self.scraper.find_element_by_id_waiting(self._download_video_button_id)
421
+ downloaded_file_path: 'Path' = press_button_and_wait_downloaded(
422
+ button = button,
423
+ download_dir = self._download_folder,
424
+ timeout = self.timeout
425
+ )
426
+
427
+ # TODO: This should be a parameter in a reusable method
428
+ # TODO: Why if respecting not the extension (?)
429
+ # output_filename = f'test_files/{downloaded_file_path.name}'
430
+
431
+ # TODO: Not sure if this will work in Docker
432
+ # 4th. Move to the 'output_filename' provided
433
+ shutil.move(
434
+ downloaded_file_path,
435
+ output_filename
436
+ )
437
+
438
+ return output_filename
@@ -0,0 +1,106 @@
1
+ from yta_fastapi_docker_motion_generator.app.services.motion_resources import MotionResource, AnimatedMotionResource
2
+ from yta_programming_env import Environment
3
+ from typing import Union
4
+
5
+
6
+ class BookingReviewMotionResource(MotionResource):
7
+ """
8
+ A static resource that is a Booking Review.
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ download_folder: str = 'C:/Users/dania/Downloads/',
14
+ timeout: float = 60.0
15
+ ):
16
+ super().__init__(
17
+ component_name = 'booking-review',
18
+ do_use_gui = False,
19
+ download_image_button_id = 'download-png',
20
+ download_folder = download_folder,
21
+ timeout = timeout
22
+ )
23
+
24
+ def download_image(
25
+ self,
26
+ name: str = 'María',
27
+ country: str = 'España',
28
+ score: str = '9.2',
29
+ scoreLabel: str = 'Fantástico',
30
+ title: str = 'Excelente ubicación',
31
+ text: str = 'El hotel estaba muy limpio, el personal fue muy amable y la ubicación era perfecta para visitar el centro de la ciudad.',
32
+ date: str = '14 de mayo de 2026',
33
+ output_filename: Union[str, None] = None
34
+ ):
35
+ fields = {
36
+ 'name': name,
37
+ 'country': country,
38
+ 'score': score,
39
+ 'scoreLabel': scoreLabel,
40
+ 'title': title,
41
+ 'text': text,
42
+ 'date': date
43
+ }
44
+
45
+ return super().download_image(
46
+ fields = fields,
47
+ output_filename = output_filename
48
+ )
49
+
50
+
51
+ class SimpleProgressBarAnimatedMotionResource(AnimatedMotionResource):
52
+ """
53
+ An animated progress bar
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ download_folder: str = 'C:/Users/dania/Downloads/',
59
+ timeout: float = 60.0
60
+ ):
61
+ super().__init__(
62
+ component_name = 'simple-progress-bar',
63
+ do_use_gui = False,
64
+ download_image_button_id = 'download-png',
65
+ download_video_button_id = 'download-video',
66
+ download_folder = download_folder,
67
+ timeout = timeout
68
+ )
69
+
70
+ def download_image(
71
+ self,
72
+ stripes_speed: float = 1.0,
73
+ duration: float = 1.0,
74
+ fps: int = 60,
75
+ frame: int = 0,
76
+ output_filename: Union[str, None] = None,
77
+ ):
78
+ fields = {
79
+ 'stripesSpeed': stripes_speed,
80
+ }
81
+
82
+ return super().download_image(
83
+ fields = fields,
84
+ duration = duration,
85
+ fps = fps,
86
+ frame = frame,
87
+ output_filename = output_filename
88
+ )
89
+
90
+ def download_video(
91
+ self,
92
+ stripes_speed: float = 1.0,
93
+ duration: float = 1.0,
94
+ fps: int = 60,
95
+ output_filename: Union[str, None] = None,
96
+ ):
97
+ fields = {
98
+ 'stripesSpeed': stripes_speed,
99
+ }
100
+
101
+ return super().download_video(
102
+ fields = fields,
103
+ duration = duration,
104
+ fps = fps,
105
+ output_filename = output_filename
106
+ )
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+
3
+ import time
4
+
5
+
6
+ def press_button_and_wait_downloaded(
7
+ button: 'WebElement',
8
+ download_dir: str,
9
+ timeout: float = 60.0
10
+ ) -> Path:
11
+ """
12
+ Press the `button` and waits until the file
13
+ has been downloaded.
14
+
15
+ This method will check the files that were
16
+ existing before pressing the button and will
17
+ wait until the new one appears (and it is a
18
+ complete download and not a temporary file).
19
+
20
+ The waiting process will last `timeout`
21
+ seconds as maximum.
22
+
23
+ This method will return the downloaded file
24
+ path.
25
+ """
26
+ downloads = Path(download_dir)
27
+
28
+ files_before_download = {
29
+ f.name
30
+ for f in downloads.iterdir()
31
+ }
32
+
33
+ button.click()
34
+ time_elapsed = 0.0
35
+ time_sleep_interval = 0.5
36
+
37
+ while True:
38
+ current = list(downloads.iterdir())
39
+
40
+ new_files = [
41
+ f
42
+ for f in current
43
+ if f.name not in files_before_download
44
+ ]
45
+
46
+ files_completed = [
47
+ f for f in new_files
48
+ if (
49
+ not f.name.endswith('.crdownload') and
50
+ not f.name.endswith('.tmp')
51
+ )
52
+ ]
53
+
54
+ if files_completed:
55
+ downloaded_file = files_completed[0]
56
+ break
57
+
58
+ time.sleep(time_sleep_interval)
59
+ time_elapsed += time_sleep_interval
60
+
61
+ if (time_elapsed >= timeout):
62
+ # TODO: Raise an exception (?)
63
+ break
64
+
65
+ return downloaded_file