Repository: Zulko/moviepy

Issue:
how to get the frames number of the whole video ?
hi,
Just wanna get the frames number directly,
do not want to use **duration*fps**,
any help ?
thx


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/36443797/badge)](https://coveralls.io/builds/36443797)

Coverage decreased (-0.04%) to 66.012% when pulling **a2b7ec5cfd1f8a4bf89b1dfcebf07a74f8edd679 on mondeja:nframes-property** into **22e207ee745b258651ef61e03125b3f9cbde2d71 on Zulko:master**.

tburrows13: I prefer `n_frames` to `nframes` (and I'll likely change similar ones like `nchannels` to that in the future). Other than that its good.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df0c844..b10d312 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,12 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added <!-- for new features -->
 - Support for `copy.copy(clip)` and `copy.deepcopy(clip)` with same behaviour as `clip.copy()` [\#1442](https://github.com/Zulko/moviepy/pull/1442)
 - `audio.fx.multiply_stereo_volume` to control volume by audio channels [\#1424](https://github.com/Zulko/moviepy/pull/1424)
+- Support for retrieve clip frames number using `clip.n_frames` [\#1471](https://github.com/Zulko/moviepy/pull/1471)
 
 ### Changed <!-- for changes in existing functionality -->
 - Lots of method and parameter names have been changed. This will be explained better in the documentation soon. See https://github.com/Zulko/moviepy/pull/1170 for more information. [\#1170](https://github.com/Zulko/moviepy/pull/1170)
 - Changed recommended import from `import moviepy.editor` to `import moviepy`. This change is fully backwards compatible [\#1340](https://github.com/Zulko/moviepy/pull/1340)
 - Renamed `audio.fx.volumex` to `audio.fx.multiply_volume` [\#1424](https://github.com/Zulko/moviepy/pull/1424)
 - Renamed `cols_widths` argument of `clips_array` function by `cols_heights` [\#1465](https://github.com/Zulko/moviepy/pull/1465)
+- `video_nframes` attribute of dictionary returned from `ffmpeg_parse_infos` renamed to `video_n_frames` [\#1471](https://github.com/Zulko/moviepy/pull/1471)
 
 ### Deprecated <!-- for soon-to-be removed features -->
 - `moviepy.video.fx.all` and `moviepy.audio.fx.all`. Use the fx method directly from the clip instance or import the fx function from `moviepy.video.fx` and `moviepy.audio.fx`. [\#1105](https://github.com/Zulko/moviepy/pull/1105)
diff --git a/docs/examples/quick_recipes.rst b/docs/examples/quick_recipes.rst
index b7eabbb..65c67dd 100644
--- a/docs/examples/quick_recipes.rst
+++ b/docs/examples/quick_recipes.rst
@@ -58,9 +58,8 @@ Getting the average frame of a video
 
     from moviepy import VideoFileClip, ImageClip
     clip = VideoFileClip("video.mp4")
-    fps= 1.0 # take one frame per second
-    nframes = clip.duration*fps # total number of frames used
-    total_image = sum(clip.iter_frames(fps,dtype=float,logger='bar'))
-    average_image = ImageClip(total_image/ nframes)
+    fps = 1.0  # take one frame per second
+    total_image = sum(clip.iter_frames(fps, dtype=float, logger='bar'))
+    average_image = ImageClip(total_image / clip.n_frames)
     average_image.save_frame("average_test.png")
 
diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py
index 4542e09..ddf9fdb 100644
--- a/moviepy/audio/io/readers.py
+++ b/moviepy/audio/io/readers.py
@@ -67,8 +67,8 @@ class FFMPEG_AudioReader:
         self.infos = infos
         self.proc = None
 
-        self.nframes = int(self.fps * self.duration)
-        self.buffersize = min(self.nframes + 1, buffersize)
+        self.n_frames = int(self.fps * self.duration)
+        self.buffersize = min(self.n_frames + 1, buffersize)
         self.buffer = None
         self.buffer_startframe = 1
         self.initialize()
@@ -218,7 +218,7 @@ class FFMPEG_AudioReader:
         else:
 
             ind = int(self.fps * tt)
-            if ind < 0 or ind > self.nframes:  # out of time: return 0
+            if ind < 0 or ind > self.n_frames:  # out of time: return 0
                 return np.zeros(self.nchannels)
 
             if not (0 <= (ind - self.buffer_startframe) < len(self.buffer)):
diff --git a/moviepy/decorators.py b/moviepy/decorators.py
index 92bea29..0be6487 100644
--- a/moviepy/decorators.py
+++ b/moviepy/decorators.py
@@ -56,6 +56,16 @@ def requires_duration(func, clip, *args, **kwargs):
         return func(clip, *args, **kwargs)
 
 
+@decorator.decorator
+def requires_fps(func, clip, *args, **kwargs):
+    """ Raise an error if the clip has no fps."""
+
+    if not hasattr(clip, "fps") or clip.fps is None:
+        raise ValueError("Attribute 'fps' not set")
+    else:
+        return func(clip, *args, **kwargs)
+
+
 @decorator.decorator
 def audio_video_fx(func, clip, *args, **kwargs):
     """Use an audio function on a video/audio clip
diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py
index b21249b..e5f5dfc 100644
--- a/moviepy/video/VideoClip.py
+++ b/moviepy/video/VideoClip.py
@@ -22,6 +22,7 @@ from moviepy.decorators import (
     convert_parameter_to_seconds,
     outplace,
     requires_duration,
+    requires_fps,
     use_clip_fps_by_default,
 )
 from moviepy.tools import (
@@ -122,6 +123,12 @@ class VideoClip(Clip):
     def aspect_ratio(self):
         return self.w / float(self.h)
 
+    @property
+    @requires_duration
+    @requires_fps
+    def n_frames(self):
+        return int(self.duration * self.fps)
+
     def __copy__(self):
         """Mixed copy of the clip.
 
diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py
index 967d664..9f0f36c 100644
--- a/moviepy/video/io/ffmpeg_reader.py
+++ b/moviepy/video/io/ffmpeg_reader.py
@@ -49,7 +49,7 @@ class FFMPEG_VideoReader:
 
         self.duration = infos["video_duration"]
         self.ffmpeg_duration = infos["duration"]
-        self.nframes = infos["video_nframes"]
+        self.n_frames = infos["video_n_frames"]
         self.bitrate = infos["video_bitrate"]
 
         self.infos = infos
@@ -155,7 +155,7 @@ class FFMPEG_VideoReader:
                     nbytes,
                     len(s),
                     self.pos,
-                    self.nframes,
+                    self.n_frames,
                     1.0 * self.pos / self.fps,
                     self.duration,
                 ),
@@ -287,7 +287,7 @@ def ffmpeg_parse_infos(
     """Get file infos using ffmpeg.
 
     Returns a dictionnary with the fields:
-    "video_found", "video_fps", "duration", "video_nframes",
+    "video_found", "video_fps", "duration", "video_n_frames",
     "video_duration", "video_bitrate","audio_found", "audio_fps", "audio_bitrate"
 
     "video_duration" is slightly smaller than "duration" to avoid
@@ -420,14 +420,14 @@ def ffmpeg_parse_infos(
                 result["video_fps"] = x * coef
 
         if check_duration:
-            result["video_nframes"] = int(result["duration"] * result["video_fps"])
+            result["video_n_frames"] = int(result["duration"] * result["video_fps"])
             result["video_duration"] = result["duration"]
         else:
-            result["video_nframes"] = 1
+            result["video_n_frames"] = 1
             result["video_duration"] = None
         # We could have also recomputed the duration from the number
         # of frames, as follows:
-        # >>> result['video_duration'] = result['video_nframes'] / result['video_fps']
+        # >>> result['video_duration'] = result['video_n_frames'] / result['video_fps']
 
         # get the video rotation info.
         try:


Test patch:
diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py
index f509bae..d214b6a 100644
--- a/tests/test_ffmpeg_reader.py
+++ b/tests/test_ffmpeg_reader.py
@@ -30,10 +30,10 @@ def test_ffmpeg_parse_infos():
 
 def test_ffmpeg_parse_infos_duration():
     infos = ffmpeg_parse_infos("media/big_buck_bunny_0_30.webm")
-    assert infos["video_nframes"] == 720
+    assert infos["video_n_frames"] == 720
 
     infos = ffmpeg_parse_infos("media/bitmap.mp4")
-    assert infos["video_nframes"] == 5
+    assert infos["video_n_frames"] == 5
 
 
 def test_ffmpeg_parse_infos_for_i926():