Tell user to install ffmpeg if missing (#117)

Tell user to install ffmpeg if missing
pull/357/head
nyanpasu64 2019-01-04 19:00:05 -08:00 zatwierdzone przez GitHub
rodzic b29e5e9252
commit fd24ab6b0d
7 zmienionych plików z 129 dodań i 20 usunięć

Wyświetl plik

@ -8,11 +8,13 @@ from PyInstaller.building.datastruct import TOC
block_cipher = None block_cipher = None
dir = "corrscope/gui" def keep(dir, wildcard):
includes = glob.glob("corrscope/gui/*.ui") includes = glob.glob(f"{dir}/{wildcard}")
assert includes assert includes
return [(include, dir) for include in includes]
datas = [(include, dir) for include in includes]
datas = keep("corrscope/gui", "*.ui") + keep("corrscope/path", "*")
a = Analysis( a = Analysis(
["corrscope\\__main__.py"], ["corrscope\\__main__.py"],

Wyświetl plik

@ -1,4 +1,5 @@
import datetime import datetime
import sys
from itertools import count from itertools import count
from pathlib import Path from pathlib import Path
from typing import Optional, List, Tuple, Union, Iterator from typing import Optional, List, Tuple, Union, Iterator
@ -8,6 +9,7 @@ import click
from corrscope import __version__ from corrscope import __version__
from corrscope.channel import ChannelConfig from corrscope.channel import ChannelConfig
from corrscope.config import yaml from corrscope.config import yaml
from corrscope.ffmpeg_path import MissingFFmpegError
from corrscope.outputs import IOutputConfig, FFplayOutputConfig, FFmpegOutputConfig from corrscope.outputs import IOutputConfig, FFplayOutputConfig, FFmpegOutputConfig
from corrscope.corrscope import default_config, CorrScope, Config, Arguments from corrscope.corrscope import default_config, CorrScope, Config, Arguments
@ -211,7 +213,11 @@ def main(
profile_dump_name = get_profile_dump_name(first_song_name) profile_dump_name = get_profile_dump_name(first_song_name)
cProfile.runctx('command()', globals(), locals(), profile_dump_name) cProfile.runctx('command()', globals(), locals(), profile_dump_name)
else: else:
try:
command() command()
except MissingFFmpegError as e:
# Tell user how to install ffmpeg (__str__).
print(e, file=sys.stderr)
def get_profile_dump_name(prefix: str) -> str: def get_profile_dump_name(prefix: str) -> str:

Wyświetl plik

@ -0,0 +1,50 @@
import os
import platform
import sys
from pathlib import Path
from corrscope.config import CorrError
# Add app-specific ffmpeg path.
path_dir = str(Path(__file__).parent / "path")
os.environ["PATH"] += os.pathsep + path_dir
# Editing sys.path doesn't work.
# https://bugs.python.org/issue8557 is relevant but may be outdated.
# Unused
# def ffmpeg_exists():
# return shutil.which("ffmpeg") is not None
def get_ffmpeg_url() -> str:
# is_python_64 = sys.maxsize > 2 ** 32
is_os_64 = platform.machine().endswith("64")
def url(os_ver):
return f"https://ffmpeg.zeranoe.com/builds/{os_ver}/shared/ffmpeg-latest-{os_ver}-shared.zip"
if sys.platform == "win32" and is_os_64:
return url("win64")
elif sys.platform == "win32" and not is_os_64:
return url("win32")
elif sys.platform == "darwin" and is_os_64:
return url("macos64")
else:
return ""
class MissingFFmpegError(CorrError):
ffmpeg_url = get_ffmpeg_url()
can_download = bool(ffmpeg_url)
message = f'FFmpeg must be in PATH or "{path_dir}" in order to use corrscope.\n'
if can_download:
message += f"Download ffmpeg from {ffmpeg_url}."
else:
message += "Cannot download FFmpeg for your platform."
def __str__(self):
return self.message

Wyświetl plik

@ -15,8 +15,10 @@ from PyQt5.QtWidgets import QShortcut
from corrscope import __version__ # variable from corrscope import __version__ # variable
from corrscope import cli # module wtf? from corrscope import cli # module wtf?
from corrscope import ffmpeg_path
from corrscope.channel import ChannelConfig from corrscope.channel import ChannelConfig
from corrscope.config import CorrError, copy_config, yaml from corrscope.config import CorrError, copy_config, yaml
from corrscope.corrscope import CorrScope, Config, Arguments, default_config
from corrscope.gui.data_bind import ( from corrscope.gui.data_bind import (
PresentationModel, PresentationModel,
map_gui, map_gui,
@ -32,7 +34,6 @@ from corrscope.gui.util import (
TracebackDialog, TracebackDialog,
) )
from corrscope.outputs import IOutputConfig, FFplayOutputConfig, FFmpegOutputConfig from corrscope.outputs import IOutputConfig, FFplayOutputConfig, FFmpegOutputConfig
from corrscope.corrscope import CorrScope, Config, Arguments, default_config
from corrscope.triggers import CorrelationTriggerConfig, ITriggerConfig from corrscope.triggers import CorrelationTriggerConfig, ITriggerConfig
from corrscope.util import obj_name from corrscope.util import obj_name
@ -336,15 +337,19 @@ class MainWindow(qw.QMainWindow):
cfg = copy_config(self.model.cfg) cfg = copy_config(self.model.cfg)
t = self.corr_thread.obj = CorrThread(cfg, arg) t = self.corr_thread.obj = CorrThread(cfg, arg)
t.error.connect(self.on_play_thread_error)
t.finished.connect(self.on_play_thread_finished) t.finished.connect(self.on_play_thread_finished)
t.error.connect(self.on_play_thread_error)
t.ffmpeg_missing.connect(self.on_play_thread_ffmpeg_missing)
t.start() t.start()
def on_play_thread_finished(self):
self.corr_thread.set(None)
def on_play_thread_error(self, stack_trace: str): def on_play_thread_error(self, stack_trace: str):
TracebackDialog(self).showMessage(stack_trace) TracebackDialog(self).showMessage(stack_trace)
def on_play_thread_finished(self): def on_play_thread_ffmpeg_missing(self):
self.corr_thread.set(None) DownloadFFmpegActivity(self)
def _get_args(self, outputs: List[IOutputConfig]): def _get_args(self, outputs: List[IOutputConfig]):
arg = Arguments(cfg_dir=self.cfg_dir, outputs=outputs) arg = Arguments(cfg_dir=self.cfg_dir, outputs=outputs)
@ -401,14 +406,24 @@ class CorrThread(qc.QThread):
arg = self.arg arg = self.arg
try: try:
CorrScope(cfg, arg).play() CorrScope(cfg, arg).play()
except Exception:
except ffmpeg_path.MissingFFmpegError:
arg.on_end() arg.on_end()
self.ffmpeg_missing.emit()
except Exception as e:
arg.on_end()
if isinstance(e, CorrError):
stack_trace = traceback.format_exc(limit=0)
else:
stack_trace = traceback.format_exc() stack_trace = traceback.format_exc()
self.error.emit(stack_trace) self.error.emit(stack_trace)
else: else:
arg.on_end() arg.on_end()
error = qc.pyqtSignal(str) error = qc.pyqtSignal(str)
ffmpeg_missing = qc.pyqtSignal()
class CorrProgressDialog(qw.QProgressDialog): class CorrProgressDialog(qw.QProgressDialog):
@ -626,7 +641,6 @@ class ChannelModel(qc.QAbstractTableModel):
Column("trigger_width", int, None, "Trigger Width ×"), Column("trigger_width", int, None, "Trigger Width ×"),
Column("render_width", int, None, "Render Width ×"), Column("render_width", int, None, "Render Width ×"),
Column("line_color", str, None, "Line Color"), Column("line_color", str, None, "Line Color"),
# TODO move from table view to sidebar QDataWidgetMapper?
Column("trigger__edge_strength", float, None), Column("trigger__edge_strength", float, None),
Column("trigger__responsiveness", float, None), Column("trigger__responsiveness", float, None),
Column("trigger__buffer_falloff", float, None), Column("trigger__buffer_falloff", float, None),
@ -799,3 +813,32 @@ class ChannelModel(qc.QAbstractTableModel):
nope = qc.QVariant() nope = qc.QVariant()
class DownloadFFmpegActivity:
title = "Missing FFmpeg"
ffmpeg_url = ffmpeg_path.get_ffmpeg_url()
can_download = bool(ffmpeg_url)
path_uri = qc.QUrl.fromLocalFile(ffmpeg_path.path_dir).toString()
required = (
f"FFmpeg must be in PATH or "
f'<a href="{path_uri}">corrscope folder</a> in order to use corrscope.<br>'
)
ffmpeg_template = required + (
f'Download ffmpeg from <a href="{ffmpeg_url}">{ffmpeg_url}</a>.'
)
fail_template = required + "Cannot download FFmpeg for your platform."
def __init__(self, window: qw.QWidget):
"""Prompt the user to download and install ffmpeg."""
Msg = qw.QMessageBox
if not self.can_download:
Msg.information(window, self.title, self.fail_template, Msg.Ok)
return
Msg.information(window, self.title, self.ffmpeg_template, Msg.Ok)

Wyświetl plik

@ -1,12 +1,13 @@
# https://ffmpeg.org/ffplay.html
import numpy as np
import shlex import shlex
import subprocess import subprocess
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from os.path import abspath from os.path import abspath
from typing import TYPE_CHECKING, Type, List, Union, Optional from typing import TYPE_CHECKING, Type, List, Union, Optional
import numpy as np
from corrscope.config import register_config from corrscope.config import register_config
from corrscope.ffmpeg_path import MissingFFmpegError
if TYPE_CHECKING: if TYPE_CHECKING:
from corrscope.corrscope import Config from corrscope.corrscope import Config
@ -95,11 +96,15 @@ class _FFmpegProcess:
self.templates.append(cfg.audio_template) # audio self.templates.append(cfg.audio_template) # audio
def popen(self, extra_args, bufsize, **kwargs) -> subprocess.Popen: def popen(self, extra_args, bufsize, **kwargs) -> subprocess.Popen:
"""Raises FileNotFoundError if FFmpeg missing"""
try:
args = self._generate_args() + extra_args
return subprocess.Popen( return subprocess.Popen(
self._generate_args() + extra_args, args, stdin=subprocess.PIPE, bufsize=bufsize, **kwargs
stdin=subprocess.PIPE, )
bufsize=bufsize, except FileNotFoundError as e:
**kwargs, raise MissingFFmpegError(
# FIXME REMOVE f'Class {obj_name(self)}: program {args[0]} is missing'
) )
def _generate_args(self) -> List[str]: def _generate_args(self) -> List[str]:

3
corrscope/path/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,3 @@
*
!*.txt
!.gitignore