# ==========================================
# 【App A】 MEPSガイダンス専用 GPV解析エンジン
# VERSION: MEPS_DEDICATED_4.1 (全21メンバー抽出・最適化・タイムアウト対応版)
# ==========================================
import sys, os, glob, re, subprocess
from datetime import datetime
os.environ['QT_API'] = 'pyqt6'
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QAction
try:
import cfgrib
except ImportError as e:
print("\n" + "="*60)
print("【起動エラー】cfgrib または依存ライブラリの読み込みに失敗しました。")
print(f"エラー詳細: {e}")
input("\nEnterキーを押すとウィンドウを閉じます...")
sys.exit(1)
APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, "gpv_cache_npz")
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
WGRIB2_EXE = os.path.join(APP_DIR, "wgrib2_data", "wgrib2.exe")
os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)
def write_syslog(msg):
log_file = os.path.join(CURRENT_OUTPUT_DIR, "meps_engine_log.txt")
try:
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
f.flush()
os.fsync(f.fileno())
except Exception: pass
WORKER_SCRIPT = os.path.join(APP_DIR, "_meps_worker.py")
def create_lightning_icon():
pixmap = QPixmap(64, 64); pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap); font = painter.font(); font.setPixelSize(50)
painter.setFont(font); painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "🌀")
painter.end()
return QIcon(pixmap)
# ==========================================
# MEPS専用ワーカーコード (インベントリ解析・全メンバー結合・最適化ロジック)
# ==========================================
WORKER_CODE = """
import sys, traceback
def main():
try:
import os, gc, subprocess, time
import numpy as np
import warnings
import cfgrib
import re
os.environ["ECCODES_MAX_VALUES"] = "5000000"
if sys.platform == "win32":
conda_dir = os.path.dirname(sys.executable)
dll_paths = [os.path.join(conda_dir, "Library", "bin"), os.path.join(conda_dir, "bin")]
for p in dll_paths:
if os.path.exists(p):
os.environ["PATH"] = f"{p};{os.environ.get('PATH', '')}"
try: os.add_dll_directory(p)
except Exception: pass
model = sys.argv[1]; mode = sys.argv[2]; cache = sys.argv[3]
init = sys.argv[4]; target_fts_str = sys.argv[5]
f_pall = sys.argv[6]; f_prrst = sys.argv[7]; wgrib2_path = sys.argv[8]
target_fts = [int(x) for x in target_fts_str.split(',')]
d_max = {ft: {} for ft in target_fts}; d_min = {ft: {} for ft in target_fts}
d_mean = {ft: {} for ft in target_fts}; d_p25 = {ft: {} for ft in target_fts}; d_p75 = {ft: {} for ft in target_fts}
creationflags = 0x08000000 if sys.platform == "win32" else 0
# 【最適化】ファイルごとに1回だけインベントリを取得し、指定された全FTを処理する設計に変更
def process_file_optimized(fin, src_name, target_fts):
if fin == "NONE" or not os.path.exists(fin): return
try:
# 【安定性向上】 タイムアウト120秒を設定
cmd_inv = [wgrib2_path, fin, "-s"]
res = subprocess.run(cmd_inv, capture_output=True, text=True, creationflags=creationflags, timeout=120)
except subprocess.TimeoutExpired:
print(f"WARN: wgrib2 inventory timeout for {fin}")
return
if not res.stdout.strip(): return
parsed_lines = []
for line in res.stdout.strip().split('\\n'):
if not line: continue
parts = line.split(':')
if len(parts) < 6: continue
param = parts[3]
time_str = parts[5]
m = re.search(r'([0-9]+)-([0-9]+)\\s+(hour|day)', time_str)
if not m:
m2 = re.search(r'([0-9]+)\\s+(hour|day)', time_str)
if m2:
end_val = int(m2.group(1))
unit = m2.group(2)
if unit == 'day': end_val *= 24
start_val = end_val
else: continue
else:
start_val = int(m.group(1))
end_val = int(m.group(2))
unit = m.group(3)
if unit == 'day':
start_val *= 24; end_val *= 24
duration = end_val - start_val
if duration == 1: mapped_ft = start_val + 3
else: mapped_ft = end_val
if mapped_ft not in target_fts: continue
trange_str = f"{start_val}_{end_val}"
step_type = "accum"
if " max " in time_str: step_type = "max"
elif " min " in time_str: step_type = "min"
elif " ave " in time_str: step_type = "ave"
elif "inst" in time_str: step_type = "inst"
var_name = param.lower()
if param == "TPRATE": var_name = "tp"
elif param == "TSRATE": var_name = "asnow"
elif param == "TSTM": var_name = "tstm"
parsed_lines.append((mapped_ft, (var_name, step_type, trange_str, duration), line))
for t_ft in target_fts:
ft_lines = [item for item in parsed_lines if item[0] == t_ft]
if not ft_lines: continue
grouped = {}
for _, meta, line in ft_lines:
if meta not in grouped: grouped[meta] = []
grouped[meta].append(line)
for meta, group_lines in grouped.items():
var_name, step_type, trange_str, duration = meta
inv_input = "\\n".join(group_lines) + "\\n"
temp_bin = os.path.join(cache, f"~tmp_{model}_{init}_{t_ft}_{src_name}_{var_name}_{trange_str}.bin")
try:
# 【安定性向上】抽出処理にもタイムアウト120秒を設定
cmd_extract = [wgrib2_path, fin, "-i", "-grib", temp_bin]
process = subprocess.Popen(cmd_extract, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creationflags)
process.communicate(input=inv_input.encode('utf-8'), timeout=120)
except subprocess.TimeoutExpired:
process.kill()
print(f"WARN: wgrib2 extraction timeout for {temp_bin}")
continue
if os.path.exists(temp_bin) and os.path.getsize(temp_bin) > 0:
try:
# cfgribの次元圧縮問題を回避するため、返されるすべてのDatasetをリスト化する
dss = cfgrib.open_datasets(temp_bin, backend_kwargs={'indexpath': ''})
all_fields = []
lon_1d, lat_1d = None, None
for ds in dss:
if lon_1d is None:
lon = ds.coords['longitude'].values if 'longitude' in ds.coords else (ds.longitude.values if hasattr(ds, 'longitude') else None)
lat = ds.coords['latitude'].values if 'latitude' in ds.coords else (ds.latitude.values if hasattr(ds, 'latitude') else None)
if lon is not None and lat is not None:
lon_1d = lon[0, :] if lon.ndim == 2 else lon
lat_1d = lat[:, 0] if lat.ndim == 2 else lat
for v in ds.data_vars:
da = ds[v]
da_step = da.isel(step=0) if 'step' in da.dims else da
val = da_step.values.copy()
# 【精度向上】分割されたメンバー(Control, Pos, Neg等)をすべて手動で1つのリストに集約(全21メンバーを保証)
if 'number' in da_step.dims:
num_axis = da_step.dims.index('number')
val = np.moveaxis(val, num_axis, 0)
for i in range(val.shape[0]):
all_fields.append(val[i])
else:
all_fields.append(val)
for ds in dss: ds.close()
if all_fields and lon_1d is not None and lat_1d is not None:
# 集約した全メンバーをスタックし、(N, lat, lon) の形状にする
stacked_val = np.stack(all_fields, axis=0)
FIXED_KEY_MAP = {
("tstm", "pall", None): "thund",
("tp", "pall", 3): "precip",
("tp", "prrsf", 1): "precip1max",
("tp", "prrsf", 3): "precip3max",
("tp", "prrsf", 24): "precip24max",
("asnow","prrsf", 3): "snow3",
("asnow","prrsf", 6): "snow6",
("asnow","prrsf", 12): "snow12",
("asnow","prrsf", 24): "snow24",
}
lookup_dur = None if var_name == "tstm" else duration
fixed_key = FIXED_KEY_MAP.get((var_name, src_name, lookup_dur))
expected_start = max(0, t_ft - (duration if duration else 3))
expected_dynamic_key = f"{var_name}_{src_name}_{step_type}_{expected_start}_{t_ft}"
# 【ファイルサイズ最適化】マルチエイリアスを廃止し、ビューワーで読める単一の固定キー名のみ保存
aliases = [fixed_key] if fixed_key else [expected_dynamic_key]
grid_size = lon_1d.size * lat_1d.size
def set_coords(d_dict, keys):
for key in keys:
d_dict[f'lon_{key}'] = lon_1d.copy()
d_dict[f'lat_{key}'] = lat_1d.copy()
if grid_size > 100000:
d_dict['lon'] = lon_1d.copy()
d_dict['lat'] = lat_1d.copy()
else:
if 'lon' not in d_dict:
d_dict['lon'] = lon_1d.copy()
d_dict['lat'] = lat_1d.copy()
for d_dict in [d_max[t_ft], d_min[t_ft], d_mean[t_ft], d_p25[t_ft], d_p75[t_ft]]:
set_coords(d_dict, aliases)
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
# 集約した全21メンバー(stacked_val)に対して正しく統計を算出
mean_val = np.nanmean(stacked_val, axis=0)
max_val = np.nanmax(stacked_val, axis=0)
min_val = np.nanmin(stacked_val, axis=0)
p75_val = np.nanpercentile(stacked_val, 75, axis=0)
p25_val = np.nanpercentile(stacked_val, 25, axis=0)
for key in aliases:
d_mean[t_ft][key] = mean_val
d_max[t_ft][key] = max_val
d_min[t_ft][key] = min_val
d_p75[t_ft][key] = p75_val
d_p25[t_ft][key] = p25_val
except Exception as e:
print(f"WARN: Error processing group {meta}: {e}")
finally:
gc.collect()
try: os.remove(temp_bin)
except: pass
# ファイル単位で一度に全FTを処理
process_file_optimized(f_pall, "pall", target_fts)
process_file_optimized(f_prrst, "prrsf", target_fts)
for t_ft in target_fts:
if not d_max[t_ft]: continue
stat_dicts = {
"MAX": d_max[t_ft], "MIN": d_min[t_ft], "MEAN": d_mean[t_ft],
"P25": d_p25[t_ft], "P75": d_p75[t_ft]
}
for stat_name, data_dict in stat_dicts.items():
if not data_dict: continue
final_filepath = os.path.join(cache, f"{model}_GUID_{stat_name}_{init}_FT{t_ft:02d}.npz")
temp_filepath = os.path.join(cache, f"~tmp_{model}_GUID_{stat_name}_{init}_FT{t_ft:02d}.npz")
try:
np.savez_compressed(temp_filepath, **data_dict)
success = False
for _ in range(10):
try:
os.replace(temp_filepath, final_filepath)
success = True
break
except PermissionError: time.sleep(0.5)
if not success:
try: os.remove(final_filepath)
except Exception: pass
np.savez_compressed(final_filepath, **data_dict)
try:
if os.path.exists(temp_filepath): os.remove(temp_filepath)
except Exception: pass
except Exception: pass
print(f"SUCCESS:{t_ft}", flush=True)
except BaseException as e:
print(f"CRITICAL_ERROR: {traceback.format_exc()}", flush=True)
sys.exit(1)
if __name__ == '__main__': main()
"""
try:
with open(WORKER_SCRIPT, "w", encoding="utf-8") as f: f.write(WORKER_CODE)
except Exception: pass
class DataParserThread(QThread):
log_signal = pyqtSignal(str); progress_signal = pyqtSignal(int, str); finished_signal = pyqtSignal(bool)
def __init__(self, folder_paths, output_dir, scan_mode="48h"):
super().__init__()
self.folder_paths = [path for path in folder_paths if path and os.path.exists(path)]
self.output_dir = output_dir
self.scan_mode = scan_mode
self.abort = False
def run(self):
if not self.folder_paths: self.finished_signal.emit(False); return
if not os.path.exists(WGRIB2_EXE):
self.log_signal.emit(f"⚠️ wgrib2が見つかりません。"); self.finished_signal.emit(False); return
has_new_data = False; scan_started = False
creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
conda_dir = os.path.dirname(sys.executable); lib_bin = os.path.join(conda_dir, "Library", "bin")
env = os.environ.copy(); env["PATH"] = f"{lib_bin};{env.get('PATH', '')}"; env["PYTHONIOENCODING"] = "utf-8"
now_utc = datetime.utcnow()
for folder_path in self.folder_paths:
if self.abort: break
all_grib_files = []
for ext in ['*.bin', '*.grib2', '*.grb']:
all_grib_files.extend(glob.glob(os.path.join(folder_path, '**', f'*MEPS*{ext}'), recursive=True))
pall_files = []
prrst_files = []
for f in all_grib_files:
basename = os.path.basename(f).lower()
if 'p-all' in basename or 'pall' in basename:
pall_files.append(f)
elif 'prrst' in basename or 'prrsf' in basename:
prrst_files.append(f)
def get_all_init_groups(files_list):
groups = {}
for f in files_list:
m = re.search(r'_(\d{14})_', os.path.basename(f))
if m:
init = m.group(1)
if self.scan_mode == "48h":
try:
file_time = datetime.strptime(init, '%Y%m%d%H%M%S')
if (now_utc - file_time).total_seconds() > 48 * 3600:
continue
except Exception: pass
groups[init] = f
return groups
pall_groups = get_all_init_groups(pall_files)
prrst_groups = get_all_init_groups(prrst_files)
all_inits = set(pall_groups.keys()) | set(prrst_groups.keys())
target_ft_list = list(range(3, 40, 3))
execution_plan = []
for init_time_str in all_inits:
f_pall = pall_groups.get(init_time_str, "NONE")
f_prrst = prrst_groups.get(init_time_str, "NONE")
execution_plan.append((init_time_str, "MEPS", f_pall, f_prrst, target_ft_list, "GUID"))
execution_plan.sort(key=lambda x: x[0], reverse=True)
for init_time_str, model_name, f_pall, f_prrst, ft_list, mode in execution_plan:
if self.abort: break
tasks = []
for ft in ft_list:
files_exist = all(os.path.exists(os.path.join(self.output_dir, f"{model_name}_GUID_{stat}_{init_time_str}_FT{ft:02d}.npz"))
for stat in ["MAX", "MIN", "MEAN", "P25", "P75"])
if not files_exist: tasks.append(ft)
if not tasks: continue
if not scan_started:
self.log_signal.emit("🔍 MEPS アンサンブルデータの抽出を開始中..."); scan_started = True; has_new_data = True
if self.abort: break
self.log_signal.emit(f"🚀 {model_name} アンサンブル ({init_time_str}) 抽出開始 (対象: {len(tasks)}FT)...")
total_extracted = 0
for i in range(0, len(tasks), 3):
if self.abort: break
chunk_fts = tasks[i:i + 3]; ft_str = ",".join(map(str, chunk_fts))
cmd = [sys.executable, WORKER_SCRIPT, model_name, mode, self.output_dir, init_time_str, ft_str, f_pall, f_prrst, WGRIB2_EXE]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creationflags, env=env)
for line in iter(process.stdout.readline, ''):
if self.abort:
try: process.kill()
except: pass
break
line = line.strip()
if not line: continue
if line.startswith("SUCCESS:"):
ft_success = line.split(":")[1]; total_extracted += 1
self.progress_signal.emit(int((total_extracted / len(tasks)) * 100), f"⚡ {model_name} {init_time_str} FT={ft_success}h...")
self.log_signal.emit(f"⚡ 抽出完了: {model_name} {init_time_str} FT={ft_success}h (MAX/MIN/MEAN/P25/P75)")
elif "WARN:" in line or "CRITICAL_ERROR:" in line or "Traceback" in line or "Error:" in line or "File " in line:
self.log_signal.emit(f"⚠️ {line}")
try:
process.stdout.close(); process.wait()
except: pass
if not self.abort and has_new_data:
self.log_signal.emit("✅ 全てのMEPSスキャン・抽出が完了しました")
self.finished_signal.emit(has_new_data)
class EngineStatusWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("MEPS ガイダンス専用 解析エンジン V4.1 (結合最適化版)")
self.setFixedSize(650, 620)
self.setStyleSheet("""
QWidget { background-color: #1A1A24; color: #E0E0E0; font-family: 'MS Gothic'; font-size: 11pt; }
QLabel { color: #00FFCC; font-weight: bold; }
QPushButton { background-color: #2C3E50; color: white; padding: 6px; border-radius: 4px; font-weight: bold; border: 1px solid #34495E; }
QPushButton:hover { background-color: #34495E; }
QListWidget { background-color: #111118; border: 1px solid #34495E; color: #00FFCC; padding: 5px; font-family: Consolas, monospace; font-size: 10pt; }
""")
self.is_quitting = False
layout = QVBoxLayout(self)
self.status_label = QLabel("状態: 起動中..."); self.status_label.setStyleSheet("font-size: 14pt;")
layout.addWidget(self.status_label)
control_layout = QHBoxLayout()
self.pause_btn = QPushButton("⏹ 抽出を即時停止")
self.pause_btn.setStyleSheet("background-color: #C0392B;")
self.resume_btn = QPushButton("▶ 手動強制スキャン")
self.mode_btn = QPushButton("🔄 モード: 最新48時間 (推奨)")
self.mode_btn.setStyleSheet("background-color: #D35400;")
for btn in [self.pause_btn, self.resume_btn, self.mode_btn]: control_layout.addWidget(btn)
self.clear_btn = QPushButton("🧹 キャッシュ全削除"); self.clear_btn.setStyleSheet("background-color: #8E44AD;")
control_layout.addWidget(self.clear_btn)
layout.addLayout(control_layout)
out_layout = QHBoxLayout()
self.out_folder_btn = QPushButton("💾 出力先フォルダを設定"); self.out_folder_btn.setStyleSheet("background-color: #2980B9;")
out_layout.addWidget(self.out_folder_btn)
self.out_folder_label = QLabel("未設定"); self.out_folder_label.setStyleSheet("background: #111118; padding: 4px; border: 1px solid #34495E; color: #8892B0;")
out_layout.addWidget(self.out_folder_label, stretch=1)
layout.addLayout(out_layout)
self.log_list = QListWidget(); layout.addWidget(self.log_list)
self.copy_btn = QPushButton("📋 ログをコピー"); self.copy_btn.setStyleSheet("background-color: #16A085;"); layout.addWidget(self.copy_btn)
folder_layout = QHBoxLayout()
self.folder_btns = [QPushButton(f"📁 読込元フォルダ {i+1}") for i in range(3)]
for b in self.folder_btns: folder_layout.addWidget(b)
layout.addLayout(folder_layout)
self.exit_btn = QPushButton("システムを完全に終了"); self.exit_btn.setStyleSheet("background-color: #8b0000;"); layout.addWidget(self.exit_btn)
def log(self, message):
self.log_list.addItem(f"[{datetime.now().strftime('%H:%M:%S')}] {message}")
self.log_list.scrollToBottom(); write_syslog(message)
def closeEvent(self, event):
if getattr(self, 'is_quitting', False): event.accept()
else:
event.ignore(); self.hide()
if hasattr(self, 'tray_msg_callback'): self.tray_msg_callback()
class TrayApp(QApplication):
def __init__(self, sys_argv):
super().__init__(sys_argv)
self.setQuitOnLastWindowClosed(False)
self.settings = QSettings("SapporoWeatherApp", "MEPSEngine")
self.monitor_folders = [self.settings.value(f"watch_dir_{i+1}", "") for i in range(3)]
global CURRENT_OUTPUT_DIR
self.output_dir = self.settings.value("output_dir", DEFAULT_OUTPUT_DIR)
CURRENT_OUTPUT_DIR = self.output_dir
os.makedirs(self.output_dir, exist_ok=True)
self.scan_mode = "48h"
self.window = EngineStatusWindow()
self.window.tray_msg_callback = self.show_tray_message
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(create_lightning_icon())
self.tray_icon.setToolTip("MEPSエンジン (稼働中)")
tray_menu = QMenu(); show_action = QAction("ウィンドウを表示"); show_action.triggered.connect(self.restore_window)
quit_action = QAction("完全に終了する"); quit_action.triggered.connect(self.quit_app)
tray_menu.addAction(show_action); tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu); self.tray_icon.activated.connect(self.tray_icon_activated); self.tray_icon.show()
self.window.log("===============================")
self.window.log("MEPS専用エンジン起動 V4.1")
self.window.log("===============================")
self.window.out_folder_label.setText(self.output_dir)
self.window.out_folder_btn.clicked.connect(self.select_output_folder)
for i in range(3):
self.window.folder_btns[i].clicked.connect(lambda checked, idx=i: self.select_folder(idx))
self.update_folder_btn_text(i)
self.window.copy_btn.clicked.connect(self.copy_logs)
self.window.pause_btn.clicked.connect(self.stop_and_pause)
self.window.resume_btn.clicked.connect(self.force_scan)
self.window.mode_btn.clicked.connect(self.toggle_mode)
self.window.clear_btn.clicked.connect(self.clear_cache)
self.window.exit_btn.clicked.connect(self.quit_app)
self.timer = QTimer(); self.timer.timeout.connect(self.run_parser); self.timer.start(15000)
self.is_processing = False; self.run_parser(); self.window.show()
def kill_current_worker(self):
if hasattr(self, 'worker') and self.worker.isRunning():
self.window.log("⚠️ 抽出処理を中断しています...")
self.worker.abort = True
self.is_processing = False
def tray_icon_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick: self.restore_window()
def restore_window(self):
self.window.showNormal(); self.window.activateWindow()
def show_tray_message(self):
self.tray_icon.showMessage("MEPSエンジン待機中", "裏で抽出を続けます。", QIcon(self.tray_icon.icon()), 3000)
def quit_app(self):
self.kill_current_worker()
self.window.is_quitting = True
self.quit()
def stop_and_pause(self):
self.timer.stop(); self.kill_current_worker()
self.window.log("⏸ 処理を即時停止しました。")
self.window.status_label.setText("状態: 停止中")
def toggle_mode(self):
self.kill_current_worker()
if self.scan_mode == "48h":
self.scan_mode = "all"
self.window.mode_btn.setText("🔄 モード: すべてのデータ"); self.window.mode_btn.setStyleSheet("background-color: #27AE60;")
else:
self.scan_mode = "48h"
self.window.mode_btn.setText("🔄 モード: 最新48時間 (推奨)"); self.window.mode_btn.setStyleSheet("background-color: #D35400;")
self.run_parser()
def select_output_folder(self):
global CURRENT_OUTPUT_DIR
folder = QFileDialog.getExistingDirectory(self.window, "出力先フォルダ", self.output_dir)
if folder:
self.kill_current_worker()
self.output_dir = folder; CURRENT_OUTPUT_DIR = folder; self.settings.setValue("output_dir", folder)
self.window.out_folder_label.setText(folder)
self.run_parser()
def clear_cache(self):
reply = QMessageBox.question(self.window, "確認", "MEPSのキャッシュを削除しますか?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.timer.stop(); self.kill_current_worker()
for f in glob.glob(os.path.join(self.output_dir, "MEPS_*.npz")):
try: os.remove(f)
except: pass
self.run_parser(); self.timer.start(15000)
def select_folder(self, index):
folder = QFileDialog.getExistingDirectory(self.window, "読込元フォルダ選択")
if folder:
self.kill_current_worker()
self.monitor_folders[index] = folder; self.settings.setValue(f"watch_dir_{index+1}", folder)
self.update_folder_btn_text(index)
self.run_parser()
def update_folder_btn_text(self, i):
f = self.monitor_folders[i]
self.window.folder_btns[i].setText(f"📁 {os.path.basename(f)}" if f else f"📁 未設定 {i+1}")
def copy_logs(self): QApplication.clipboard().setText("\n".join([self.window.log_list.item(i).text() for i in range(self.window.log_list.count())]))
def force_scan(self):
self.timer.start(15000); self.kill_current_worker(); self.run_parser()
def run_parser(self):
if self.is_processing: return
if not any(self.monitor_folders): self.window.status_label.setText("状態: 読込元フォルダ未設定"); return
self.is_processing = True
self.worker = DataParserThread(self.monitor_folders, self.output_dir, self.scan_mode)
self.worker.log_signal.connect(self.window.log); self.worker.progress_signal.connect(lambda v, t: self.window.status_label.setText(f"状態: {t}"))
self.worker.finished_signal.connect(self.on_parse_finished)
self.worker.start()
def on_parse_finished(self, has_new_data):
self.is_processing = False
if not hasattr(self.worker, 'abort') or not self.worker.abort:
self.window.status_label.setText("状態: 待機中")
if __name__ == '__main__':
import multiprocessing
multiprocessing.freeze_support()
if len(sys.argv) > 1 and '_meps_worker.py' in sys.argv[1]:
sys.argv = sys.argv[1:]
try:
with open(sys.argv[0], "r", encoding="utf-8") as f:
worker_code = f.read()
exec(worker_code, {'__name__': '__main__'})
except Exception: pass
sys.exit(0)
app = TrayApp(sys.argv); sys.exit(app.exec())