corrigindo upload
Deploy / Deploy (push) Failing after 11s
Details
Deploy / Deploy (push) Failing after 11s
Details
This commit is contained in:
parent
b533873e69
commit
a8fdfde48e
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,174 @@
|
|||
bpm: '200'
|
||||
file: floating
|
||||
original_title: floating
|
||||
src: /var/www/html/trens/src_mmpSearch/mmp/floating.mmp
|
||||
tags:
|
||||
TAG:
|
||||
- plugin
|
||||
automation: []
|
||||
bassline: []
|
||||
plugin:
|
||||
- zynaddsubfx
|
||||
sample: []
|
||||
tracks:
|
||||
- arpeggiator:
|
||||
arp: '0'
|
||||
arp-enabled: '0'
|
||||
arpcycle: '0'
|
||||
arpdir: '0'
|
||||
arpgate: '100'
|
||||
arpmiss: '0'
|
||||
arpmode: '0'
|
||||
arprange: '1'
|
||||
arpskip: '0'
|
||||
arptime: '200'
|
||||
arptime_denominator: '4'
|
||||
arptime_numerator: '4'
|
||||
arptime_syncmode: '0'
|
||||
chordcreator:
|
||||
chord: '0'
|
||||
chord-enabled: '0'
|
||||
chordrange: '1'
|
||||
controller:
|
||||
bandwidth_depth: '64'
|
||||
filter_cutoff_depth: '64'
|
||||
filter_q_depth: '64'
|
||||
mod_wheel_depth: '80'
|
||||
panning_depth: '64'
|
||||
pitchwheel_bendrange: '100'
|
||||
portamento_pitchthresh: '3'
|
||||
portamento_pitchthreshtype: '1'
|
||||
portamento_portamento: '0'
|
||||
portamento_propdepth: '90'
|
||||
portamento_proportional: '0'
|
||||
portamento_proprate: '80'
|
||||
portamento_time: '64'
|
||||
portamento_updowntimestretch: '64'
|
||||
resonance_bandwidth_depth: '64'
|
||||
resonance_center_depth: '64'
|
||||
elcut:
|
||||
amt: '0'
|
||||
att: '0'
|
||||
ctlenvamt: '0'
|
||||
dec: '0.5'
|
||||
hold: '0.5'
|
||||
lamt: '0'
|
||||
latt: '0'
|
||||
lpdel: '0'
|
||||
lshp: '0'
|
||||
lspd: '0.1'
|
||||
lspd_denominator: '4'
|
||||
lspd_numerator: '4'
|
||||
lspd_syncmode: '0'
|
||||
pdel: '0'
|
||||
rel: '0.1'
|
||||
sustain: '0.5'
|
||||
userwavefile: ''
|
||||
x100: '0'
|
||||
eldata:
|
||||
fcut: '14000'
|
||||
fres: '0.5'
|
||||
ftype: '0'
|
||||
fwet: '0'
|
||||
elres:
|
||||
amt: '0'
|
||||
att: '0'
|
||||
ctlenvamt: '0'
|
||||
dec: '0.5'
|
||||
hold: '0.5'
|
||||
lamt: '0'
|
||||
latt: '0'
|
||||
lpdel: '0'
|
||||
lshp: '0'
|
||||
lspd: '0.1'
|
||||
lspd_denominator: '4'
|
||||
lspd_numerator: '4'
|
||||
lspd_syncmode: '0'
|
||||
pdel: '0'
|
||||
rel: '0.1'
|
||||
sustain: '0.5'
|
||||
userwavefile: ''
|
||||
x100: '0'
|
||||
elvol:
|
||||
amt: '0'
|
||||
att: '0'
|
||||
ctlenvamt: '0'
|
||||
dec: '0.5'
|
||||
hold: '0.5'
|
||||
lamt: '0'
|
||||
latt: '0'
|
||||
lpdel: '0'
|
||||
lshp: '0'
|
||||
lspd: '0.1'
|
||||
lspd_denominator: '4'
|
||||
lspd_numerator: '4'
|
||||
lspd_syncmode: '0'
|
||||
pdel: '0'
|
||||
rel: '0.1'
|
||||
sustain: '0.5'
|
||||
userwavefile: ''
|
||||
x100: '0'
|
||||
fxchain:
|
||||
enabled: '0'
|
||||
numofeffects: '0'
|
||||
insertion_effects:
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
- part: '-1'
|
||||
type: '0'
|
||||
instrument_effects:
|
||||
- par: '119'
|
||||
preset: '0'
|
||||
route: '0'
|
||||
type: '3'
|
||||
- route: '0'
|
||||
type: '0'
|
||||
- route: '0'
|
||||
type: '0'
|
||||
instrument_name: zynaddsubfx
|
||||
instrumenttrack:
|
||||
basenote: '57'
|
||||
fxch: '0'
|
||||
pan: '0'
|
||||
pitch: '0'
|
||||
pitchrange: '1'
|
||||
usemasterpitch: '1'
|
||||
vol: '100'
|
||||
midiport:
|
||||
basevelocity: '63'
|
||||
fixedinputvelocity: '-1'
|
||||
fixedoutputnote: '-1'
|
||||
fixedoutputvelocity: '-1'
|
||||
inputchannel: '0'
|
||||
inputcontroller: '0'
|
||||
outputchannel: '1'
|
||||
outputcontroller: '0'
|
||||
outputprogram: '1'
|
||||
readable: '0'
|
||||
writable: '0'
|
||||
system_effects:
|
||||
- send_vol: '0'
|
||||
type: '0'
|
||||
vol: '0'
|
||||
- send_vol: '0'
|
||||
type: '0'
|
||||
vol: '0'
|
||||
- send_vol: '0'
|
||||
type: '0'
|
||||
vol: '0'
|
||||
- type: '0'
|
||||
vol: '0'
|
||||
track_name: Ice Rhodes3
|
||||
type: plugin
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,6 +6,9 @@
|
|||
"bassdrum_acoustic02_-_Copia.ogg": {
|
||||
"_isFile": true
|
||||
},
|
||||
"bassdrum01.ogg": {
|
||||
"_isFile": true
|
||||
},
|
||||
"bassdrum_acoustic01_-_Copia.ogg": {
|
||||
"_isFile": true
|
||||
},
|
||||
|
|
@ -15,6 +18,9 @@
|
|||
"bassdrum03_-_Copia.ogg": {
|
||||
"_isFile": true
|
||||
},
|
||||
"bassdrum_acoustic01.ogg": {
|
||||
"_isFile": true
|
||||
},
|
||||
"bassdrum02_-_Copia.ogg": {
|
||||
"_isFile": true
|
||||
}
|
||||
|
|
|
|||
BIN
_data/users.db
BIN
_data/users.db
Binary file not shown.
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
import multiprocessing
|
||||
import concurrent.futures # NOVO: Necessário para usar o ProcessPoolExecutor
|
||||
import csv
|
||||
|
|
@ -7,6 +6,7 @@ import time
|
|||
import glob
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# ================= 1. LIMITADOR DE CPU DINÂMICO =================
|
||||
def configurar_limites_cpu():
|
||||
try:
|
||||
|
|
@ -28,6 +28,7 @@ def configurar_limites_cpu():
|
|||
except Exception as e:
|
||||
print(f"Aviso: Não foi possível limitar CPU automaticamente: {e}")
|
||||
|
||||
|
||||
configurar_limites_cpu()
|
||||
|
||||
# ================= IMPORTS =================
|
||||
|
|
@ -84,6 +85,7 @@ GLOBAL_EMBEDDING = None
|
|||
GLOBAL_CLASSIFIER = None
|
||||
GLOBAL_CLASSES = None
|
||||
|
||||
|
||||
# ================= CLASSE DE AUDITORIA OTIMIZADA =================
|
||||
class Auditoria:
|
||||
def __init__(self, arquivo_csv):
|
||||
|
|
@ -110,7 +112,9 @@ class Auditoria:
|
|||
)
|
||||
self.file.flush()
|
||||
|
||||
def registrar_processamento(self, nome_arquivo, tamanho_bytes, duracao_audio, tempo_gasto):
|
||||
def registrar_processamento(
|
||||
self, nome_arquivo, tamanho_bytes, duracao_audio, tempo_gasto
|
||||
):
|
||||
try:
|
||||
ram_mb = self.process.memory_info().rss / (1024 * 1024)
|
||||
cpu_pct = self.process.cpu_percent(interval=None)
|
||||
|
|
@ -134,7 +138,7 @@ class Auditoria:
|
|||
logging.error(f"Erro ao auditar arquivo: {e}")
|
||||
|
||||
def verificar_marco(self, contagem):
|
||||
pass # Você pode customizar logs de progresso aqui depois se quiser
|
||||
pass # Você pode customizar logs de progresso aqui depois se quiser
|
||||
|
||||
def fechar(self):
|
||||
try:
|
||||
|
|
@ -145,6 +149,7 @@ class Auditoria:
|
|||
|
||||
# ================= FUNÇÕES AUXILIARES =================
|
||||
|
||||
|
||||
def verificar_memoria_segura(tamanho_arquivo_bytes):
|
||||
mem = psutil.virtual_memory()
|
||||
livre_mb = mem.available / (1024 * 1024)
|
||||
|
|
@ -225,7 +230,9 @@ def carregar_database_yaml():
|
|||
logging.error(f"Erro ao ler lote {arquivo}: {e}")
|
||||
print(f"X Erro ao ler {os.path.basename(arquivo)}")
|
||||
|
||||
print(f"--- Total de Projetos no DB (Memória): {len(db)} (de {total_carregados} lidos) ---")
|
||||
print(
|
||||
f"--- Total de Projetos no DB (Memória): {len(db)} (de {total_carregados} lidos) ---"
|
||||
)
|
||||
return db
|
||||
|
||||
|
||||
|
|
@ -300,14 +307,14 @@ def calcular_complexidade(projeto_yaml):
|
|||
score += num_fx * W_FX
|
||||
stats["num_effects"] += num_fx
|
||||
|
||||
if score <= 15:
|
||||
estrelas = 1
|
||||
elif score <= 40:
|
||||
estrelas = 2
|
||||
if score <= 120:
|
||||
estrelas = 4
|
||||
elif score <= 80:
|
||||
estrelas = 3
|
||||
elif score <= 120:
|
||||
estrelas = 4
|
||||
elif score <= 40:
|
||||
estrelas = 2
|
||||
elif score <= 15:
|
||||
estrelas = 1
|
||||
else:
|
||||
estrelas = 5
|
||||
|
||||
|
|
@ -316,6 +323,7 @@ def calcular_complexidade(projeto_yaml):
|
|||
|
||||
# ================= NÚCLEO DE ANÁLISE =================
|
||||
|
||||
|
||||
def detectar_estrutura(audio_vec, sample_rate, duration):
|
||||
try:
|
||||
if duration < 30:
|
||||
|
|
@ -496,7 +504,9 @@ def analisar_faixa(caminho_arquivo, embedding_model, classifier_model, classes_r
|
|||
|
||||
metadata["analise_ia"] = {
|
||||
"genero_macro": genero_pai,
|
||||
"estilo_principal": tags_detectadas[0]["tag"] if tags_detectadas else "Unknown",
|
||||
"estilo_principal": tags_detectadas[0]["tag"]
|
||||
if tags_detectadas
|
||||
else "Unknown",
|
||||
"nuvem_tags": tags_detectadas,
|
||||
}
|
||||
|
||||
|
|
@ -525,6 +535,7 @@ def worker_analise(caminho):
|
|||
global GLOBAL_EMBEDDING, GLOBAL_CLASSIFIER, GLOBAL_CLASSES
|
||||
return analisar_faixa(caminho, GLOBAL_EMBEDDING, GLOBAL_CLASSIFIER, GLOBAL_CLASSES)
|
||||
|
||||
|
||||
def main():
|
||||
print("\n--- INICIANDO PROCESSADOR + AUDITORIA ---")
|
||||
|
||||
|
|
@ -549,11 +560,27 @@ def main():
|
|||
print(f"Auditoria será salva em: {ARQUIVO_AUDITORIA}")
|
||||
|
||||
# === CARREGAMENTO MODELOS GLOBAIS ===
|
||||
path_embed = MODELO_EMBEDDING if os.path.exists(MODELO_EMBEDDING) else os.path.join(BASE_DIR, MODELO_EMBEDDING)
|
||||
path_class = MODELO_CLASSIFIER if os.path.exists(MODELO_CLASSIFIER) else os.path.join(BASE_DIR, MODELO_CLASSIFIER)
|
||||
path_json = MODELO_CLASSES if os.path.exists(MODELO_CLASSES) else os.path.join(BASE_DIR, MODELO_CLASSES)
|
||||
path_embed = (
|
||||
MODELO_EMBEDDING
|
||||
if os.path.exists(MODELO_EMBEDDING)
|
||||
else os.path.join(BASE_DIR, MODELO_EMBEDDING)
|
||||
)
|
||||
path_class = (
|
||||
MODELO_CLASSIFIER
|
||||
if os.path.exists(MODELO_CLASSIFIER)
|
||||
else os.path.join(BASE_DIR, MODELO_CLASSIFIER)
|
||||
)
|
||||
path_json = (
|
||||
MODELO_CLASSES
|
||||
if os.path.exists(MODELO_CLASSES)
|
||||
else os.path.join(BASE_DIR, MODELO_CLASSES)
|
||||
)
|
||||
|
||||
if os.path.exists(path_embed) and os.path.exists(path_class) and os.path.exists(path_json):
|
||||
if (
|
||||
os.path.exists(path_embed)
|
||||
and os.path.exists(path_class)
|
||||
and os.path.exists(path_json)
|
||||
):
|
||||
print("--- Carregando Modelos de IA ---")
|
||||
try:
|
||||
with open(path_json, "r") as f:
|
||||
|
|
@ -561,13 +588,23 @@ def main():
|
|||
|
||||
print("[1/2] Carregando Extrator de Embeddings...")
|
||||
if hasattr(es, "TensorflowPredictEffnetDiscogs"):
|
||||
GLOBAL_EMBEDDING = es.TensorflowPredictEffnetDiscogs(graphFilename=path_embed, output="PartitionedCall:1")
|
||||
GLOBAL_EMBEDDING = es.TensorflowPredictEffnetDiscogs(
|
||||
graphFilename=path_embed, output="PartitionedCall:1"
|
||||
)
|
||||
else:
|
||||
print(" -> Usando TensorflowPredict genérico.")
|
||||
GLOBAL_EMBEDDING = es.TensorflowPredict(graphFilename=path_embed, input="serving_default_model_Placeholder", output="PartitionedCall:1")
|
||||
GLOBAL_EMBEDDING = es.TensorflowPredict(
|
||||
graphFilename=path_embed,
|
||||
input="serving_default_model_Placeholder",
|
||||
output="PartitionedCall:1",
|
||||
)
|
||||
print(" -> Embeddings carregados com sucesso.")
|
||||
|
||||
GLOBAL_CLASSIFIER = es.TensorflowPredict2D(graphFilename=path_class, input="serving_default_model_Placeholder", output="PartitionedCall:0")
|
||||
GLOBAL_CLASSIFIER = es.TensorflowPredict2D(
|
||||
graphFilename=path_class,
|
||||
input="serving_default_model_Placeholder",
|
||||
output="PartitionedCall:0",
|
||||
)
|
||||
print("[2/2] Classificador de Gênero carregado.")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -587,11 +624,19 @@ def main():
|
|||
contagem_sessao = 0
|
||||
|
||||
try:
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
|
||||
resultados_futuros = {executor.submit(worker_analise, caminho): caminho for caminho in a_fazer}
|
||||
with concurrent.futures.ProcessPoolExecutor(
|
||||
max_workers=max_workers
|
||||
) as executor:
|
||||
resultados_futuros = {
|
||||
executor.submit(worker_analise, caminho): caminho for caminho in a_fazer
|
||||
}
|
||||
|
||||
# O as_completed processa os que terminam mais rápido em vez de travar na ordem
|
||||
for future in tqdm(concurrent.futures.as_completed(resultados_futuros), total=len(a_fazer), unit="track"):
|
||||
for future in tqdm(
|
||||
concurrent.futures.as_completed(resultados_futuros),
|
||||
total=len(a_fazer),
|
||||
unit="track",
|
||||
):
|
||||
try:
|
||||
res, duracao, tempo_gasto = future.result()
|
||||
caminho = resultados_futuros[future]
|
||||
|
|
@ -601,7 +646,9 @@ def main():
|
|||
lista_resultados.append(res)
|
||||
|
||||
tamanho = os.path.getsize(caminho)
|
||||
auditor.registrar_processamento(res['arquivo'], tamanho, duracao, tempo_gasto)
|
||||
auditor.registrar_processamento(
|
||||
res["arquivo"], tamanho, duracao, tempo_gasto
|
||||
)
|
||||
|
||||
contagem_sessao += 1
|
||||
auditor.verificar_marco(contagem_sessao)
|
||||
|
|
@ -613,7 +660,7 @@ def main():
|
|||
print("\nParando paralelismo graciosamente...")
|
||||
|
||||
finally:
|
||||
auditor.fechar() # Fecha o CSV com segurança
|
||||
auditor.fechar() # Fecha o CSV com segurança
|
||||
|
||||
tempo_total_sessao = time.time() - auditor.inicio_global
|
||||
print("\n--- FIM DO PROCESSAMENTO ---")
|
||||
|
|
|
|||
|
|
@ -133,12 +133,12 @@ def calcular_nivel_ponderado(
|
|||
pontos += 1
|
||||
|
||||
# 3. Estrutura (Áudio)
|
||||
if qtd_secoes_audio >= 3:
|
||||
pontos += 1.5
|
||||
if qtd_secoes_audio >= 10:
|
||||
pontos += 5
|
||||
elif qtd_secoes_audio >= 5:
|
||||
pontos += 3
|
||||
elif qtd_secoes_audio >= 10:
|
||||
pontos += 5
|
||||
elif qtd_secoes_audio >= 3:
|
||||
pontos += 1.5
|
||||
|
||||
if pontos < 3:
|
||||
return "Iniciante"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import shutil
|
|||
import time
|
||||
import io
|
||||
import threading
|
||||
from threading import Lock
|
||||
|
||||
from flask import Flask, request, jsonify, send_file, redirect
|
||||
from flask_cors import CORS
|
||||
|
|
@ -41,6 +42,15 @@ from utils import (
|
|||
SAMPLE_MANIFEST,
|
||||
)
|
||||
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO
|
||||
) # obriga o servidor a mostrar os sucessos também
|
||||
|
||||
# Trava do build do Jekyll
|
||||
JEKYLL_LOCK = Lock()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# --- CONFIGURAÇÃO DE SEGURANÇA E BANCO ---
|
||||
|
|
@ -164,17 +174,13 @@ def allowed_sample(filename):
|
|||
and filename.rsplit(".", 1)[1].lower() in ALLOWED_SAMPLE_EXTENSIONS
|
||||
)
|
||||
|
||||
|
||||
def run_jekyll_build():
|
||||
RUBY_BIN_PATH = "/usr/bin/ruby3.2"
|
||||
BUNDLE_PATH = (
|
||||
"/nethome/jotachina/projetos/mmpSearch/vendor/bundle/ruby/3.2.0/bin/bundle"
|
||||
)
|
||||
# Prepara o ambiente para o subprocesso
|
||||
BUNDLE_PATH = "/nethome/jotachina/projetos/mmpSearch/vendor/bundle/ruby/3.2.0/bin/bundle"
|
||||
|
||||
env_vars = os.environ.copy()
|
||||
# Adiciona o caminho do Ruby ao PATH do usuário www-data temporariamente
|
||||
env_vars["PATH"] = f"{RUBY_BIN_PATH}:{env_vars.get('PATH', '')}"
|
||||
print("Iniciando build do Jekyll...")
|
||||
|
||||
command = [
|
||||
BUNDLE_PATH,
|
||||
"exec",
|
||||
|
|
@ -183,18 +189,24 @@ def run_jekyll_build():
|
|||
"--destination",
|
||||
"/var/www/html/trens/mmpSearch/",
|
||||
]
|
||||
try:
|
||||
# Redirecionamos a saída para DEVNULL para não encher o buffer e travar
|
||||
subprocess.Popen(
|
||||
command,
|
||||
cwd=BASE_DATA,
|
||||
env=env_vars,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
print("Jekyll Build iniciado em segundo plano (background).")
|
||||
except Exception as e:
|
||||
print(f"Erro ao iniciar Jekyll: {e}")
|
||||
|
||||
logging.info("⏳ Aguardando liberação para iniciar build do Jekyll...")
|
||||
with JEKYLL_LOCK:
|
||||
logging.info("🔨 Iniciando build do Jekyll (Travando a fila)...")
|
||||
try:
|
||||
resultado = subprocess.run(
|
||||
command,
|
||||
cwd=BASE_DATA,
|
||||
env=env_vars,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
logging.info("✅ Jekyll Build concluído com sucesso!")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"❌ ERRO FATAL no Jekyll Build (Code {e.returncode}):\n{e.stderr}")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Erro inesperado ao chamar Jekyll: {e}")
|
||||
|
||||
|
||||
def load_manifest_keys():
|
||||
|
|
@ -321,31 +333,27 @@ def update_xml_paths_exact(mmp_filename, replacements):
|
|||
|
||||
|
||||
def run_heavy_tasks_in_background():
|
||||
"""Esta função roda isolada sem travar o usuário"""
|
||||
print("--- [BACKGROUND] Iniciando reconstrução de índices ---")
|
||||
logging.info("--- [BACKGROUND] Iniciando tarefas assíncronas ---")
|
||||
try:
|
||||
|
||||
# 2. Isso gera os manifestos (Python puro)
|
||||
generate_manifests(SRC_MMPSEARCH)
|
||||
|
||||
# 3. Isso chama o subprocesso do Jekyll (Externo)
|
||||
# Mantém sua função original que usa subprocess
|
||||
run_jekyll_build()
|
||||
|
||||
print("--- [BACKGROUND] Tarefas concluídas com sucesso ---")
|
||||
logging.info("--- [BACKGROUND] Todas as tarefas concluídas ---")
|
||||
except Exception as e:
|
||||
print(f"--- [BACKGROUND] Erro: {e} ---")
|
||||
|
||||
logging.error(f"--- [BACKGROUND] Erro: {e} ---")
|
||||
|
||||
def process_and_build(filename):
|
||||
"""Função chamada pela rota de upload"""
|
||||
# Processamento inicial do arquivo (rápido)
|
||||
result = process_single_file(filename)
|
||||
|
||||
# 1. Preparamos os 3 dados que o main.py exige: (file_name, clean_slug, total_files)
|
||||
name_without_ext = os.path.splitext(filename)[0]
|
||||
clean_slug = slugify(name_without_ext)
|
||||
args_tuple = (filename, clean_slug, 1) # O 1 representa que é só 1 arquivo no total
|
||||
|
||||
# 2. Passamos a tupla!
|
||||
result = process_single_file(args_tuple)
|
||||
|
||||
if result["success"]:
|
||||
# Em vez de chamar rebuild_indexes() direto, criamos a Thread
|
||||
# O Flask vai responder o return abaixo imediatamente,
|
||||
# enquanto a thread continua rodando no servidor.
|
||||
task_thread = threading.Thread(target=run_heavy_tasks_in_background)
|
||||
task_thread.start()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue