├── ptmv ├── __init__.py ├── yt.py ├── snd.py ├── img.py ├── vid.py ├── console.py └── __main__.py ├── assets ├── image_demo.gif └── video_demo.gif ├── .gitignore ├── makefile ├── setup.py ├── LICENSE ├── README-PIP.md └── README.md /ptmv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/image_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-kj/ptmv/HEAD/assets/image_demo.gif -------------------------------------------------------------------------------- /assets/video_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-kj/ptmv/HEAD/assets/video_demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | build 4 | bin 5 | dist 6 | ptmv.spec 7 | ptmv.egg-info 8 | 9 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | default: clean 2 | python3 setup.py sdist bdist_wheel 3 | twine upload dist/* 4 | 5 | clean: 6 | @$(RM) -rf bin/ build/ dist/ ptmv.egg-info -------------------------------------------------------------------------------- /ptmv/yt.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import yt_dlp as ytdl 3 | import os 4 | import tempfile 5 | import time 6 | 7 | def download(url): 8 | ptmv_tempdir = os.path.join(tempfile.gettempdir(), "ptmv") 9 | if not os.path.exists(ptmv_tempdir): os.makedirs(ptmv_tempdir) 10 | file = ptmv_tempdir + str(int(time.time())) 11 | 12 | ydl_opts = { 13 | "format": "worst[ext=mp4]", 14 | "outtmpl": file + ".%(ext)s", 15 | "quiet": True, 16 | "no_warnings": True, 17 | } 18 | 19 | ytdl.YoutubeDL(ydl_opts).download([url]) 20 | return file + ".mp4" 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | 4 | HERE = pathlib.Path(__file__).parent 5 | README = (HERE / "README-PIP.md").read_text() 6 | 7 | setup( 8 | name="ptmv", 9 | version="1.0.1", 10 | description="An utf-8/truecolor image and video viewer for the terminal", 11 | long_description=README, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/kal39/ptmv", 14 | author="kal39", 15 | author_email="kaikitagawajones@gmail.com", 16 | license="MIT", 17 | packages=["ptmv"], 18 | include_package_data=True, 19 | install_requires=["wheel", "opencv-python", "simpleaudio", "yt_dlp"], 20 | entry_points={ 21 | "console_scripts": [ 22 | "ptmv=ptmv.__main__:main", 23 | ] 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /ptmv/snd.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tempfile 4 | import time 5 | import wave 6 | import simpleaudio 7 | 8 | def extract(file): 9 | ptmv_tempdir = os.path.join(tempfile.gettempdir(), "ptmv") 10 | if not os.path.exists(ptmv_tempdir): os.makedirs(ptmv_tempdir) 11 | snd_file = ptmv_tempdir + str(int(time.time())) + ".wav" 12 | command = "ffmpeg -i " + file + " -b:a 48k -ac 1 " + snd_file 13 | subprocess.run(command.split(), stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) 14 | return snd_file 15 | 16 | def play(file, start_time): 17 | if not os.path.exists(file): return 18 | wave_raw = wave.open(file) 19 | frame_rate = wave_raw.getframerate() 20 | wave_raw.setpos(int(frame_rate * start_time)) 21 | return simpleaudio.WaveObject.from_wave_read(wave_raw).play() 22 | 23 | def stop(play_obj): play_obj.stop() -------------------------------------------------------------------------------- /ptmv/img.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | from . import console 4 | 5 | def display(file, user_width, user_height): 6 | image = cv2.imread(file) 7 | if image is None: print("Could not read [%s]" % file); return 8 | image = resize(image, user_width, user_height) 9 | console.set_image_height(image.shape[0]) 10 | console.clear() 11 | console.draw_image(image) 12 | 13 | def resize(image, user_width, user_height): 14 | image_height, image_width, _ = image.shape 15 | 16 | if user_width is None and user_height is None: 17 | scale_x = min(console.width() / image_width, console.height() / image_height) 18 | scale_y = scale_x 19 | elif user_width is not None: 20 | scale_x = user_width / image_width 21 | scale_y = scale_x 22 | elif user_height is not None: 23 | scale_y = user_height / image_height 24 | scale_x = scale_y 25 | else: 26 | scale_x = user_width / image_width 27 | scale_y = user_height / image_height 28 | 29 | return cv2.resize(image, (0, 0), interpolation = cv2.INTER_AREA, fx = scale_x, fy = scale_y) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kai Kitagawa-Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ptmv/vid.py: -------------------------------------------------------------------------------- 1 | import time 2 | import cv2 3 | 4 | from . import console 5 | from . import img 6 | from . import snd 7 | 8 | def play(file, width, height, target_fps, start_offset): 9 | video = cv2.VideoCapture(file) 10 | audio = snd.extract(file) 11 | if video is None: print("Could not read [%s]" % file); return 12 | 13 | orig_height, orig_width, _ = get_frame(video, 0).shape 14 | out_height, out_width, _ = img.resize(get_frame(video, 0), width, height).shape 15 | 16 | console.set_image_height(out_height) 17 | video_loop(video, audio, out_width / orig_width, out_height / orig_height, target_fps, start_offset) 18 | 19 | def video_loop(video, audio, scale_x, scale_y, target_fps, start_offset): 20 | frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) 21 | fps = int(video.get(cv2.CAP_PROP_FPS)) 22 | dt = 1 / target_fps 23 | 24 | console.hide_cursor() 25 | snd.play(audio, start_offset) 26 | console.clear() 27 | 28 | prev_frame = None 29 | start_time = time.time() - start_offset 30 | prev_time = time.time() - dt 31 | 32 | while True: 33 | current_time = time.time() 34 | if current_time - prev_time > dt: 35 | prev_time = current_time 36 | current_frame = get_frame(video, int((current_time - start_time) * fps)) 37 | if current_frame is None: break 38 | 39 | current_frame = cv2.resize(current_frame, (0, 0), fx = scale_x, fy = scale_y) 40 | console.draw_frame(prev_frame, current_frame) 41 | prev_frame = current_frame 42 | 43 | def get_frame(video, frame_num): 44 | video.set(1, frame_num) 45 | return video.read()[1] -------------------------------------------------------------------------------- /README-PIP.md: -------------------------------------------------------------------------------- 1 |
2 |
View images and videos without leaving the console
4 |2 |
View images and videos without leaving the console
4 |
5 |
6 |
7 |
8 |
30 |
31 | **Watching a video**
32 |
33 |
34 |
35 | ----
36 |
37 | ### Requirements
38 |
39 | * A terminal that supports **truecolor** ([list](https://gist.github.com/XVilka/8346728)) and **utf-8** (most terminals should support utf-8).
40 | * `libasound2-dev` / `alsa-lib`
41 | * for Ubuntu: `apt install libasound2-dev`
42 | * for Arch Linux: `pacman -S alsa-lib`
43 | * `ffmpeg`
44 | * for Ubuntu: `apt install ffmpeg`
45 | * for Arch Linux: `pacman -S ffmpeg`
46 |
47 | ----
48 |
49 | ### Installation
50 |
51 | ```shell
52 | pip install ptmv
53 | ```
54 |
55 | ----
56 |
57 | ### Usage
58 |
59 | ```shell
60 | ptmv FILE [OPTIONS]
61 | ```
62 |
63 | * **Required arguments**
64 |
65 | * `FILE`
66 |
67 | File to display/play or youtube url
68 |
69 | * **Optional arguments**
70 |
71 | * `-y`. `--youtube`
72 | View youtube videos
73 |
74 | * `--height`
75 | Set height (setting both `width` and `height` will ignore original aspect ratio)
76 |
77 | * `--width`
78 | Set width (setting both `width` and `height` will ignore original aspect ratio)
79 |
80 | * `--start-time`
81 |
82 | Set start position for video.
83 |
84 | * `-f`, `--fps`
85 | Set fps (default 15 fps)
86 |
87 | * `-m`, `--mute`
88 | Mute audio
89 |
90 | * `-h`, `--help `
91 | Display help
92 |
93 | ----
94 |
95 | ### Contributing
96 |
97 | Any contributions are greatly appreciated.
98 |
99 | ----
100 |
101 | **kal39**(https://github.com/kal39) - kaikitagawajones@gmail.com
102 | Distributed under the MIT license. See `LICENSE` for more information.
103 |
--------------------------------------------------------------------------------