auth
Deploy / Deploy (push) Has been cancelled Details

This commit is contained in:
JotaChina 2026-03-26 20:45:04 -03:00
parent 30fab33f02
commit b474a2a181
2 changed files with 241 additions and 162 deletions

View File

@ -15,7 +15,7 @@ plugins:
page_gen-dirs: true page_gen-dirs: true
page_gen: page_gen:
- data: all - data: all_leve
template: projetos template: projetos
dir: projetos dir: projetos
index_files: false index_files: false

View File

@ -1,9 +1,7 @@
import os import os
import subprocess import subprocess
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import gzip
import zipfile import zipfile
import zlib
import json import json
import shutil import shutil
import time import time
@ -17,20 +15,42 @@ from datetime import datetime
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from flask_login import (
LoginManager,
UserMixin,
login_user,
login_required,
logout_user,
current_user,
)
from flask_admin import Admin, AdminIndexView, expose from flask_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla import ModelView
from main import process_single_file, rebuild_indexes, generate_manifests, slugify from main import process_single_file, generate_manifests, slugify
from utils import ALLOWED_EXTENSIONS, ALLOWED_SAMPLE_EXTENSIONS, MMP_FOLDER, MMPZ_FOLDER, DATA_FOLDER, BASE_DATA, SRC_MMPSEARCH, SAMPLE_SRC, METADATA_FOLDER, XML_IMPORTED_PATH_PREFIX, SAMPLE_MANIFEST from utils import (
ALLOWED_EXTENSIONS,
ALLOWED_SAMPLE_EXTENSIONS,
MMP_FOLDER,
MMPZ_FOLDER,
DATA_FOLDER,
BASE_DATA,
SRC_MMPSEARCH,
SAMPLE_SRC,
XML_IMPORTED_PATH_PREFIX,
SAMPLE_MANIFEST,
)
app = Flask(__name__) app = Flask(__name__)
# --- CONFIGURAÇÃO DE SEGURANÇA E BANCO --- # --- CONFIGURAÇÃO DE SEGURANÇA E BANCO ---
app.config['SECRET_KEY'] = '25de5592bf94c2ca18e27baa0ae2d4ee22a63012f32e1be719d31f530c215a387b9ec0c9d96be38e80a7ccdd859e04408facefff8fd9119e7f5a2d987d85abb7' app.config["SECRET_KEY"] = (
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(DATA_FOLDER, 'users.db') "25de5592bf94c2ca18e27baa0ae2d4ee22a63012f32e1be719d31f530c215a387b9ec0c9d96be38e80a7ccdd859e04408facefff8fd9119e7f5a2d987d85abb7"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False )
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join(
DATA_FOLDER, "users.db"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# CORS precisa suportar credenciais para o cookie de login funcionar # CORS precisa suportar credenciais para o cookie de login funcionar
CORS(app, supports_credentials=True) CORS(app, supports_credentials=True)
@ -38,12 +58,13 @@ CORS(app, supports_credentials=True)
db = SQLAlchemy(app) db = SQLAlchemy(app)
bcrypt = Bcrypt(app) bcrypt = Bcrypt(app)
login_manager = LoginManager(app) login_manager = LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = "login"
# ============================================================================== # ==============================================================================
# CLASSES UTILIZADAS # CLASSES UTILIZADAS
# ============================================================================== # ==============================================================================
# --- MODELO DE USUÁRIO --- # --- MODELO DE USUÁRIO ---
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -53,13 +74,14 @@ class User(UserMixin, db.Model):
# Detalhes do usuário # Detalhes do usuário
bio = db.Column(db.String(240), default="") bio = db.Column(db.String(240), default="")
tags = db.Column(db.String(500), default="") # Vamos salvar separado por vírgula tags = db.Column(db.String(500), default="") # Vamos salvar separado por vírgula
avatar_url = db.Column(db.String(255), default="/assets/img/default_avatar.png") avatar_url = db.Column(db.String(255), default="/assets/img/default_avatar.png")
cover_url = db.Column(db.String(255), default="/assets/img/default_cover.jpg") cover_url = db.Column(db.String(255), default="/assets/img/default_cover.jpg")
projects = db.relationship('Project', backref='owner', lazy=True) projects = db.relationship("Project", backref="owner", lazy=True)
samples = db.relationship('Sample', backref='owner', lazy=True) samples = db.relationship("Sample", backref="owner", lazy=True)
recordings = db.relationship('Recording', backref='owner', lazy=True) recordings = db.relationship("Recording", backref="owner", lazy=True)
class SecureModelView(ModelView): class SecureModelView(ModelView):
def is_accessible(self): def is_accessible(self):
@ -69,97 +91,124 @@ class SecureModelView(ModelView):
# Retorna erro JSON ou redireciona (como é painel, melhor redirecionar ou negar) # Retorna erro JSON ou redireciona (como é painel, melhor redirecionar ou negar)
return jsonify({"error": "Acesso restrito a administradores."}), 403 return jsonify({"error": "Acesso restrito a administradores."}), 403
class SecureIndexView(AdminIndexView): class SecureIndexView(AdminIndexView):
@expose('/') @expose("/")
def index(self): def index(self):
if not current_user.is_authenticated or not current_user.is_admin: if not current_user.is_authenticated or not current_user.is_admin:
# Em vez de retornar JSON, redireciona para a home # Em vez de retornar JSON, redireciona para a home
# O usuário verá o botão de login lá. # O usuário verá o botão de login lá.
return redirect('/') return redirect("/")
# OU, se quiser ser mais explícito, retorna 403 mas em HTML (opcional) # OU, se quiser ser mais explícito, retorna 403 mas em HTML (opcional)
# return "<h1>Acesso Negado</h1><p>Você precisa ser admin.</p>", 403 # return "<h1>Acesso Negado</h1><p>Você precisa ser admin.</p>", 403
return super(SecureIndexView, self).index() return super(SecureIndexView, self).index()
# ============================================================================== # ==============================================================================
# CLASSES DE ARTEFATOS # CLASSES DE ARTEFATOS
# ============================================================================== # ==============================================================================
# Tabela de Projetos # Tabela de Projetos
class Project(db.Model): class Project(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False) # Nome do arquivo físico .mmp filename = db.Column(db.String(255), nullable=False) # Nome do arquivo físico .mmp
display_name = db.Column(db.String(255), nullable=False) # Nome "bonito" para exibir display_name = db.Column(
db.String(255), nullable=False
) # Nome "bonito" para exibir
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Chave estrangeira ligando ao usuário # Chave estrangeira ligando ao usuário
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
class Sample(db.Model): class Sample(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False) filename = db.Column(db.String(255), nullable=False)
display_name = db.Column(db.String(255), nullable=False) display_name = db.Column(db.String(255), nullable=False)
category = db.Column(db.String(50), default="Uncategorized") # Ex: drums, bass category = db.Column(db.String(50), default="Uncategorized") # Ex: drums, bass
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
class Recording(db.Model): class Recording(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False) filename = db.Column(db.String(255), nullable=False)
display_name = db.Column(db.String(255), nullable=False) display_name = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
# Cria o banco na inicialização se não existir # Cria o banco na inicialização se não existir
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return User.query.get(int(user_id)) return User.query.get(int(user_id))
# --- FUNÇÕES UTILITÁRIAS --- # --- FUNÇÕES UTILITÁRIAS ---
def allowed_file(filename): def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def allowed_sample(filename): def allowed_sample(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_SAMPLE_EXTENSIONS return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in ALLOWED_SAMPLE_EXTENSIONS
)
def run_jekyll_build(): def run_jekyll_build():
RUBY_BIN_PATH = "/usr/bin/ruby3.2" RUBY_BIN_PATH = "/usr/bin/ruby3.2"
BUNDLE_PATH = "/nethome/jotachina/projetos/mmpSearch/vendor/bundle/ruby/3.2.0/bin/bundle" BUNDLE_PATH = (
"/nethome/jotachina/projetos/mmpSearch/vendor/bundle/ruby/3.2.0/bin/bundle"
)
# Prepara o ambiente para o subprocesso # Prepara o ambiente para o subprocesso
env_vars = os.environ.copy() env_vars = os.environ.copy()
# Adiciona o caminho do Ruby ao PATH do usuário www-data temporariamente # Adiciona o caminho do Ruby ao PATH do usuário www-data temporariamente
env_vars["PATH"] = f"{RUBY_BIN_PATH}:{env_vars.get('PATH', '')}" env_vars["PATH"] = f"{RUBY_BIN_PATH}:{env_vars.get('PATH', '')}"
print("Iniciando build do Jekyll...") print("Iniciando build do Jekyll...")
command = [BUNDLE_PATH, "exec", "jekyll", "build", "--destination", "/var/www/html/trens/mmpSearch/"] command = [
BUNDLE_PATH,
"exec",
"jekyll",
"build",
"--destination",
"/var/www/html/trens/mmpSearch/",
]
try: try:
# Redirecionamos a saída para DEVNULL para não encher o buffer e travar # Redirecionamos a saída para DEVNULL para não encher o buffer e travar
subprocess.Popen( subprocess.Popen(
command, command,
cwd=BASE_DATA, cwd=BASE_DATA,
env=env_vars, env=env_vars,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL,
) )
print("Jekyll Build iniciado em segundo plano (background).") print("Jekyll Build iniciado em segundo plano (background).")
except Exception as e: except Exception as e:
print(f"Erro ao iniciar Jekyll: {e}") print(f"Erro ao iniciar Jekyll: {e}")
def load_manifest_keys(): def load_manifest_keys():
"""Carrega as pastas de fábrica (keys do manifesto).""" """Carrega as pastas de fábrica (keys do manifesto)."""
if not os.path.exists(SAMPLE_MANIFEST): if not os.path.exists(SAMPLE_MANIFEST):
return ["drums", "instruments", "effects", "presets", "samples"] return ["drums", "instruments", "effects", "presets", "samples"]
try: try:
with open(SAMPLE_MANIFEST, 'r', encoding='utf-8') as f: with open(SAMPLE_MANIFEST, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
return list(data.keys()) return list(data.keys())
except Exception: except Exception:
return [] return []
def convert_mmpz_to_mmp(mmpz_path, mmp_target_path): def convert_mmpz_to_mmp(mmpz_path, mmp_target_path):
LMMS_PATH = "/usr/bin/lmms" LMMS_PATH = "/usr/bin/lmms"
"""Tenta descompactar .mmpz usando Python ou LMMS CLI.""" """Tenta descompactar .mmpz usando Python ou LMMS CLI."""
@ -185,7 +234,9 @@ def convert_mmpz_to_mmp(mmpz_path, mmp_target_path):
try: try:
with open(mmp_target_path, "w") as f_out: with open(mmp_target_path, "w") as f_out:
subprocess.run(cmd, stdout=f_out, stderr=subprocess.PIPE, check=True, env=env_vars) subprocess.run(
cmd, stdout=f_out, stderr=subprocess.PIPE, check=True, env=env_vars
)
if os.path.exists(mmp_target_path) and os.path.getsize(mmp_target_path) > 0: if os.path.exists(mmp_target_path) and os.path.getsize(mmp_target_path) > 0:
print("Sucesso: Descompactado via LMMS CLI (--dump).") print("Sucesso: Descompactado via LMMS CLI (--dump).")
@ -194,12 +245,15 @@ def convert_mmpz_to_mmp(mmpz_path, mmp_target_path):
print("Erro: LMMS CLI rodou, mas arquivo de saída está vazio.") print("Erro: LMMS CLI rodou, mas arquivo de saída está vazio.")
return False return False
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Falha crítica no LMMS CLI: {e.stderr.decode('utf-8') if e.stderr else str(e)}") print(
f"Falha crítica no LMMS CLI: {e.stderr.decode('utf-8') if e.stderr else str(e)}"
)
return False return False
except Exception as e: except Exception as e:
print(f"Erro inesperado ao chamar LMMS: {e}") print(f"Erro inesperado ao chamar LMMS: {e}")
return False return False
def analyze_mmp_dependencies(mmp_filename): def analyze_mmp_dependencies(mmp_filename):
"""Analisa um arquivo .mmp XML puro.""" """Analisa um arquivo .mmp XML puro."""
filepath = os.path.join(MMP_FOLDER, mmp_filename) filepath = os.path.join(MMP_FOLDER, mmp_filename)
@ -218,8 +272,9 @@ def analyze_mmp_dependencies(mmp_filename):
missing_samples = set() missing_samples = set()
for audio_node in root.findall(".//audiofileprocessor"): for audio_node in root.findall(".//audiofileprocessor"):
src = audio_node.get('src', '') src = audio_node.get("src", "")
if not src: continue if not src:
continue
is_factory = any(src.startswith(folder + "/") for folder in factory_folders) is_factory = any(src.startswith(folder + "/") for folder in factory_folders)
is_already_imported = src.startswith("src_mmpSearch/samples/imported/") is_already_imported = src.startswith("src_mmpSearch/samples/imported/")
@ -230,6 +285,7 @@ def analyze_mmp_dependencies(mmp_filename):
return len(missing_samples) == 0, list(missing_samples) return len(missing_samples) == 0, list(missing_samples)
def update_xml_paths_exact(mmp_filename, replacements): def update_xml_paths_exact(mmp_filename, replacements):
"""Substitui caminhos no XML baseando-se no mapeamento exato.""" """Substitui caminhos no XML baseando-se no mapeamento exato."""
filepath = os.path.join(MMP_FOLDER, mmp_filename) filepath = os.path.join(MMP_FOLDER, mmp_filename)
@ -240,21 +296,22 @@ def update_xml_paths_exact(mmp_filename, replacements):
replacements_lower = {k.lower(): v for k, v in replacements.items()} replacements_lower = {k.lower(): v for k, v in replacements.items()}
for audio_node in root.findall(".//audiofileprocessor"): for audio_node in root.findall(".//audiofileprocessor"):
src = audio_node.get('src', '') src = audio_node.get("src", "")
if not src: continue if not src:
continue
current_basename = os.path.basename(src) current_basename = os.path.basename(src)
current_basename_lower = current_basename.lower() current_basename_lower = current_basename.lower()
if current_basename_lower in replacements_lower: if current_basename_lower in replacements_lower:
new_filename = replacements_lower[current_basename_lower] new_filename = replacements_lower[current_basename_lower]
new_src = f"{XML_IMPORTED_PATH_PREFIX}/{new_filename}" new_src = f"{XML_IMPORTED_PATH_PREFIX}/{new_filename}"
audio_node.set('src', new_src) audio_node.set("src", new_src)
audio_node.set('vol', audio_node.get('vol', '1')) audio_node.set("vol", audio_node.get("vol", "1"))
print(f"[XML FIX] Substituído: {current_basename} -> {new_src}") print(f"[XML FIX] Substituído: {current_basename} -> {new_src}")
changes_made = True changes_made = True
if changes_made: if changes_made:
tree.write(filepath, encoding='UTF-8', xml_declaration=True) tree.write(filepath, encoding="UTF-8", xml_declaration=True)
return True return True
return True return True
@ -262,6 +319,7 @@ def update_xml_paths_exact(mmp_filename, replacements):
print(f"Erro crítico ao editar XML: {e}") print(f"Erro crítico ao editar XML: {e}")
return False return False
def run_heavy_tasks_in_background(): def run_heavy_tasks_in_background():
"""Esta função roda isolada sem travar o usuário""" """Esta função roda isolada sem travar o usuário"""
print("--- [BACKGROUND] Iniciando reconstrução de índices ---") print("--- [BACKGROUND] Iniciando reconstrução de índices ---")
@ -280,6 +338,7 @@ def run_heavy_tasks_in_background():
except Exception as e: except Exception as e:
print(f"--- [BACKGROUND] Erro: {e} ---") print(f"--- [BACKGROUND] Erro: {e} ---")
def process_and_build(filename): def process_and_build(filename):
"""Função chamada pela rota de upload""" """Função chamada pela rota de upload"""
# Processamento inicial do arquivo (rápido) # Processamento inicial do arquivo (rápido)
@ -292,26 +351,30 @@ def process_and_build(filename):
task_thread = threading.Thread(target=run_heavy_tasks_in_background) task_thread = threading.Thread(target=run_heavy_tasks_in_background)
task_thread.start() task_thread.start()
return jsonify({ return jsonify(
"message": "Sucesso! O projeto foi recebido. O site será atualizado em instantes.", {
"data": result["data"] "message": "Sucesso! O projeto foi recebido. O site será atualizado em instantes.",
}), 200 "data": result["data"],
}
), 200
else: else:
return jsonify({"error": f"Erro no processamento: {result['error']}"}), 500 return jsonify({"error": f"Erro no processamento: {result['error']}"}), 500
# ============================================================================== # ==============================================================================
# ROTAS DE AUTENTICAÇÃO # ROTAS DE AUTENTICAÇÃO
# ============================================================================== # ==============================================================================
@app.route('/api/register', methods=['POST'])
@app.route("/api/register", methods=["POST"])
def register(): def register():
data = request.json data = request.json
if not data or not data.get('username') or not data.get('password'): if not data or not data.get("username") or not data.get("password"):
return jsonify({"message": "Dados incompletos"}), 400 return jsonify({"message": "Dados incompletos"}), 400
if User.query.filter_by(username=data['username']).first(): if User.query.filter_by(username=data["username"]).first():
return jsonify({"message": "Usuário já existe"}), 400 return jsonify({"message": "Usuário já existe"}), 400
hashed_password = bcrypt.generate_password_hash(data['password']).decode('utf-8') hashed_password = bcrypt.generate_password_hash(data["password"]).decode("utf-8")
new_user = User(username=data['username'], password=hashed_password) new_user = User(username=data["username"], password=hashed_password)
try: try:
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
@ -319,41 +382,47 @@ def register():
except Exception as e: except Exception as e:
return jsonify({"message": f"Erro: {str(e)}"}), 500 return jsonify({"message": f"Erro: {str(e)}"}), 500
@app.route('/api/login', methods=['POST'])
@app.route("/api/login", methods=["POST"])
def login(): def login():
data = request.json data = request.json
user = User.query.filter_by(username=data['username']).first() user = User.query.filter_by(username=data["username"]).first()
if user and bcrypt.check_password_hash(user.password, data['password']): if user and bcrypt.check_password_hash(user.password, data["password"]):
login_user(user) login_user(user)
return jsonify({"message": "Login realizado", "user": user.username}), 200 return jsonify({"message": "Login realizado", "user": user.username}), 200
return jsonify({"message": "Credenciais inválidas"}), 401 return jsonify({"message": "Credenciais inválidas"}), 401
@app.route('/api/logout', methods=['POST'])
@app.route("/api/logout", methods=["POST"])
@login_required @login_required
def logout(): def logout():
logout_user() logout_user()
return jsonify({"message": "Logout realizado"}), 200 return jsonify({"message": "Logout realizado"}), 200
@app.route('/api/check_auth', methods=['GET'])
@app.route("/api/check_auth", methods=["GET"])
def check_auth(): def check_auth():
if current_user.is_authenticated: if current_user.is_authenticated:
return jsonify({"logged_in": True, "user": current_user.username}) return jsonify({"logged_in": True, "user": current_user.username})
return jsonify({"logged_in": False}) return jsonify({"logged_in": False})
# No upload_server.py, adicione esta rota # No upload_server.py, adicione esta rota
@app.route('/api/user/update', methods=['POST']) @app.route("/api/user/update", methods=["POST"])
@login_required @login_required
def update_profile(): def update_profile():
data = request.json data = request.json
new_username = data.get('username') new_username = data.get("username")
if new_username and new_username != current_user.username: if new_username and new_username != current_user.username:
if User.query.filter_by(username=new_username).first(): if User.query.filter_by(username=new_username).first():
return jsonify({"error": "Nome em uso."}), 400 return jsonify({"error": "Nome em uso."}), 400
current_user.username = new_username current_user.username = new_username
if 'bio' in data: current_user.bio = data['bio'][:240] if "bio" in data:
if 'tags' in data: current_user.tags = data['tags'] current_user.bio = data["bio"][:240]
if "tags" in data:
current_user.tags = data["tags"]
try: try:
db.session.commit() db.session.commit()
@ -361,20 +430,23 @@ def update_profile():
except Exception: except Exception:
return jsonify({"error": "Erro ao salvar."}), 500 return jsonify({"error": "Erro ao salvar."}), 500
# Atualize a rota user_profile antiga para retornar os novos dados # Atualize a rota user_profile antiga para retornar os novos dados
@app.route('/api/user/profile', methods=['GET']) @app.route("/api/user/profile", methods=["GET"])
@login_required @login_required
def user_profile(): def user_profile():
# Projetos # Projetos
user_projects = [] user_projects = []
for p in current_user.projects: for p in current_user.projects:
user_projects.append({ user_projects.append(
"id": p.id, {
"display_name": p.display_name, "id": p.id,
"filename": p.filename, "display_name": p.display_name,
"created_at": p.created_at.strftime("%d/%m/%Y"), "filename": p.filename,
"download_link": f"/api/download/{p.filename}" "created_at": p.created_at.strftime("%d/%m/%Y"),
}) "download_link": f"/api/download/{p.filename}",
}
)
# Samples # Samples
user_samples = [] user_samples = []
@ -382,25 +454,30 @@ def user_profile():
# Ajuste do prefixo exato solicitado # Ajuste do prefixo exato solicitado
public_url = f"/mmpSearch/src_mmpSearch/samples/{s.category}/{s.filename}" public_url = f"/mmpSearch/src_mmpSearch/samples/{s.category}/{s.filename}"
user_samples.append({ user_samples.append(
"id": s.id, {
"display_name": s.display_name, "id": s.id,
"category": s.category, "display_name": s.display_name,
"created_at": s.created_at.strftime("%d/%m/%Y"), "category": s.category,
"download_link": public_url "created_at": s.created_at.strftime("%d/%m/%Y"),
}) "download_link": public_url,
}
)
return jsonify({ return jsonify(
"username": current_user.username, {
"bio": current_user.bio, "username": current_user.username,
"tags": current_user.tags, "bio": current_user.bio,
"avatar": current_user.avatar_url, "tags": current_user.tags,
"cover": current_user.cover_url, "avatar": current_user.avatar_url,
"projects": user_projects, "cover": current_user.cover_url,
"samples": user_samples "projects": user_projects,
}) "samples": user_samples,
}
)
@app.route('/api/project/<int:project_id>', methods=['DELETE'])
@app.route("/api/project/<int:project_id>", methods=["DELETE"])
@login_required @login_required
def delete_project(project_id): def delete_project(project_id):
project = Project.query.get_or_404(project_id) project = Project.query.get_or_404(project_id)
@ -425,15 +502,17 @@ def delete_project(project_id):
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# ============================================================================== # ==============================================================================
# ROTAS PRINCIPAIS # ROTAS PRINCIPAIS
# ============================================================================== # ==============================================================================
@app.route("/api/download/<project_name>", methods=["GET"]) @app.route("/api/download/<project_name>", methods=["GET"])
def download_project_package(project_name): def download_project_package(project_name):
"""Gera um ZIP com caminhos limpos (Não exige login para baixar).""" """Gera um ZIP com caminhos limpos (Não exige login para baixar)."""
if not project_name.lower().endswith('.mmp'): if not project_name.lower().endswith(".mmp"):
project_name += '.mmp' project_name += ".mmp"
clean_name = secure_filename(project_name) clean_name = secure_filename(project_name)
mmp_path = os.path.join(MMP_FOLDER, clean_name) mmp_path = os.path.join(MMP_FOLDER, clean_name)
@ -448,15 +527,15 @@ def download_project_package(project_name):
samples_to_pack = set() samples_to_pack = set()
for audio_node in root.findall(".//audiofileprocessor"): for audio_node in root.findall(".//audiofileprocessor"):
src = audio_node.get('src', '') src = audio_node.get("src", "")
if src.startswith(XML_IMPORTED_PATH_PREFIX): if src.startswith(XML_IMPORTED_PATH_PREFIX):
filename = os.path.basename(src) filename = os.path.basename(src)
short_path = f"imported/{filename}" short_path = f"imported/{filename}"
audio_node.set('src', short_path) audio_node.set("src", short_path)
samples_to_pack.add(filename) samples_to_pack.add(filename)
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
xml_str = ET.tostring(root, encoding='utf-8', method='xml') xml_str = ET.tostring(root, encoding="utf-8", method="xml")
zf.writestr(clean_name, xml_str) zf.writestr(clean_name, xml_str)
for sample_name in samples_to_pack: for sample_name in samples_to_pack:
@ -468,9 +547,9 @@ def download_project_package(project_name):
memory_file.seek(0) memory_file.seek(0)
return send_file( return send_file(
memory_file, memory_file,
mimetype='application/zip', mimetype="application/zip",
as_attachment=True, as_attachment=True,
download_name=f"{os.path.splitext(clean_name)[0]}.zip" download_name=f"{os.path.splitext(clean_name)[0]}.zip",
) )
except Exception as e: except Exception as e:
return jsonify({"error": f"Erro ao gerar pacote: {str(e)}"}), 500 return jsonify({"error": f"Erro ao gerar pacote: {str(e)}"}), 500
@ -517,8 +596,8 @@ def upload_file():
# Registra no banco de dados vinculado ao usuário logado # Registra no banco de dados vinculado ao usuário logado
new_project = Project( new_project = Project(
filename=final_mmp_filename, filename=final_mmp_filename,
display_name=clean_name, # Ou name_without_ext se preferir o original display_name=clean_name, # Ou name_without_ext se preferir o original
owner=current_user # Flask-Login pega o user atual owner=current_user, # Flask-Login pega o user atual
) )
db.session.add(new_project) db.session.add(new_project)
db.session.commit() db.session.commit()
@ -528,12 +607,14 @@ def upload_file():
is_clean, missing_files = analyze_mmp_dependencies(final_mmp_filename) is_clean, missing_files = analyze_mmp_dependencies(final_mmp_filename)
if not is_clean: if not is_clean:
return jsonify({ return jsonify(
"status": "missing_samples", {
"message": "Samples externos detectados.", "status": "missing_samples",
"project_file": final_mmp_filename, "message": "Samples externos detectados.",
"missing_files": missing_files "project_file": final_mmp_filename,
}), 202 "missing_files": missing_files,
}
), 202
return process_and_build(final_mmp_filename) return process_and_build(final_mmp_filename)
@ -546,7 +627,7 @@ def upload_file():
@app.route("/api/upload/resolve", methods=["POST"]) @app.route("/api/upload/resolve", methods=["POST"])
@login_required # <--- PROTEGIDO @login_required # <--- PROTEGIDO
def resolve_samples(): def resolve_samples():
project_filename = request.form.get('project_file') project_filename = request.form.get("project_file")
if not project_filename: if not project_filename:
return jsonify({"error": "Nome do projeto não informado"}), 400 return jsonify({"error": "Nome do projeto não informado"}), 400
@ -556,7 +637,8 @@ def resolve_samples():
for original_name, file_storage in request.files.items(): for original_name, file_storage in request.files.items():
if file_storage and allowed_sample(file_storage.filename): if file_storage and allowed_sample(file_storage.filename):
safe_new_name = secure_filename(file_storage.filename) safe_new_name = secure_filename(file_storage.filename)
if not safe_new_name: safe_new_name = f"sample_{int(time.time())}.wav" if not safe_new_name:
safe_new_name = f"sample_{int(time.time())}.wav"
save_path = os.path.join(XML_IMPORTED_PATH_PREFIX, safe_new_name) save_path = os.path.join(XML_IMPORTED_PATH_PREFIX, safe_new_name)
file_storage.save(save_path) file_storage.save(save_path)
@ -571,20 +653,20 @@ def resolve_samples():
return jsonify({"error": "Falha ao atualizar o arquivo de projeto."}), 500 return jsonify({"error": "Falha ao atualizar o arquivo de projeto."}), 500
@app.route('/api/upload/sample', methods=['POST']) @app.route("/api/upload/sample", methods=["POST"])
@login_required @login_required
def upload_sample_standalone(): def upload_sample_standalone():
if 'sample_file' not in request.files: if "sample_file" not in request.files:
return jsonify({'error': 'Nenhum arquivo'}), 400 return jsonify({"error": "Nenhum arquivo"}), 400
file = request.files['sample_file'] file = request.files["sample_file"]
# Pega a pasta escolhida pelo usuário, padrão 'uncategorized' # Pega a pasta escolhida pelo usuário, padrão 'uncategorized'
raw_subfolder = request.form.get('subfolder', 'uncategorized').strip() raw_subfolder = request.form.get("subfolder", "uncategorized").strip()
# Limpa o nome da pasta para evitar ../../etc/passwd # Limpa o nome da pasta para evitar ../../etc/passwd
safe_subfolder = secure_filename(raw_subfolder) safe_subfolder = secure_filename(raw_subfolder)
if not safe_subfolder: if not safe_subfolder:
safe_subfolder = 'uncategorized' safe_subfolder = "uncategorized"
if file and allowed_sample(file.filename): if file and allowed_sample(file.filename):
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
@ -601,7 +683,7 @@ def upload_sample_standalone():
filename=filename, filename=filename,
display_name=filename, display_name=filename,
category=safe_subfolder, category=safe_subfolder,
owner=current_user owner=current_user,
) )
db.session.add(new_sample) db.session.add(new_sample)
db.session.commit() db.session.commit()
@ -609,11 +691,12 @@ def upload_sample_standalone():
generate_manifests(SRC_MMPSEARCH) generate_manifests(SRC_MMPSEARCH)
run_jekyll_build() run_jekyll_build()
return jsonify({'message': 'Sample enviado com sucesso!'}), 200 return jsonify({"message": "Sample enviado com sucesso!"}), 200
return jsonify({'error': 'Tipo de arquivo inválido'}), 400 return jsonify({"error": "Tipo de arquivo inválido"}), 400
@app.route('/api/sample/<int:sample_id>', methods=['DELETE'])
@app.route("/api/sample/<int:sample_id>", methods=["DELETE"])
@login_required @login_required
def delete_sample(sample_id): def delete_sample(sample_id):
sample = Sample.query.get_or_404(sample_id) sample = Sample.query.get_or_404(sample_id)
@ -635,18 +718,14 @@ def delete_sample(sample_id):
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Inicializa o Flask-Admin # Inicializa o Flask-Admin
admin = Admin( admin = Admin(app, name="MMP Admin", url="/api/admin", index_view=SecureIndexView())
app,
name='MMP Admin',
url='/api/admin',
index_view=SecureIndexView()
)
# Adiciona a visualização da tabela de Usuários # Adiciona a visualização da tabela de Usuários
admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários")) admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários"))
if __name__ == "__main__": if __name__ == "__main__":
admin = Admin(app, name='MMP Admin', url='/api/admin', index_view=SecureIndexView()) admin = Admin(app, name="MMP Admin", url="/api/admin", index_view=SecureIndexView())
admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários")) admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários"))
app.run(host="0.0.0.0", port=33002, debug=True) app.run(host="0.0.0.0", port=33002, debug=True)