New scripts stepmania,truenas,spotify
This commit is contained in:
@@ -17,7 +17,7 @@ SERVER=$(curl -s --connect-timeout 2 -4 ifconfig.me || hostname -I | awk '{print
|
||||
# --- PRE-CALCULO DE PROCESOS PHP-FPM ---
|
||||
declare -A PROCESS_MAP
|
||||
|
||||
echo "Analizando procesos PHP-FPM..."
|
||||
echo "Analyzing PHP-FPM processes..."
|
||||
|
||||
while read -r count domain; do
|
||||
domain=$(echo "$domain" | xargs)
|
||||
@@ -32,7 +32,7 @@ else
|
||||
fi
|
||||
|
||||
# --- IMPRIMIR CABECERA ---
|
||||
printf "%-30s %-8s %-12s %-12s %-12s %-10s %-10s %-20s %-15s\n" "DOMINIO" "PROCS" "RAM (Lim)" "UPLOAD" "POST" "TIME" "PHP VER" "HANDLER" "IP (DNS)"
|
||||
printf "%-30s %-8s %-12s %-12s %-12s %-10s %-10s %-20s %-15s\n" "DOMAIN" "PROCS" "RAM (Lim)" "UPLOAD" "POST" "TIME" "PHP VER" "HANDLER" "IP (DNS)"
|
||||
printf "%s\n" "------------------------------------------------------------------------------------------------------------------------------------------------"
|
||||
|
||||
for DOMAIN in $DOMAINS; do
|
||||
|
||||
252
spotify/Stream_History_To_csv_png.py
Normal file
252
spotify/Stream_History_To_csv_png.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Configuración de estilo para los gráficos
|
||||
plt.style.use('seaborn-v0_8-darkgrid')
|
||||
sns.set_palette("husl")
|
||||
|
||||
def cargar_archivos_json(directorio='.'):
|
||||
"""
|
||||
Carga todos los archivos JSON del historial de Spotify
|
||||
"""
|
||||
archivos = Path(directorio).glob('Streaming_History_Audio_*.json')
|
||||
todos_los_datos = []
|
||||
|
||||
for archivo in archivos:
|
||||
print(f"Cargando {archivo.name}...")
|
||||
with open(archivo, 'r', encoding='utf-8') as f:
|
||||
datos = json.load(f)
|
||||
todos_los_datos.extend(datos)
|
||||
|
||||
print(f"Total de reproducciones cargadas: {len(todos_los_datos)}")
|
||||
return todos_los_datos
|
||||
|
||||
def procesar_datos(datos):
|
||||
"""
|
||||
Convierte los datos JSON en un DataFrame de pandas y procesa las fechas
|
||||
"""
|
||||
df = pd.DataFrame(datos)
|
||||
|
||||
# Convertir timestamp a datetime
|
||||
df['ts'] = pd.to_datetime(df['ts'])
|
||||
|
||||
# Convertir milisegundos a minutos
|
||||
df['minutos_reproducidos'] = df['ms_played'] / 60000
|
||||
|
||||
# Extraer año, mes, día
|
||||
df['año'] = df['ts'].dt.year
|
||||
df['mes'] = df['ts'].dt.month
|
||||
df['año_mes'] = df['ts'].dt.to_period('M')
|
||||
df['nombre_mes'] = df['ts'].dt.strftime('%B %Y')
|
||||
|
||||
# Filtrar solo canciones (no podcasts ni audiolibros)
|
||||
df = df[df['master_metadata_track_name'].notna()]
|
||||
|
||||
return df
|
||||
|
||||
def minutos_por_mes(df):
|
||||
"""
|
||||
Calcula y grafica los minutos reproducidos por mes
|
||||
"""
|
||||
minutos_mes = df.groupby('año_mes')['minutos_reproducidos'].sum().reset_index()
|
||||
minutos_mes['año_mes'] = minutos_mes['año_mes'].astype(str)
|
||||
|
||||
plt.figure(figsize=(15, 6))
|
||||
plt.bar(minutos_mes['año_mes'], minutos_mes['minutos_reproducidos'], color='#1DB954')
|
||||
plt.xlabel('Mes', fontsize=12)
|
||||
plt.ylabel('Minutos Reproducidos', fontsize=12)
|
||||
plt.title('Minutos Reproducidos por Mes', fontsize=16, fontweight='bold')
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.tight_layout()
|
||||
plt.savefig('minutos_por_mes.png', dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
return minutos_mes
|
||||
|
||||
def top_artistas_mes(df, año, mes, top_n=10):
|
||||
"""
|
||||
Obtiene el top 10 de artistas de un mes específico
|
||||
"""
|
||||
df_mes = df[(df['año'] == año) & (df['mes'] == mes)]
|
||||
|
||||
if len(df_mes) == 0:
|
||||
return None
|
||||
|
||||
top_artistas = df_mes.groupby('master_metadata_album_artist_name')['minutos_reproducidos'].sum().sort_values(ascending=False).head(top_n)
|
||||
|
||||
plt.figure(figsize=(12, 8))
|
||||
top_artistas.plot(kind='barh', color='#1DB954')
|
||||
plt.xlabel('Minutos Reproducidos', fontsize=12)
|
||||
plt.ylabel('Artista', fontsize=12)
|
||||
plt.title(f'Top {top_n} Artistas - {mes:02d}/{año}', fontsize=16, fontweight='bold')
|
||||
plt.gca().invert_yaxis()
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'top_artistas_{año}_{mes:02d}.png', dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
return top_artistas
|
||||
|
||||
def top_artistas_año(df, año, top_n=10):
|
||||
"""
|
||||
Obtiene el top 10 de artistas de un año específico
|
||||
"""
|
||||
df_año = df[df['año'] == año]
|
||||
|
||||
if len(df_año) == 0:
|
||||
return None
|
||||
|
||||
top_artistas = df_año.groupby('master_metadata_album_artist_name')['minutos_reproducidos'].sum().sort_values(ascending=False).head(top_n)
|
||||
|
||||
plt.figure(figsize=(12, 8))
|
||||
top_artistas.plot(kind='barh', color='#1ED760')
|
||||
plt.xlabel('Minutos Reproducidos', fontsize=12)
|
||||
plt.ylabel('Artista', fontsize=12)
|
||||
plt.title(f'Top {top_n} Artistas - {año}', fontsize=16, fontweight='bold')
|
||||
plt.gca().invert_yaxis()
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'top_artistas_{año}.png', dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
return top_artistas
|
||||
|
||||
def top_canciones_mes(df, año, mes, top_n=10):
|
||||
"""
|
||||
Obtiene el top 10 de canciones de un mes específico
|
||||
"""
|
||||
df_mes = df[(df['año'] == año) & (df['mes'] == mes)]
|
||||
|
||||
if len(df_mes) == 0:
|
||||
return None
|
||||
|
||||
df_mes['cancion_artista'] = df_mes['master_metadata_track_name'] + ' - ' + df_mes['master_metadata_album_artist_name']
|
||||
top_canciones = df_mes.groupby('cancion_artista')['minutos_reproducidos'].sum().sort_values(ascending=False).head(top_n)
|
||||
|
||||
plt.figure(figsize=(12, 10))
|
||||
top_canciones.plot(kind='barh', color='#1DB954')
|
||||
plt.xlabel('Minutos Reproducidos', fontsize=12)
|
||||
plt.ylabel('Canción', fontsize=12)
|
||||
plt.title(f'Top {top_n} Canciones - {mes:02d}/{año}', fontsize=16, fontweight='bold')
|
||||
plt.gca().invert_yaxis()
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'top_canciones_{año}_{mes:02d}.png', dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
return top_canciones
|
||||
|
||||
def top_canciones_año(df, año, top_n=10):
|
||||
"""
|
||||
Obtiene el top 10 de canciones de un año específico
|
||||
"""
|
||||
df_año = df[df['año'] == año]
|
||||
|
||||
if len(df_año) == 0:
|
||||
return None
|
||||
|
||||
df_año['cancion_artista'] = df_año['master_metadata_track_name'] + ' - ' + df_año['master_metadata_album_artist_name']
|
||||
top_canciones = df_año.groupby('cancion_artista')['minutos_reproducidos'].sum().sort_values(ascending=False).head(top_n)
|
||||
|
||||
plt.figure(figsize=(12, 10))
|
||||
top_canciones.plot(kind='barh', color='#1ED760')
|
||||
plt.xlabel('Minutos Reproducidos', fontsize=12)
|
||||
plt.ylabel('Canción', fontsize=12)
|
||||
plt.title(f'Top {top_n} Canciones - {año}', fontsize=16, fontweight='bold')
|
||||
plt.gca().invert_yaxis()
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'top_canciones_{año}.png', dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
return top_canciones
|
||||
|
||||
def resumen_estadisticas(df):
|
||||
"""
|
||||
Muestra un resumen de estadísticas generales
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("RESUMEN DE ESTADÍSTICAS")
|
||||
print("="*60)
|
||||
print(f"Total de reproducciones: {len(df):,}")
|
||||
print(f"Total de minutos reproducidos: {df['minutos_reproducidos'].sum():,.2f}")
|
||||
print(f"Total de horas reproducidas: {df['minutos_reproducidos'].sum()/60:,.2f}")
|
||||
print(f"Artistas únicos: {df['master_metadata_album_artist_name'].nunique():,}")
|
||||
print(f"Canciones únicas: {df['master_metadata_track_name'].nunique():,}")
|
||||
print(f"Período: {df['ts'].min().date()} a {df['ts'].max().date()}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
def generar_todos_los_graficos(df):
|
||||
"""
|
||||
Genera todos los gráficos para cada año y cada mes disponible
|
||||
"""
|
||||
# Obtener años y meses únicos
|
||||
años = sorted(df['año'].unique())
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("GENERANDO GRÁFICOS")
|
||||
print("="*60)
|
||||
|
||||
# Generar gráficos por año
|
||||
print("\n📊 Generando gráficos anuales...")
|
||||
for año in años:
|
||||
print(f" - Año {año}")
|
||||
top_artistas_año(df, año)
|
||||
top_canciones_año(df, año)
|
||||
|
||||
# Generar gráficos por mes
|
||||
print("\n📊 Generando gráficos mensuales...")
|
||||
for año in años:
|
||||
meses_del_año = sorted(df[df['año'] == año]['mes'].unique())
|
||||
for mes in meses_del_año:
|
||||
print(f" - {año}-{mes:02d}")
|
||||
top_artistas_mes(df, año, mes)
|
||||
top_canciones_mes(df, año, mes)
|
||||
|
||||
print("\n✅ Todos los gráficos generados!")
|
||||
|
||||
# EJECUCIÓN PRINCIPAL
|
||||
if __name__ == "__main__":
|
||||
# 1. Cargar datos
|
||||
datos = cargar_archivos_json()
|
||||
|
||||
# 2. Procesar datos
|
||||
df = procesar_datos(datos)
|
||||
|
||||
# 3. Mostrar resumen
|
||||
resumen_estadisticas(df)
|
||||
|
||||
# 4. Gráfico de minutos por mes
|
||||
print("📊 Generando gráfico general de minutos por mes...")
|
||||
minutos_mes = minutos_por_mes(df)
|
||||
|
||||
# 5. Guardar CSV de Año-Mes-Minutos
|
||||
print("\n💾 Guardando CSV de minutos por mes...")
|
||||
minutos_mes_csv = df.groupby(['año', 'mes'])['minutos_reproducidos'].sum().reset_index()
|
||||
minutos_mes_csv.columns = ['Año', 'Mes', 'Minutos']
|
||||
minutos_mes_csv = minutos_mes_csv.sort_values(['Año', 'Mes'])
|
||||
minutos_mes_csv.to_csv('minutos_por_año_mes.csv', index=False, encoding='utf-8')
|
||||
print("✅ CSV guardado: minutos_por_año_mes.csv")
|
||||
|
||||
# 6. Generar todos los gráficos
|
||||
generar_todos_los_graficos(df)
|
||||
|
||||
# 7. Guardar datos procesados completos en CSV
|
||||
print("\n💾 Guardando datos procesados completos en CSV...")
|
||||
df.to_csv('historial_spotify_procesado.csv', index=False, encoding='utf-8')
|
||||
print("✅ CSV guardado: historial_spotify_procesado.csv")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎉 ANÁLISIS COMPLETADO")
|
||||
print("="*60)
|
||||
print("\nArchivos generados:")
|
||||
print(" 📊 minutos_por_mes.png")
|
||||
print(" 📊 top_artistas_[año].png (para cada año)")
|
||||
print(" 📊 top_canciones_[año].png (para cada año)")
|
||||
print(" 📊 top_artistas_[año]_[mes].png (para cada mes)")
|
||||
print(" 📊 top_canciones_[año]_[mes].png (para cada mes)")
|
||||
print(" 📄 minutos_por_año_mes.csv")
|
||||
print(" 📄 historial_spotify_procesado.csv")
|
||||
print("="*60 + "\n")
|
||||
804
stepmania-ddr/stepmania_simplifier.py
Normal file
804
stepmania-ddr/stepmania_simplifier.py
Normal file
@@ -0,0 +1,804 @@
|
||||
#!/usr/bin/env python3
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import os
|
||||
import re
|
||||
from typing import List, Dict, Tuple
|
||||
import copy
|
||||
import random
|
||||
|
||||
class StepManiaSimplifier:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("StepMania Chart Simplifier v2")
|
||||
self.root.geometry("800x850") # Aumentado un poco el alto para las nuevas opciones
|
||||
|
||||
self.current_file = None
|
||||
self.chart_data = {}
|
||||
|
||||
# Configuración para las opciones de notas
|
||||
self.note_options_config = [
|
||||
{"color": "🟣", "text": "Eliminar notas púrpura (24th+) - Muy rápidas", "var_name": "24th", "default_remove": True},
|
||||
{"color": "🟢", "text": "Eliminar notas verdes (16th) - Semicorcheas", "var_name": "16th", "default_remove": True},
|
||||
{"color": "🟡", "text": "Eliminar notas amarillas (12th) - Tripletes", "var_name": "12th", "default_remove": False},
|
||||
{"color": "🔵", "text": "Eliminar notas azules (8th) - Corcheas", "var_name": "8th", "default_remove": False},
|
||||
]
|
||||
self.keep_percentage_values = ["0%", "10%", "25%", "50%", "75%"]
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def create_note_option_frame(self, parent, row, config):
|
||||
frame = ttk.Frame(parent)
|
||||
frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), padx=(10, 0), pady=2)
|
||||
|
||||
# Variable para "Eliminar X notas"
|
||||
remove_var = tk.BooleanVar(value=config["default_remove"])
|
||||
setattr(self, f"remove_{config['var_name']}", remove_var)
|
||||
|
||||
# Variable para el porcentaje de "Dejar algunas"
|
||||
keep_percentage_var = tk.StringVar(value=self.keep_percentage_values[0]) # Default a 0%
|
||||
setattr(self, f"keep_percentage_{config['var_name']}", keep_percentage_var)
|
||||
|
||||
ttk.Checkbutton(frame, text=f"{config['color']} {config['text']}",
|
||||
variable=remove_var,
|
||||
command=lambda vn=config['var_name']: self.toggle_keep_percentage_option(vn)).pack(side=tk.LEFT)
|
||||
|
||||
ttk.Label(frame, text="Dejar algunas:").pack(side=tk.LEFT, padx=(10, 2))
|
||||
|
||||
keep_combo = ttk.Combobox(frame, textvariable=keep_percentage_var,
|
||||
values=self.keep_percentage_values,
|
||||
width=5, state="disabled" if not config["default_remove"] else "readonly")
|
||||
keep_combo.pack(side=tk.LEFT, padx=(0, 0))
|
||||
setattr(self, f"combo_keep_percentage_{config['var_name']}", keep_combo)
|
||||
|
||||
# Inicializar estado del combobox
|
||||
self.toggle_keep_percentage_option(config['var_name'])
|
||||
|
||||
|
||||
return frame
|
||||
|
||||
def create_jump_option_frame(self, parent, row):
|
||||
frame = ttk.Frame(parent)
|
||||
frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), padx=(10, 0))
|
||||
|
||||
self.remove_jumps = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(frame, text="Eliminar saltos (notas simultáneas)",
|
||||
variable=self.remove_jumps,
|
||||
command=self.toggle_jump_options).pack(side=tk.LEFT)
|
||||
|
||||
self.keep_some_jumps = tk.BooleanVar(value=False) # Este controla si se aplica el porcentaje
|
||||
self.jump_percentage_val = tk.IntVar(value=50) # Este es el valor del slider
|
||||
|
||||
self.check_jumps_some = ttk.Checkbutton(frame, text="Dejar algunos:",
|
||||
variable=self.keep_some_jumps,
|
||||
state="normal" if self.remove_jumps.get() else "disabled",
|
||||
command=self.toggle_jump_slider_visibility) # Comando para mostrar/ocultar slider
|
||||
self.check_jumps_some.pack(side=tk.LEFT, padx=(10, 0))
|
||||
|
||||
self.jump_scale = ttk.Scale(frame, from_=0, to=100, variable=self.jump_percentage_val,
|
||||
orient=tk.HORIZONTAL, length=100, state="disabled") # Inicia deshabilitado
|
||||
self.jump_scale.pack(side=tk.LEFT, padx=(5, 0))
|
||||
|
||||
self.jump_label = ttk.Label(frame, text="50%", state="disabled") # Inicia deshabilitado
|
||||
self.jump_label.pack(side=tk.LEFT, padx=(5, 0))
|
||||
|
||||
self.jump_percentage_val.trace_add('write', self.update_jump_label)
|
||||
self.toggle_jump_options() # Para setear estado inicial correcto
|
||||
self.toggle_jump_slider_visibility() # Para setear estado inicial correcto del slider/label
|
||||
|
||||
return frame
|
||||
|
||||
def toggle_keep_percentage_option(self, var_name):
|
||||
remove_var = getattr(self, f"remove_{var_name}")
|
||||
combo_widget = getattr(self, f"combo_keep_percentage_{var_name}")
|
||||
keep_percentage_var = getattr(self, f"keep_percentage_{var_name}")
|
||||
|
||||
if remove_var.get():
|
||||
combo_widget.config(state="readonly")
|
||||
else:
|
||||
combo_widget.config(state="disabled")
|
||||
keep_percentage_var.set("0%") # Si no se eliminan, no se dejan algunas
|
||||
|
||||
def toggle_jump_options(self):
|
||||
# Habilita/deshabilita el checkbox "Dejar algunos" para saltos
|
||||
if self.remove_jumps.get():
|
||||
self.check_jumps_some.config(state="normal")
|
||||
else:
|
||||
self.check_jumps_some.config(state="disabled")
|
||||
self.keep_some_jumps.set(False) # Si no se eliminan saltos, no se "dejan algunos"
|
||||
self.toggle_jump_slider_visibility() # Actualiza visibilidad del slider y label
|
||||
|
||||
def toggle_jump_slider_visibility(self):
|
||||
# Muestra/oculta el slider y label de porcentaje de saltos
|
||||
# Solo se muestran si "Eliminar saltos" Y "Dejar algunos" están activos
|
||||
if self.remove_jumps.get() and self.keep_some_jumps.get():
|
||||
self.jump_scale.config(state="normal")
|
||||
self.jump_label.config(state="normal")
|
||||
else:
|
||||
self.jump_scale.config(state="disabled")
|
||||
self.jump_label.config(state="disabled")
|
||||
|
||||
|
||||
def update_jump_label(self, *args):
|
||||
self.jump_label.config(text=f"{self.jump_percentage_val.get()}%")
|
||||
|
||||
def setup_ui(self):
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
self.setup_file_selection(main_frame, 0)
|
||||
self.setup_chart_selection(main_frame, 1)
|
||||
self.setup_options(main_frame, 2)
|
||||
self.setup_buttons(main_frame, 3)
|
||||
self.setup_info_area(main_frame, 4)
|
||||
self.setup_debug_area(main_frame, 5)
|
||||
|
||||
def setup_file_selection(self, parent, row):
|
||||
ttk.Label(parent, text="Archivo .sm:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
|
||||
file_frame = ttk.Frame(parent)
|
||||
file_frame.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
file_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.file_var = tk.StringVar()
|
||||
self.file_entry = ttk.Entry(file_frame, textvariable=self.file_var, state="readonly")
|
||||
self.file_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
ttk.Button(file_frame, text="Examinar", command=self.browse_file).grid(row=0, column=1)
|
||||
|
||||
def setup_chart_selection(self, parent, row):
|
||||
base_frame = ttk.LabelFrame(parent, text="Chart Base", padding="10")
|
||||
base_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
base_frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(base_frame, text="Seleccionar chart base:").grid(row=0, column=0, sticky=tk.W)
|
||||
self.base_chart_var = tk.StringVar()
|
||||
self.base_chart_combo = ttk.Combobox(base_frame, textvariable=self.base_chart_var, state="readonly")
|
||||
self.base_chart_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
|
||||
|
||||
def setup_options(self, parent, row):
|
||||
options_frame = ttk.LabelFrame(parent, text="Opciones de Simplificación", padding="10")
|
||||
options_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
options_frame.columnconfigure(1, weight=1)
|
||||
|
||||
current_row = 0
|
||||
|
||||
ttk.Label(options_frame, text="Eliminar notas rápidas:", font=('TkDefaultFont', 9, 'bold')).grid(
|
||||
row=current_row, column=0, columnspan=2, sticky=tk.W, pady=(0, 5))
|
||||
current_row += 1
|
||||
|
||||
for config in self.note_options_config:
|
||||
self.create_note_option_frame(options_frame, current_row, config)
|
||||
current_row +=1
|
||||
|
||||
ttk.Separator(options_frame, orient='horizontal').grid(row=current_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
current_row += 1
|
||||
|
||||
ttk.Label(options_frame, text="Simplificar patrones:", font=('TkDefaultFont', 9, 'bold')).grid(
|
||||
row=current_row, column=0, columnspan=2, sticky=tk.W, pady=(0, 5))
|
||||
current_row += 1
|
||||
|
||||
self.create_jump_option_frame(options_frame, current_row)
|
||||
current_row += 1
|
||||
|
||||
self.simplify_holds = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(options_frame, text="Convertir holds largos en notas normales",
|
||||
variable=self.simplify_holds).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, padx=(10, 0))
|
||||
current_row += 1
|
||||
|
||||
ttk.Separator(options_frame, orient='horizontal').grid(row=current_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
current_row += 1
|
||||
|
||||
ttk.Label(options_frame, text="Nuevo chart:", font=('TkDefaultFont', 9, 'bold')).grid(
|
||||
row=current_row, column=0, columnspan=2, sticky=tk.W, pady=(0, 5))
|
||||
current_row += 1
|
||||
|
||||
ttk.Label(options_frame, text="Nombre nueva dificultad:").grid(row=current_row, column=0, sticky=tk.W, padx=(10, 0))
|
||||
self.new_difficulty_name = tk.StringVar(value="Easy")
|
||||
ttk.Entry(options_frame, textvariable=self.new_difficulty_name, width=15).grid(row=current_row, column=1, sticky=tk.W, padx=5)
|
||||
|
||||
def setup_buttons(self, parent, row):
|
||||
button_frame = ttk.Frame(parent)
|
||||
button_frame.grid(row=row, column=0, columnspan=2, pady=10)
|
||||
|
||||
# El botón de Analizar ya no es necesario aquí, se hace al cargar
|
||||
# ttk.Button(button_frame, text="Analizar Archivo", command=self.analyze_file).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Generar Versión Simplificada", command=self.generate_simplified).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def setup_info_area(self, parent, row):
|
||||
info_frame = ttk.LabelFrame(parent, text="Información del Archivo", padding="10")
|
||||
info_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
|
||||
info_frame.columnconfigure(0, weight=1)
|
||||
info_frame.rowconfigure(0, weight=1)
|
||||
parent.rowconfigure(row, weight=1)
|
||||
|
||||
self.info_text = scrolledtext.ScrolledText(info_frame, height=10, width=70, wrap=tk.WORD) # wrap=tk.WORD
|
||||
self.info_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
def setup_debug_area(self, parent, row):
|
||||
debug_frame = ttk.LabelFrame(parent, text="Debug Output", padding="10")
|
||||
debug_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
debug_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.debug_text = scrolledtext.ScrolledText(debug_frame, height=5, width=70, wrap=tk.WORD) # wrap=tk.WORD
|
||||
self.debug_text.grid(row=0, column=0, sticky=(tk.W, tk.E))
|
||||
|
||||
def browse_file(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Seleccionar archivo .sm",
|
||||
filetypes=[("StepMania files", "*.sm"), ("All files", "*.*")]
|
||||
)
|
||||
if filename:
|
||||
self.file_var.set(filename)
|
||||
self.current_file = filename
|
||||
self.analyze_file() # <--- ANÁLISIS AUTOMÁTICO
|
||||
|
||||
def parse_sm_file(self, filepath: str) -> Dict:
|
||||
# Intenta con 'utf-8' primero, que es lo más común y robusto
|
||||
encodings_to_try = ['utf-8', 'cp1252', 'iso-8859-1', 'latin1']
|
||||
content = None
|
||||
|
||||
for encoding in encodings_to_try:
|
||||
try:
|
||||
with open(filepath, 'r', encoding=encoding) as f:
|
||||
content = f.read()
|
||||
self.debug_print(f"Archivo leído exitosamente con encoding: {encoding}")
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
self.debug_print(f"Fallo al leer con encoding: {encoding}")
|
||||
continue
|
||||
except Exception as e:
|
||||
self.debug_print(f"Error inesperado al abrir el archivo con {encoding}: {e}")
|
||||
continue
|
||||
|
||||
if content is None:
|
||||
# Si todos fallan, intenta con errors='ignore' como último recurso
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
self.debug_print("Archivo leído con encoding utf-8 e ignorando errores.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error de Lectura", f"No se pudo leer el archivo '{os.path.basename(filepath)}' con los encodings probados.\n{e}")
|
||||
return {}
|
||||
|
||||
|
||||
data = {}
|
||||
patterns = {
|
||||
'title': r'#TITLE:([^;]+);',
|
||||
'artist': r'#ARTIST:([^;]+);',
|
||||
'bpms': r'#BPMS:([^;]+);',
|
||||
'offset': r'#OFFSET:([^;]+);'
|
||||
}
|
||||
|
||||
for key, pattern in patterns.items():
|
||||
match = re.search(pattern, content, re.IGNORECASE)
|
||||
data[key] = match.group(1).strip() if match else 'Unknown'
|
||||
|
||||
charts = []
|
||||
# Patrón mejorado para ser más tolerante con espacios y saltos de línea
|
||||
chart_pattern = r'#NOTES:\s*([^:]*):\s*([^:]*):\s*([^:]*):\s*([^:]*):\s*([^:]*):\s*([^;]+);'
|
||||
|
||||
for match in re.finditer(chart_pattern, content, re.DOTALL | re.IGNORECASE):
|
||||
chart = {
|
||||
'type': match.group(1).strip(),
|
||||
'description': match.group(2).strip(), # Author/Description
|
||||
'difficulty': match.group(3).strip(), # Difficulty Name
|
||||
'level': match.group(4).strip(), # Difficulty Meter
|
||||
'radar': match.group(5).strip().replace('\n','').replace('\r','').replace(' ',''), # Radar values
|
||||
'notes': match.group(6).strip() # Note data
|
||||
}
|
||||
charts.append(chart)
|
||||
|
||||
data['charts'] = charts
|
||||
data['original_content'] = content
|
||||
return data
|
||||
|
||||
def analyze_file(self):
|
||||
if not self.current_file:
|
||||
# Esto no debería ocurrir si se llama desde browse_file, pero por si acaso
|
||||
messagebox.showerror("Error", "Por favor selecciona un archivo .sm primero")
|
||||
return
|
||||
|
||||
self.info_text.delete(1.0, tk.END) # Limpiar info anterior
|
||||
self.debug_text.delete(1.0, tk.END) # Limpiar debug anterior
|
||||
self.base_chart_combo['values'] = [] # Limpiar combobox de charts
|
||||
self.base_chart_var.set("")
|
||||
|
||||
try:
|
||||
self.debug_print(f"Analizando archivo: {self.current_file}")
|
||||
self.chart_data = self.parse_sm_file(self.current_file)
|
||||
if not self.chart_data: # Si parse_sm_file devolvió vacío por error
|
||||
return
|
||||
self.display_file_info()
|
||||
self.debug_print("Análisis completado.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Error al analizar el archivo: {str(e)}")
|
||||
self.debug_print(f"Excepción en analyze_file: {str(e)}")
|
||||
|
||||
|
||||
def display_file_info(self):
|
||||
info = f"=== INFORMACIÓN DEL ARCHIVO ===\n"
|
||||
info += f"Título: {self.chart_data.get('title', 'N/A')}\n"
|
||||
info += f"Artista: {self.chart_data.get('artist', 'N/A')}\n"
|
||||
info += f"BPMs: {self.chart_data.get('bpms', 'N/A')}\n"
|
||||
info += f"Offset: {self.chart_data.get('offset', 'N/A')}\n\n"
|
||||
|
||||
info += "=== CHARTS DISPONIBLES ===\n"
|
||||
chart_options = []
|
||||
|
||||
if not self.chart_data.get('charts'):
|
||||
info += "No se encontraron charts válidos en el archivo.\n"
|
||||
self.debug_print("No se encontraron charts en los datos parseados.")
|
||||
else:
|
||||
for i, chart in enumerate(self.chart_data.get('charts', [])):
|
||||
chart_name = f"{chart.get('difficulty','UnknownDif')} (Lv.{chart.get('level','?')}) - {chart.get('description','NoDesc')}"
|
||||
chart_options.append(chart_name)
|
||||
|
||||
info += f"{i+1}. {chart_name}\n"
|
||||
info += f" Tipo: {chart.get('type','N/A')}\n"
|
||||
|
||||
notes_analysis = self.analyze_notes_summary(chart['notes'])
|
||||
info += f" Notas totales: {notes_analysis['total_notes']}\n"
|
||||
info += f" Saltos: {notes_analysis['jumps']}\n"
|
||||
info += f" Holds: {notes_analysis['holds']}\n"
|
||||
info += f" Minas: {notes_analysis['mines']}\n"
|
||||
info += f" Compases estimados: {notes_analysis['measures']}\n\n"
|
||||
|
||||
|
||||
self.base_chart_combo['values'] = chart_options
|
||||
if chart_options:
|
||||
self.base_chart_combo.current(0)
|
||||
else:
|
||||
self.base_chart_var.set("No hay charts disponibles")
|
||||
|
||||
|
||||
self.info_text.delete(1.0, tk.END)
|
||||
self.info_text.insert(1.0, info)
|
||||
|
||||
def analyze_notes_summary(self, notes_data: str) -> Dict:
|
||||
lines = [line.strip() for line in notes_data.split('\n') if line.strip()]
|
||||
|
||||
total_notes = 0
|
||||
jumps = 0
|
||||
holds_start = 0 # '2'
|
||||
mines = 0
|
||||
measures = 0
|
||||
|
||||
for line in lines:
|
||||
if line.startswith(','):
|
||||
measures += 1
|
||||
continue
|
||||
|
||||
# Solo procesar líneas que parecen ser de notas (longitud y caracteres)
|
||||
if len(line) >= 4 and all(c in '01234MFLK' for c in line[:4].upper()): # Simplificado
|
||||
note_count_in_line = 0
|
||||
for char_idx, char_val in enumerate(line[:4]):
|
||||
if char_val == '1': # Tap note
|
||||
total_notes += 1
|
||||
note_count_in_line +=1
|
||||
elif char_val == '2': # Hold start
|
||||
total_notes += 1
|
||||
holds_start += 1
|
||||
note_count_in_line +=1
|
||||
elif char_val == '4': # Roll start (contar como hold)
|
||||
total_notes += 1
|
||||
holds_start += 1
|
||||
note_count_in_line +=1
|
||||
elif char_val.upper() == 'M': # Mine
|
||||
mines +=1
|
||||
|
||||
if note_count_in_line > 1:
|
||||
jumps +=1
|
||||
|
||||
return {
|
||||
'total_notes': total_notes,
|
||||
'jumps': jumps,
|
||||
'holds': holds_start,
|
||||
'mines': mines,
|
||||
'measures': measures
|
||||
}
|
||||
|
||||
def calculate_difficulty_level(self, original_level: int, notes_removed_percentage: float) -> int:
|
||||
if not isinstance(original_level, int) or original_level < 1:
|
||||
original_level = 1 # Default a 1 si el original no es válido
|
||||
|
||||
# Reducción más pronunciada si se quitan muchas notas, menos si se quitan pocas.
|
||||
# Por ejemplo, quitar 50% de notas podría reducir el nivel a la mitad.
|
||||
# Quitar 10% de notas podría reducir el nivel un 10-20%.
|
||||
difficulty_reduction_factor = notes_removed_percentage * 0.7 # Ajustar este factor según se vea
|
||||
|
||||
new_level = original_level * (1 - difficulty_reduction_factor)
|
||||
|
||||
# Asegurar que el nivel no baje de 1 y redondear
|
||||
new_level = max(1, round(new_level))
|
||||
|
||||
return new_level
|
||||
|
||||
def simplify_chart(self, chart: Dict) -> Dict:
|
||||
simplified_chart = copy.deepcopy(chart)
|
||||
notes_lines = chart['notes'].split('\n')
|
||||
|
||||
simplified_lines = []
|
||||
current_measure_lines = []
|
||||
notes_removed_count = 0
|
||||
original_tap_and_hold_notes_count = 0 # Contar solo '1' y '2' para el % de reducción
|
||||
|
||||
# Primera pasada para contar notas originales relevantes
|
||||
for line_idx, line_content in enumerate(notes_lines):
|
||||
stripped_line = line_content.strip()
|
||||
if stripped_line == ',':
|
||||
continue
|
||||
if len(stripped_line) >= 4 and self.is_valid_note_line(stripped_line):
|
||||
for char_note in stripped_line[:4]:
|
||||
if char_note in '124': # Tap, Hold start, Roll start
|
||||
original_tap_and_hold_notes_count += 1
|
||||
|
||||
self.debug_print(f"Notas originales (tap/hold/roll): {original_tap_and_hold_notes_count}")
|
||||
|
||||
|
||||
for line in notes_lines:
|
||||
stripped_line = line.strip()
|
||||
if not stripped_line: # Línea vacía
|
||||
if current_measure_lines: # Procesar compás acumulado si existe
|
||||
processed_measure, removed_in_measure = self.process_measure(current_measure_lines)
|
||||
simplified_lines.extend(processed_measure)
|
||||
notes_removed_count += removed_in_measure
|
||||
current_measure_lines = []
|
||||
simplified_lines.append(line) # Añadir la línea vacía
|
||||
continue
|
||||
|
||||
if stripped_line == ',':
|
||||
if current_measure_lines:
|
||||
processed_measure, removed_in_measure = self.process_measure(current_measure_lines)
|
||||
simplified_lines.extend(processed_measure)
|
||||
notes_removed_count += removed_in_measure
|
||||
current_measure_lines = []
|
||||
simplified_lines.append(line) # Añadir la coma
|
||||
continue
|
||||
|
||||
if self.is_valid_note_line(stripped_line):
|
||||
current_measure_lines.append(line) # Usar línea original con sus espacios
|
||||
else: # Líneas que no son de notas (comentarios, etc.)
|
||||
if current_measure_lines: # Procesar compás acumulado si lo hubiera antes de esta línea no-nota
|
||||
processed_measure, removed_in_measure = self.process_measure(current_measure_lines)
|
||||
simplified_lines.extend(processed_measure)
|
||||
notes_removed_count += removed_in_measure
|
||||
current_measure_lines = []
|
||||
simplified_lines.append(line)
|
||||
|
||||
|
||||
if current_measure_lines: # Procesar el último compás si queda algo
|
||||
processed_measure, removed_in_measure = self.process_measure(current_measure_lines)
|
||||
simplified_lines.extend(processed_measure)
|
||||
notes_removed_count += removed_in_measure
|
||||
|
||||
simplified_chart['notes'] = '\n'.join(simplified_lines)
|
||||
|
||||
try:
|
||||
original_level = int(chart['level'])
|
||||
except ValueError:
|
||||
self.debug_print(f"Nivel original '{chart['level']}' no es un número. Usando 5 por defecto.")
|
||||
original_level = 5 # Default si el nivel no es un número
|
||||
|
||||
notes_removed_percentage = 0
|
||||
if original_tap_and_hold_notes_count > 0:
|
||||
notes_removed_percentage = notes_removed_count / original_tap_and_hold_notes_count
|
||||
|
||||
self.debug_print(f"Notas eliminadas: {notes_removed_count}")
|
||||
self.debug_print(f"Porcentaje de notas eliminadas: {notes_removed_percentage:.2%}")
|
||||
|
||||
new_level = self.calculate_difficulty_level(original_level, notes_removed_percentage)
|
||||
|
||||
simplified_chart['difficulty'] = self.new_difficulty_name.get()
|
||||
simplified_chart['level'] = str(new_level)
|
||||
|
||||
base_desc = chart['description']
|
||||
if "(Simplified)" not in base_desc and "(Easy)" not in base_desc: # Evitar duplicados
|
||||
simplified_chart['description'] = f"{base_desc} ({self.new_difficulty_name.get()})"
|
||||
else: # Si ya tiene un tag de simplificación, lo reemplazamos o usamos el nuevo
|
||||
parts = re.split(r'\s*\(.*\)', base_desc) # Quitar el (tag) viejo
|
||||
simplified_chart['description'] = f"{parts[0].strip()} ({self.new_difficulty_name.get()})"
|
||||
|
||||
|
||||
return simplified_chart
|
||||
|
||||
|
||||
def is_valid_note_line(self, line: str) -> bool:
|
||||
# Una línea de nota válida tiene al menos 4 caracteres y esos son 0-4, M, F, L, K
|
||||
# (considerando mayúsculas para MFLK)
|
||||
if len(line) < 4:
|
||||
return False
|
||||
return all(c in '01234MFLK' for c in line[:4].upper())
|
||||
|
||||
|
||||
def process_measure(self, measure_lines: List[str]) -> Tuple[List[str], int]:
|
||||
if not measure_lines:
|
||||
return [], 0
|
||||
|
||||
processed_lines = []
|
||||
notes_removed_in_measure = 0
|
||||
|
||||
num_lines_in_measure = len(measure_lines)
|
||||
|
||||
for i, original_line_text in enumerate(measure_lines):
|
||||
line_text = original_line_text.strip() # Trabajar con la línea sin espacios extra al inicio/fin
|
||||
|
||||
# Mantener los espacios originales del inicio para el formato
|
||||
leading_whitespace = ""
|
||||
match_whitespace = re.match(r"(\s*)", original_line_text)
|
||||
if match_whitespace:
|
||||
leading_whitespace = match_whitespace.group(1)
|
||||
|
||||
modified_line_chars = list(line_text[:4]) # Solo los primeros 4 caracteres para notas
|
||||
rest_of_line = line_text[4:] # Comentarios, etc.
|
||||
|
||||
# 1. Detección de subdivisión y eliminación de notas rápidas
|
||||
subdivision = self.detect_note_subdivision_in_measure(i, num_lines_in_measure)
|
||||
|
||||
option_var_name = None
|
||||
if subdivision == "24th+": option_var_name = "24th"
|
||||
elif subdivision == "16th": option_var_name = "16th"
|
||||
elif subdivision == "12th": option_var_name = "12th"
|
||||
elif subdivision == "8th": option_var_name = "8th"
|
||||
|
||||
line_had_notes = any(c in '124' for c in modified_line_chars)
|
||||
|
||||
if option_var_name:
|
||||
remove_this_subdivision = getattr(self, f"remove_{option_var_name}").get()
|
||||
if remove_this_subdivision:
|
||||
keep_percentage_str = getattr(self, f"keep_percentage_{option_var_name}").get()
|
||||
keep_percentage = float(keep_percentage_str.replace('%','')) / 100.0
|
||||
|
||||
if random.random() >= keep_percentage: # Si random es MAYOR o IGUAL, se elimina
|
||||
for k_idx, k_char in enumerate(modified_line_chars):
|
||||
if k_char in '124': # Tap, Hold, Roll
|
||||
modified_line_chars[k_idx] = '0'
|
||||
notes_removed_in_measure += 1
|
||||
# self.debug_print(f"L{i} ({subdivision}) eliminada (o parte), %keep: {keep_percentage_str}")
|
||||
# else:
|
||||
# self.debug_print(f"L{i} ({subdivision}) MANTENIDA por % ({keep_percentage_str})")
|
||||
|
||||
|
||||
# 2. Simplificación de saltos (después de posible eliminación por subdivisión)
|
||||
current_notes_in_line = sum(1 for char_note in modified_line_chars if char_note in '124')
|
||||
if current_notes_in_line > 1 and self.remove_jumps.get():
|
||||
# self.debug_print(f"Salto detectado en L{i}: {''.join(modified_line_chars)}")
|
||||
notes_indices_in_jump = [idx for idx, char_note in enumerate(modified_line_chars) if char_note in '124']
|
||||
|
||||
notes_to_keep_count = 1 # Por defecto, dejar 1 nota de un salto
|
||||
if self.keep_some_jumps.get(): # Si el checkbox "Dejar algunos [saltos]" está activo
|
||||
percentage_to_keep_jumps = self.jump_percentage_val.get() / 100.0
|
||||
# self.debug_print(f" Intentando mantener {percentage_to_keep_jumps*100}% de {current_notes_in_line} notas del salto")
|
||||
notes_to_keep_count = max(1, int(round(len(notes_indices_in_jump) * percentage_to_keep_jumps)))
|
||||
|
||||
# self.debug_print(f" Original: {notes_indices_in_jump}, a mantener: {notes_to_keep_count}")
|
||||
|
||||
# Mantener las primeras 'notes_to_keep_count' notas, eliminar el resto
|
||||
random.shuffle(notes_indices_in_jump) # Aleatorizar cuáles se quedan
|
||||
notes_to_remove_from_jump = notes_indices_in_jump[notes_to_keep_count:]
|
||||
|
||||
for idx_to_remove in notes_to_remove_from_jump:
|
||||
if modified_line_chars[idx_to_remove] in '124':
|
||||
modified_line_chars[idx_to_remove] = '0'
|
||||
notes_removed_in_measure += 1
|
||||
# self.debug_print(f" Salto simplificado L{i}: {''.join(modified_line_chars)}")
|
||||
|
||||
|
||||
# 3. Simplificación de holds (después de todo lo anterior)
|
||||
if self.simplify_holds.get():
|
||||
for k_idx, k_char in enumerate(modified_line_chars):
|
||||
if k_char == '2': # Inicio de Hold
|
||||
modified_line_chars[k_idx] = '1' # Convertir a Tap
|
||||
# No contamos esto como nota eliminada, es una conversión
|
||||
elif k_char == '3': # Fin de Hold
|
||||
modified_line_chars[k_idx] = '0' # Eliminar marcador de fin
|
||||
elif k_char == '4': # Inicio de Roll
|
||||
modified_line_chars[k_idx] = '1' # Convertir a Tap
|
||||
# No se tocan 'L' (fin de roll) ya que no tienen valor numérico en StepMania y son más raros
|
||||
|
||||
processed_lines.append(leading_whitespace + "".join(modified_line_chars) + rest_of_line)
|
||||
|
||||
return processed_lines, notes_removed_in_measure
|
||||
|
||||
|
||||
def detect_note_subdivision_in_measure(self, line_index: int, total_lines_in_measure: int) -> str:
|
||||
"""
|
||||
Detecta la subdivisión de una nota basada en su índice dentro del compás y el total de líneas.
|
||||
Esta es una heurística y puede no ser perfecta para todos los casos de BPM changes o time signatures.
|
||||
Asume que las líneas de notas están distribuidas uniformemente dentro del compás.
|
||||
"""
|
||||
if total_lines_in_measure == 0: return "Unknown"
|
||||
|
||||
# Casos comunes para 4/4 time signature
|
||||
if total_lines_in_measure % 48 == 0: # Probablemente 48ths (o 24ths si son pares)
|
||||
beat_division = 48
|
||||
elif total_lines_in_measure % 32 == 0: # Probablemente 32nds
|
||||
beat_division = 32
|
||||
elif total_lines_in_measure % 24 == 0: # Probablemente 24ths
|
||||
beat_division = 24
|
||||
elif total_lines_in_measure % 16 == 0: # Probablemente 16ths
|
||||
beat_division = 16
|
||||
elif total_lines_in_measure % 12 == 0: # Probablemente 12ths (triplets over 4th)
|
||||
beat_division = 12
|
||||
elif total_lines_in_measure % 8 == 0: # Probablemente 8ths
|
||||
beat_division = 8
|
||||
elif total_lines_in_measure % 6 == 0: # Probablemente 6ths (triplets over 8th, raro pero posible)
|
||||
beat_division = 6
|
||||
elif total_lines_in_measure % 4 == 0: # Probablemente 4ths
|
||||
beat_division = 4
|
||||
elif total_lines_in_measure % 3 == 0 and total_lines_in_measure <= 12 : # Podría ser un compás de 3/4 en 4ths, o 4/4 en 3 notas por alguna razón
|
||||
beat_division = total_lines_in_measure # ej. 3 notas: 3rd, 6 notas: 6th
|
||||
elif total_lines_in_measure % 2 == 0 and total_lines_in_measure <= 8:
|
||||
beat_division = total_lines_in_measure
|
||||
else: # Casos menos comunes o compases con pocas notas
|
||||
if total_lines_in_measure > 16 : return "24th+" # Si hay muchas, default a muy rápidas
|
||||
if total_lines_in_measure > 12 : return "16th"
|
||||
if total_lines_in_measure > 8 : return "12th"
|
||||
if total_lines_in_measure > 4 : return "8th"
|
||||
return "4th"
|
||||
|
||||
# Simplificación de la lógica de subdivisión
|
||||
# El `line_index` nos dice en qué "slot" de la subdivisión más fina cae esta línea.
|
||||
# Si `total_lines_in_measure` es 16 (16ths), y `line_index` es 0, 4, 8, 12, es un 4th.
|
||||
# Si es 2, 6, 10, 14, es un 8th (pero no 4th).
|
||||
# El resto son 16ths.
|
||||
|
||||
if line_index % (beat_division / 4) == 0: return "4th"
|
||||
if beat_division >= 8 and line_index % (beat_division / 8) == 0: return "8th"
|
||||
if beat_division >= 12 and line_index % (beat_division / 12) == 0: return "12th"
|
||||
if beat_division >= 16 and line_index % (beat_division / 16) == 0: return "16th"
|
||||
if beat_division >= 24 : return "24th+" # Cubre 24th, 32nd, 48th, etc.
|
||||
|
||||
return "Unknown" # Default por si acaso
|
||||
|
||||
|
||||
def debug_print(self, message):
|
||||
if hasattr(self, 'debug_text') and self.debug_text:
|
||||
try:
|
||||
self.debug_text.insert(tk.END, f"{message}\n")
|
||||
self.debug_text.see(tk.END)
|
||||
self.root.update_idletasks() # Forzar actualización de la UI
|
||||
except tk.TclError: # En caso de que el widget ya no exista (ej. al cerrar)
|
||||
pass
|
||||
print(f"DEBUG: {message}") # También imprimir a consola
|
||||
|
||||
|
||||
def remove_jump_notes(self, line: str) -> str:
|
||||
# Esta función ya no se usa directamente, su lógica está en process_measure
|
||||
# Se deja aquí por si se quiere reusar o como referencia, pero no es llamada.
|
||||
if len(line) < 4:
|
||||
return line
|
||||
|
||||
chars = list(line)
|
||||
jump_notes_indices = [i for i, char_note in enumerate(chars[:4]) if char_note in '124']
|
||||
|
||||
if len(jump_notes_indices) > 1: # Es un salto
|
||||
if self.remove_jumps.get(): # Si la opción general de eliminar saltos está activa
|
||||
notes_to_keep_count = 1 # Por defecto, dejar 1 nota
|
||||
|
||||
if self.keep_some_jumps.get(): # Si el checkbox "Dejar algunos [saltos]" está activo
|
||||
percentage_to_keep = self.jump_percentage_val.get() / 100.0
|
||||
notes_to_keep_count = max(1, int(round(len(jump_notes_indices) * percentage_to_keep)))
|
||||
|
||||
# Aleatorizar y seleccionar las notas a mantener
|
||||
random.shuffle(jump_notes_indices)
|
||||
notes_to_remove_indices = jump_notes_indices[notes_to_keep_count:]
|
||||
|
||||
for idx_to_remove in notes_to_remove_indices:
|
||||
chars[idx_to_remove] = '0'
|
||||
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def simplify_hold_notes(self, line: str) -> str:
|
||||
# Esta función ya no se usa directamente, su lógica está en process_measure
|
||||
modified_line = list(line)
|
||||
for i, char_val in enumerate(modified_line[:4]):
|
||||
if char_val == '2': # Hold Start
|
||||
modified_line[i] = '1'
|
||||
elif char_val == '3': # Hold End
|
||||
modified_line[i] = '0'
|
||||
elif char_val == '4': # Roll Start
|
||||
modified_line[i] = '1'
|
||||
return "".join(modified_line)
|
||||
|
||||
|
||||
def generate_simplified(self):
|
||||
if not self.chart_data or not self.chart_data.get('charts'):
|
||||
messagebox.showerror("Error", "Por favor carga y analiza un archivo primero, o el archivo no contiene charts.")
|
||||
return
|
||||
|
||||
if not self.base_chart_var.get() or "No hay charts" in self.base_chart_var.get():
|
||||
messagebox.showerror("Error", "Por favor selecciona un chart base válido.")
|
||||
return
|
||||
|
||||
try:
|
||||
selected_index = self.base_chart_combo.current()
|
||||
if selected_index < 0 or selected_index >= len(self.chart_data['charts']):
|
||||
messagebox.showerror("Error", "Chart seleccionado no válido. Intenta recargar el archivo.")
|
||||
return
|
||||
|
||||
base_chart = self.chart_data['charts'][selected_index]
|
||||
self.debug_print(f"Generando versión simplificada para: {base_chart.get('difficulty')} (Lv.{base_chart.get('level')})")
|
||||
|
||||
simplified_chart = self.simplify_chart(base_chart)
|
||||
|
||||
# Contar notas '1' (tap) en el chart simplificado para una verificación rápida
|
||||
simplified_tap_notes_count = 0
|
||||
for line_s in simplified_chart['notes'].split('\n'):
|
||||
if self.is_valid_note_line(line_s.strip()):
|
||||
for char_s in line_s.strip()[:4]:
|
||||
if char_s == '1':
|
||||
simplified_tap_notes_count +=1
|
||||
|
||||
self.debug_print(f"Chart simplificado: {simplified_tap_notes_count} notas '1' (tap) finales.")
|
||||
self.debug_print(f"Nivel original: {base_chart['level']}, Nivel calculado: {simplified_chart['level']}")
|
||||
|
||||
if simplified_tap_notes_count == 0: # Chequeo más específico
|
||||
if not messagebox.askyesno("Advertencia",
|
||||
"El chart simplificado no tiene notas '1' (tap notes).\n"
|
||||
"Esto puede resultar en un chart vacío o no jugable.\n"
|
||||
"¿Deseas generarlo de todas formas?"):
|
||||
self.debug_print("Generación cancelada por el usuario debido a 0 notas tap.")
|
||||
return
|
||||
|
||||
new_content = self.chart_data['original_content']
|
||||
|
||||
# Asegurarse de que el nuevo bloque de notas se añade al final del archivo
|
||||
if not new_content.endswith('\n\n'):
|
||||
if new_content.endswith('\n'):
|
||||
new_content += '\n'
|
||||
else:
|
||||
new_content += '\n\n'
|
||||
|
||||
# Formato del bloque de notas
|
||||
# //---------------dance-single - [Simplificado (Easy)]----------------
|
||||
comment_desc = simplified_chart['description'].replace(':','-') # Evitar problemas con ':' en comentarios
|
||||
comment = f"//---------------{simplified_chart['type']} - {comment_desc}----------------\n"
|
||||
|
||||
new_notes_block = comment
|
||||
new_notes_block += "#NOTES:\n"
|
||||
new_notes_block += f" {simplified_chart['type']}:\n"
|
||||
new_notes_block += f" {simplified_chart['description']}:\n" # Ya incluye el (Simplified) o (Easy)
|
||||
new_notes_block += f" {simplified_chart['difficulty']}:\n"
|
||||
new_notes_block += f" {simplified_chart['level']}:\n"
|
||||
new_notes_block += f" {simplified_chart['radar']}:\n" # Usar el radar original
|
||||
new_notes_block += f"{simplified_chart['notes']};\n\n"
|
||||
|
||||
new_content += new_notes_block
|
||||
|
||||
original_path = self.current_file
|
||||
base_name = os.path.splitext(original_path)[0]
|
||||
|
||||
# Construir nombre de archivo con tag de dificultad si es posible
|
||||
difficulty_tag = self.new_difficulty_name.get().replace(" ", "_")
|
||||
new_path = f"{base_name}_{difficulty_tag}_simplified.sm"
|
||||
|
||||
# Intentar guardar con el encoding original si es conocido, sino utf-8
|
||||
source_encoding = 'utf-8' # Default
|
||||
# (No tenemos una forma fácil de saber el encoding original exacto después de leerlo)
|
||||
|
||||
with open(new_path, 'w', encoding=source_encoding, errors='ignore') as f:
|
||||
f.write(new_content)
|
||||
|
||||
messagebox.showinfo("Éxito", f"Archivo simplificado guardado como:\n{new_path}")
|
||||
self.debug_print(f"Archivo simplificado guardado en: {new_path}")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Error al generar archivo simplificado: {str(e)}")
|
||||
self.debug_print(f"Excepción en generate_simplified: {str(e)}")
|
||||
import traceback
|
||||
self.debug_print(traceback.format_exc())
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = StepManiaSimplifier(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1208
stepmania-ddr/stepmania_sm_generator.py
Normal file
1208
stepmania-ddr/stepmania_sm_generator.py
Normal file
File diff suppressed because it is too large
Load Diff
140
truenas/ExtractMounts-freenasv1.py
Normal file
140
truenas/ExtractMounts-freenasv1.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import os
|
||||
"""
|
||||
Tool to extract and print SMB/CIFS and NFS share configurations from a FreeNAS v1
|
||||
SQLite database. It reads share records, resolves NFS paths from auxiliary tables
|
||||
when available, and outputs a human-readable summary with key settings and
|
||||
additional properties, while omitting internal IDs. Intended for quick auditing
|
||||
or migration reporting of share configurations.
|
||||
"""
|
||||
|
||||
DB_FILE = 'freenas-v1.db'
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
|
||||
def print_section(title):
|
||||
print("\n" + "#" * 60)
|
||||
print(f" {title}")
|
||||
print("#" * 60)
|
||||
|
||||
def dump_row(row, indent=2):
|
||||
"""Print all columns that have data (ignore None or empty)"""
|
||||
max_len = 0
|
||||
# Calculate width for alignment
|
||||
valid_keys = [k for k, v in row.items() if v is not None and v != '']
|
||||
if not valid_keys: return
|
||||
|
||||
max_len = max(len(k) for k in valid_keys)
|
||||
|
||||
for key in valid_keys:
|
||||
# Omit internal IDs that do not provide useful info to the user
|
||||
if key == 'id': continue
|
||||
val = row[key]
|
||||
print(f"{' ' * indent}{key.ljust(max_len)} : {val}")
|
||||
|
||||
def get_smb_shares(cursor):
|
||||
print_section("SMB / CIFS SHARES (Complete Configuration)")
|
||||
try:
|
||||
cursor.execute("SELECT * FROM sharing_cifs_share")
|
||||
shares = cursor.fetchall()
|
||||
|
||||
if not shares:
|
||||
print("No SMB shares found.")
|
||||
return
|
||||
|
||||
for share in shares:
|
||||
print(f"\n--- Share: {share.get('cifs_name', 'No Name')} ---")
|
||||
|
||||
# Print main path
|
||||
print(f" Main Path : {share.get('cifs_path', 'N/A')}")
|
||||
|
||||
# Print Hosts Allow/Deny specifically if they exist
|
||||
if share.get('cifs_hostsallow'):
|
||||
print(f" HOSTS ALLOW : {share['cifs_hostsallow']}")
|
||||
if share.get('cifs_hostsdeny'):
|
||||
print(f" HOSTS DENY : {share['cifs_hostsdeny']}")
|
||||
|
||||
# Print the rest of the properties dynamically
|
||||
print(" [Additional Details]:")
|
||||
dump_row(share, indent=4)
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f"Error reading SMB table: {e}")
|
||||
|
||||
def get_nfs_shares(cursor):
|
||||
print_section("NFS SHARES (Complete Configuration)")
|
||||
try:
|
||||
cursor.execute("SELECT * FROM sharing_nfs_share")
|
||||
nfs_shares = cursor.fetchall()
|
||||
|
||||
if not nfs_shares:
|
||||
print("No NFS shares found.")
|
||||
return
|
||||
|
||||
# Try to detect the name of the paths table
|
||||
path_table = None
|
||||
try:
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sharing_nfs_share_paths'")
|
||||
if cursor.fetchone(): path_table = 'sharing_nfs_share_paths'
|
||||
else:
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sharing_nfs_share_path'")
|
||||
if cursor.fetchone(): path_table = 'sharing_nfs_share_path'
|
||||
except:
|
||||
pass
|
||||
|
||||
for share in nfs_shares:
|
||||
share_id = share.get('id')
|
||||
print(f"\n--- NFS Share ID: {share_id} ---")
|
||||
|
||||
# 1. Intentar obtener rutas (Paths)
|
||||
paths = []
|
||||
if path_table:
|
||||
try:
|
||||
cursor.execute(f"SELECT path FROM {path_table} WHERE share_id = ?", (share_id,))
|
||||
rows = cursor.fetchall()
|
||||
paths = [r['path'] for r in rows]
|
||||
except Exception as e:
|
||||
paths = [f"Error reading paths: {e}"]
|
||||
|
||||
# Fallback for older versions where the path was in the main table
|
||||
if not paths and 'nfs_path' in share and share['nfs_path']:
|
||||
paths = [share['nfs_path']]
|
||||
|
||||
if paths:
|
||||
print(f" PATHS : {', '.join(paths)}")
|
||||
else:
|
||||
print(f" PATHS : [NOT FOUND OR EMPTY]")
|
||||
|
||||
# 2. Hosts / Networks
|
||||
if share.get('nfs_network'):
|
||||
print(f" ALLOWED NETWORKS: {share['nfs_network']}")
|
||||
if share.get('nfs_hosts'):
|
||||
print(f" ALLOWED HOSTS : {share['nfs_hosts']}")
|
||||
|
||||
# 3. Other details
|
||||
print(" [Configuration]:")
|
||||
dump_row(share, indent=4)
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f"Error reading NFS table: {e}")
|
||||
|
||||
def main():
|
||||
if not os.path.exists(DB_FILE):
|
||||
print(f"ERROR: '{DB_FILE}' not found")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
conn.row_factory = dict_factory
|
||||
cursor = conn.cursor()
|
||||
|
||||
get_smb_shares(cursor)
|
||||
get_nfs_shares(cursor)
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user