Skip to content

Commit f3fd16c

Browse files
Implement CLI logging and fix the cannot identify image file issue (GH-58)
2 parents 43594bb + ab3193b commit f3fd16c

14 files changed

+146
-60
lines changed

.github/workflows/tests.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
name: Tests Ubuntu | MacOS
22

33
on:
4-
- pull_request
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
58

69
jobs:
710
test:

README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Run the following command in the package's root directory to install it in edita
6868
python3 -m pip install -e .
6969
```
7070
This command will install the package in your local environment and allow you to make changes to the code and see the
71-
updates immediately. It will also install all the required dependencies listed in the [requirements.txt](requirements.txt) file.
71+
updates immediately. It will also install all the required dependencies.
7272

7373
## Contribute
7474

@@ -85,6 +85,14 @@ install [tox](https://github.com/tox-dev/tox) and run the tests for configured e
8585
python3 -m pip install tox && tox
8686
```
8787

88+
If you want to run tests only for the current environment with your local Python interpreter, you can use the following
89+
commands.
90+
91+
```bash
92+
python3 -m pip install -r tests/requirements.txt
93+
python3 -m pytest
94+
```
95+
8896
## License
8997

90-
Copyright (C) 2023 Artyom Vancyan. [Apache 2.0](LICENSE)
98+
Copyright (C) 2023 Artyom Vancyan. [Apache 2.0](https://github.com/pysnippet/thumbnails/blob/master/LICENSE)

requirements.txt

-5
This file was deleted.

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ install_requires =
3535
imageio-ffmpeg>=0.4.7
3636
imageio>=2.23.0
3737
pillow>=8.4.0
38+
rich>=13.0.0
3839
python_requires = >=3.7
3940
package_dir =
4041
=src

src/thumbnails/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .thumbnail import ThumbnailVTT
2323
from .thumbnail import register_thumbnail
2424

25-
__version__ = "0.1.5"
25+
__version__ = "0.1.6"
2626
__all__ = (
2727
"Generator",
2828
"Thumbnail",

src/thumbnails/ffmpeg.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ class _FFMpeg:
1212
"""This class is used to parse the metadata of a video file."""
1313

1414
def __init__(self, filepath):
15-
duration, self.size = self._parse_metadata(filepath)
16-
self.duration = int(duration + 1)
15+
self.duration, self.size = self._parse_metadata(filepath)
1716

1817
@staticmethod
1918
def _parse_duration(stdout):

src/thumbnails/generator.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .constants import DEFAULT_SKIP
1313
from .pathtools import listdir
1414
from .pathtools import metadata_path
15+
from .progress import use_progress
1516
from .thumbnail import ThumbnailExistsError
1617
from .thumbnail import ThumbnailFactory
1718
from .video import Video
@@ -45,6 +46,7 @@ def worker(video, fmt, base, skip, output):
4546
thumbnail.prepare_frames()
4647
thumbnail.generate()
4748

49+
@use_progress
4850
def generate(self):
4951
self.inputs = [file for file in self.inputs if re.match(r"^.*\.(?:(?!png|vtt|json).)+$", file)]
5052
self.inputs = dict(zip(map(lambda i: metadata_path(i, self.output, self.format), self.inputs), self.inputs))
@@ -59,7 +61,7 @@ def generate(self):
5961
self.inputs.values(),
6062
)
6163

62-
with concurrent.futures.ProcessPoolExecutor() as executor:
64+
with concurrent.futures.ThreadPoolExecutor() as executor:
6365
executor.map(
6466
functools.partial(
6567
self.worker,

src/thumbnails/progress.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import functools
2+
3+
from rich.progress import Progress as RichProgress
4+
from rich.progress import SpinnerColumn
5+
from rich.progress import TextColumn
6+
7+
8+
class Progress:
9+
_running = False
10+
_instance = RichProgress(SpinnerColumn(finished_text="*"),
11+
TextColumn("[white]{task.description}{task.fields[status]}"))
12+
13+
def __init__(self, description):
14+
if self._running:
15+
self.task = self._instance.add_task(description, status=" ... [yellow]processing")
16+
self.description = description
17+
18+
def update(self, description, status=" ... [yellow]processing"):
19+
if self._running:
20+
self._instance.update(self.task, description=description, status=status, refresh=True)
21+
22+
@classmethod
23+
def start(cls):
24+
cls._running = True
25+
cls._instance.start()
26+
27+
@classmethod
28+
def stop(cls):
29+
cls._running = False
30+
cls._instance.stop()
31+
32+
def __enter__(self):
33+
return self
34+
35+
def __exit__(self, exc_type, exc_val, _):
36+
# Set finished time to 0 to hide the spinner
37+
self._instance.tasks[self.task].finished_time = 0
38+
if exc_type is not None:
39+
return self.update(exc_val, status=" ... [red]failure")
40+
self.update(self.description, status=" ... [green]success")
41+
42+
43+
def use_progress(func):
44+
"""Decorator for using the progress bar."""
45+
46+
@functools.wraps(func)
47+
def wrapper(*args, **kwargs):
48+
Progress.start()
49+
func(*args, **kwargs)
50+
Progress.stop()
51+
52+
return wrapper

src/thumbnails/thumbnail.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .pathtools import ensure_tree
1313
from .pathtools import extract_name
1414
from .pathtools import metadata_path
15+
from .progress import Progress
1516

1617

1718
def register_thumbnail(typename):
@@ -45,7 +46,9 @@ def __init__(self, video, base, skip, output):
4546
self.thumbnail_dir = self.calc_thumbnail_dir()
4647
self.metadata_path = self._get_metadata_path()
4748
self._perform_skip()
48-
self.extract_frames()
49+
50+
with Progress("Extracting the frames by the given interval"):
51+
self.extract_frames()
4952

5053
def _get_metadata_path(self):
5154
"""Initiates the name of the thumbnail metadata file."""
@@ -100,12 +103,16 @@ def prepare_frames(self):
100103
master = Image.new(mode="RGBA", size=next(thumbnails))
101104
master_path = os.path.join(self.thumbnail_dir, extract_name(self.filepath) + ".png")
102105

103-
for frame, *_, x, y in thumbnails:
104-
with Image.open(frame) as image:
105-
image = image.resize((self.width, self.height), Image.ANTIALIAS)
106-
master.paste(image, (x, y))
106+
with Progress("Preprocessing the frames before merging") as progress:
107+
for frame, *_, x, y in thumbnails:
108+
offset = extract_name(frame).replace("-", ":").split(".")[0]
109+
progress.update("Processing [bold]%s[/bold] frame" % offset)
110+
with Image.open(frame) as image:
111+
image = image.resize((self.width, self.height), Image.ANTIALIAS)
112+
master.paste(image, (x, y))
107113

108-
master.save(master_path)
114+
with Progress("Saving the result at '%s'" % master_path):
115+
master.save(master_path)
109116

110117
def generate(self):
111118
def format_time(secs):
@@ -117,12 +124,14 @@ def format_time(secs):
117124
route = os.path.join(prefix, extract_name(self.filepath) + ".png")
118125
route = pathlib.Path(route).as_posix()
119126

120-
for _, start, end, x, y in self.thumbnails():
121-
thumbnail_data = "%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n" % (
122-
format_time(start), format_time(end),
123-
route, x, y, self.width, self.height,
124-
)
125-
metadata.append(thumbnail_data)
127+
with Progress("Saving thumbnail metadata at '%s'" % self.metadata_path) as progress:
128+
for _, start, end, x, y in self.thumbnails():
129+
progress.update("Generating metadata for '%s'" % route)
130+
thumbnail_data = "%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n" % (
131+
format_time(start), format_time(end),
132+
route, x, y, self.width, self.height,
133+
)
134+
metadata.append(thumbnail_data)
126135

127136
with open(self.metadata_path, "w") as fp:
128137
fp.writelines(metadata)

src/thumbnails/video.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .ffmpeg import _FFMpeg
1212
from .frame import _Frame
13+
from .progress import Progress
1314

1415
ffmpeg_bin = get_ffmpeg_exe()
1516

@@ -19,7 +20,7 @@ def arange(start, stop, step):
1920

2021
def _generator():
2122
nonlocal start
22-
while start < stop:
23+
while start <= stop:
2324
yield start
2425
start += step
2526

@@ -41,8 +42,9 @@ def __init__(self, filepath, compress, interval):
4142
self.__frames_count = None
4243
self.__columns = None
4344

44-
_FFMpeg.__init__(self, filepath)
45-
_Frame.__init__(self, self.size)
45+
with Progress("Parsing metadata from the video"):
46+
_FFMpeg.__init__(self, filepath)
47+
_Frame.__init__(self, self.size)
4648

4749
@property
4850
def filepath(self):
@@ -77,6 +79,7 @@ def calc_columns(self):
7779
for col in range(1, self.frames_count):
7880
if (col * width) / (self.frames_count // col * height) > ratio:
7981
return col
82+
return 1 # fixes the case when the video is too short
8083

8184
def _extract_frame(self, start_time):
8285
"""Extracts a single frame from the video by the offset."""

tests/data/snapshots/specified-base-json

+27-19
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,52 @@
33
"src": "/media/thumbnails/video/0-00-00.png",
44
"width": "1280px"
55
},
6-
"10": {
7-
"src": "/media/thumbnails/video/0-00-10.png",
6+
"8": {
7+
"src": "/media/thumbnails/video/0-00-08.200000.png",
88
"width": "1280px"
99
},
10-
"20": {
11-
"src": "/media/thumbnails/video/0-00-20.png",
10+
"16": {
11+
"src": "/media/thumbnails/video/0-00-16.400000.png",
1212
"width": "1280px"
1313
},
14-
"30": {
15-
"src": "/media/thumbnails/video/0-00-30.png",
14+
"24": {
15+
"src": "/media/thumbnails/video/0-00-24.600000.png",
1616
"width": "1280px"
1717
},
18-
"40": {
19-
"src": "/media/thumbnails/video/0-00-40.png",
18+
"32": {
19+
"src": "/media/thumbnails/video/0-00-32.800000.png",
2020
"width": "1280px"
2121
},
22-
"50": {
23-
"src": "/media/thumbnails/video/0-00-50.png",
22+
"41": {
23+
"src": "/media/thumbnails/video/0-00-41.png",
2424
"width": "1280px"
2525
},
26-
"60": {
27-
"src": "/media/thumbnails/video/0-01-00.png",
26+
"49": {
27+
"src": "/media/thumbnails/video/0-00-49.200000.png",
2828
"width": "1280px"
2929
},
30-
"70": {
31-
"src": "/media/thumbnails/video/0-01-10.png",
30+
"57": {
31+
"src": "/media/thumbnails/video/0-00-57.400000.png",
3232
"width": "1280px"
3333
},
34-
"80": {
35-
"src": "/media/thumbnails/video/0-01-20.png",
34+
"65": {
35+
"src": "/media/thumbnails/video/0-01-05.600000.png",
36+
"width": "1280px"
37+
},
38+
"73": {
39+
"src": "/media/thumbnails/video/0-01-13.800000.png",
40+
"width": "1280px"
41+
},
42+
"82": {
43+
"src": "/media/thumbnails/video/0-01-22.png",
3644
"width": "1280px"
3745
},
3846
"90": {
39-
"src": "/media/thumbnails/video/0-01-30.png",
47+
"src": "/media/thumbnails/video/0-01-30.200000.png",
4048
"width": "1280px"
4149
},
42-
"100": {
43-
"src": "/media/thumbnails/video/0-01-40.png",
50+
"98": {
51+
"src": "/media/thumbnails/video/0-01-38.400000.png",
4452
"width": "1280px"
4553
}
4654
}
+17-11
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
11
WEBVTT
22

3-
00:00:00.000 --> 00:00:10.000
3+
00:00:00.000 --> 00:00:08.200
44
/media/thumbnails/video.png#xywh=0,0,1280,536
55

6-
00:00:10.000 --> 00:00:20.000
6+
00:00:08.200 --> 00:00:16.400
77
/media/thumbnails/video.png#xywh=1280,0,1280,536
88

9-
00:00:20.000 --> 00:00:30.000
9+
00:00:16.400 --> 00:00:24.600
1010
/media/thumbnails/video.png#xywh=2560,0,1280,536
1111

12-
00:00:30.000 --> 00:00:40.000
12+
00:00:24.600 --> 00:00:32.800
1313
/media/thumbnails/video.png#xywh=0,536,1280,536
1414

15-
00:00:40.000 --> 00:00:50.000
15+
00:00:32.800 --> 00:00:41.000
1616
/media/thumbnails/video.png#xywh=1280,536,1280,536
1717

18-
00:00:50.000 --> 00:01:00.000
18+
00:00:41.000 --> 00:00:49.200
1919
/media/thumbnails/video.png#xywh=2560,536,1280,536
2020

21-
00:01:00.000 --> 00:01:10.000
21+
00:00:49.200 --> 00:00:57.400
2222
/media/thumbnails/video.png#xywh=0,1072,1280,536
2323

24-
00:01:10.000 --> 00:01:20.000
24+
00:00:57.400 --> 00:01:05.600
2525
/media/thumbnails/video.png#xywh=1280,1072,1280,536
2626

27-
00:01:20.000 --> 00:01:30.000
27+
00:01:05.600 --> 00:01:13.800
2828
/media/thumbnails/video.png#xywh=2560,1072,1280,536
2929

30-
00:01:30.000 --> 00:01:40.000
30+
00:01:13.800 --> 00:01:22.000
3131
/media/thumbnails/video.png#xywh=0,1608,1280,536
3232

33-
00:01:40.000 --> 00:01:50.000
33+
00:01:22.000 --> 00:01:30.200
3434
/media/thumbnails/video.png#xywh=1280,1608,1280,536
3535

36+
00:01:30.200 --> 00:01:38.400
37+
/media/thumbnails/video.png#xywh=2560,1608,1280,536
38+
39+
00:01:38.400 --> 00:01:46.600
40+
/media/thumbnails/video.png#xywh=0,2144,1280,536
41+

tests/test_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ def thumbnail_generation_with_with_extras(tmp_media, inputs, fmt):
3838
generator.output = os.path.join(tmp_media, "thumbnails")
3939
generator.format = fmt
4040
generator.compress = 0.5
41-
generator.interval = 10
41+
generator.interval = 8.2
4242
generator.generate()
4343

4444
snapshot = open(os.path.join(tmp_media, "snapshots", "specified-base-%s" % fmt))
4545
result = open(os.path.join(tmp_media, "thumbnails", "video.%s" % fmt))
4646

4747
if fmt == "json":
48-
assert len(os.listdir(os.path.join(tmp_media, "thumbnails", "video"))) == 11
48+
assert len(os.listdir(os.path.join(tmp_media, "thumbnails", "video"))) == 13
4949
assert snapshot.read() == result.read()
5050

5151
snapshot.close()

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ setenv =
1212
deps =
1313
-r{toxinidir}/tests/requirements.txt
1414
commands =
15-
pip install -r requirements.txt
15+
pip install -e .
1616
pytest --basetemp={envtmpdir}

0 commit comments

Comments
 (0)