Release v0.1.0: GDM Refactor, Rate Limit, Metallic UI

This commit is contained in:
2026-01-05 21:14:51 +09:00
commit fac33cff0b
22 changed files with 1829 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
{% extends "base.html" %}
{% import "macro.html" as macros %}
{% block content %}
<style>
/* 이 페이지에서만 전역 로딩 인디케이터 숨김 */
#loading { display: none !important; }
/* Metallic Theme Variables */
:root {
--metal-dark: #1a1a1a;
--metal-surface: linear-gradient(145deg, #2d2d2d, #1a1a1a);
--metal-border: #404040;
--metal-text: #e0e0e0;
--metal-text-muted: #888;
--metal-highlight: #00bcd4; /* Cyan/Blue Neon */
--metal-shadow: 0 4px 6px rgba(0,0,0,0.5);
}
/* Card Override */
.card {
background: var(--metal-surface) !important;
border: 1px solid var(--metal-border) !important;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.6);
color: var(--metal-text);
}
.card-header {
background: rgba(0,0,0,0.2) !important;
border-bottom: 1px solid var(--metal-border) !important;
color: var(--metal-text);
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Table Override */
.table {
color: var(--metal-text) !important;
}
.table thead th {
border-top: none;
border-bottom: 2px solid var(--metal-border);
color: var(--metal-text-muted);
font-size: 0.85rem;
}
.table td {
border-top: 1px solid rgba(255,255,255,0.05);
vertical-align: middle;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255,255,255,0.02) !important;
}
.table-hover tbody tr:hover {
background-color: rgba(255,255,255,0.05) !important;
}
/* Buttons */
.btn-outline-primary {
color: var(--metal-highlight);
border-color: var(--metal-highlight);
}
.btn-outline-primary:hover {
background-color: var(--metal-highlight);
color: #000;
box-shadow: 0 0 10px var(--metal-highlight);
}
.btn-outline-danger {
color: #ff5252;
border-color: #ff5252;
}
.btn-outline-danger:hover {
background-color: #ff5252;
color: white;
box-shadow: 0 0 10px #ff5252;
}
/* Badges */
.badge {
font-weight: 500;
letter-spacing: 0.5px;
}
.badge-outline-secondary {
border: 1px solid #666;
color: #aaa;
background: transparent;
}
.badge-outline-info {
border: 1px solid var(--metal-highlight);
color: var(--metal-highlight);
background: transparent;
}
/* Progress Bar */
.progress {
background-color: rgba(0,0,0,0.5);
border: 1px solid #333;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
background: linear-gradient(90deg, #00acc1, #26c6da);
box-shadow: 0 0 10px rgba(0, 188, 212, 0.5);
font-size: 0.8rem;
line-height: 20px;
}
</style>
<div id="gommi_download_manager_queue_list" class="mt-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">다운로드 목록</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-danger mr-2" onclick="resetList()">
<i class="fa fa-trash"></i> 전체 삭제
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshList()">
<i class="fa fa-refresh"></i> 새로고침
</button>
</div>
</div>
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width: 5%">#</th>
<th style="width: 10%">요청</th>
<th style="width: 35%">제목/URL</th>
<th style="width: 10%">소스</th>
<th style="width: 15%">진행률</th>
<th style="width: 10%">속도</th>
<th style="width: 10%">상태</th>
<th style="width: 15%">작업</th>
</tr>
</thead>
<tbody id="download_list">
<tr>
<td colspan="8" class="text-center text-muted py-4">
다운로드 항목이 없습니다.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="/{{arg.package_name}}/static/{{arg.package_name}}.js"></script>
<script>
console.log("Start Gommi Queue List JS");
// alert("Check: JS Running");
// Functions first
function refreshList(silent) {
// Try multiple URLs to find the correct one
var attempts = [
'/{{arg.package_name}}/ajax/{{arg.module_name}}/list', // New Candidate
'/{{arg.package_name}}/{{arg.module_name}}/ajax/list', // Standard
'/{{arg.package_name}}/{{arg.module_name}}/queue/ajax/list', // Double Queue
'/{{arg.package_name}}/queue/ajax/list' // Direct Queue
];
function tryUrl(index) {
if (index >= attempts.length) {
console.error("All list fetch attempts failed");
return;
}
var url = attempts[index];
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
data: {},
global: !silent, // Silent mode: suppress global loading indicator
success: function(ret) {
if (ret.ret === 'success') {
renderList(ret.data || []);
} else {
// tryUrl(index + 1);
}
},
error: function(e) {
// console.warn("Failed URL:", url);
tryUrl(index + 1);
}
});
}
tryUrl(0);
}
function renderList(items) {
var tbody = document.getElementById('download_list');
if (!tbody) return;
if (!items || items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted py-4">다운로드 항목이 없습니다.</td></tr>';
return;
}
var html = '';
items.forEach(function(item, index) {
html += createDownloadRow(item, index + 1);
});
tbody.innerHTML = html;
}
function createDownloadRow(item, num) {
var statusClass = {
'pending': 'badge-secondary',
'extracting': 'badge-info',
'downloading': 'badge-primary',
'completed': 'badge-success',
'error': 'badge-danger',
'cancelled': 'badge-warning',
'paused': 'badge-warning'
};
var percent = (item.progress && !isNaN(item.progress)) ? item.progress : 0;
var displayTitle = item.title ? item.title : (item.url || 'No Title');
if (displayTitle.length > 50) displayTitle = displayTitle.substring(0, 50) + '...';
return '<tr id="row_' + item.id + '">' +
'<td>' + num + '</td>' +
'<td><span class="badge badge-outline-secondary">' + (item.caller_plugin || 'User') + '</span></td>' +
'<td title="' + (item.url || '') + '">' +
'<div style="max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + displayTitle + '</div>' +
'</td>' +
'<td><span class="badge badge-outline-info">' + (item.source_type || 'auto') + '</span></td>' +
'<td>' +
'<div class="progress" style="height: 20px;">' +
'<div class="progress-bar" role="progressbar" style="width: ' + percent + '%;" aria-valuenow="' + percent + '" aria-valuemin="0" aria-valuemax="100">' + percent + '%</div>' +
'</div>' +
'</td>' +
'<td>' + (item.speed || '-') + '</td>' +
'<td><span class="badge ' + (statusClass[item.status] || 'badge-secondary') + '">' + (item.status || 'unknown') + '</span></td>' +
'<td>' +
(item.status === 'downloading' ? '<button class="btn btn-sm btn-warning" onclick="cancelDownload(\'' + item.id + '\')"><i class="fa fa-stop"></i></button>' : '') +
'</td>' +
'</tr>';
}
function updateDownloadRow(item) {
var row = document.getElementById('row_' + item.id);
if (row) {
row.outerHTML = createDownloadRow(item, row.rowIndex);
} else {
refreshList();
}
}
function cancelDownload(id) {
$.ajax({
url: '/{{arg.package_name}}/ajax/{{arg.module_name}}/cancel', // Use new pattern
type: 'POST',
data: { id: id },
dataType: 'json',
success: function(ret) {
if (ret.msg) {
$.notify('<strong>' + ret.msg + '</strong>', {type: 'success'});
}
}
});
}
function resetList() {
if (!confirm('정말 전체 목록을 삭제하시겠습니까? (진행 중인 작업도 취소됩니다)')) {
return;
}
$.ajax({
url: '/{{arg.package_name}}/ajax/{{arg.module_name}}/reset',
type: 'POST',
data: {},
dataType: 'json',
success: function(ret) {
if (ret.msg) {
$.notify('<strong>' + ret.msg + '</strong>', {type: 'success'});
}
refreshList();
}
});
}
// Socket Init
try {
if (typeof io !== 'undefined') {
// Namespace needs to match default_route_socketio_module attach param
var socket = io.connect('/' + '{{ arg.package_name }}' + '/queue');
socket.on('download_status', function(data) {
updateDownloadRow(data);
});
socket.on('connect', function() {
console.log('Socket connected!');
});
}
} catch (e) {
console.error('Socket.IO init error:', e);
}
// Initial Load
$(document).ready(function() {
console.log("OnReady: Refresh List");
refreshList();
// Auto Refresh logic (fallback for Socket.IO)
setInterval(function() {
// refreshList(); // 전체 갱신 보다는 상태만 가져오는게 좋지만, 일단 전체 갱신
// 조용히 갱신 (Optional: modify refreshList to accept silent flag)
// 단순하게 목록 갱신 호출
refreshList(true); // Silent mode
}, 5000); // 5초마다
});
</script>
{% endblock %}

View File

@@ -0,0 +1,156 @@
{% extends "base.html" %}
{% import "macro.html" as macros %}
{% block content %}
<style>
/* Metallic Theme Variables */
:root {
--metal-dark: #1a1a1a;
--metal-surface: linear-gradient(145deg, #2d2d2d, #1a1a1a);
--metal-border: #404040;
--metal-text: #e0e0e0;
--metal-text-muted: #888;
--metal-highlight: #00bcd4; /* Cyan/Blue Neon */
--metal-input-bg: rgba(0, 0, 0, 0.3);
}
/* Container Spacing */
.container-fluid {
padding-top: 20px;
}
/* Headers */
h4 {
color: var(--metal-text);
font-weight: 300;
letter-spacing: 1px;
text-transform: uppercase;
border-bottom: 2px solid var(--metal-highlight);
display: inline-block;
padding-bottom: 5px;
}
/* Form Controls */
.form-control, .custom-select {
background-color: var(--metal-input-bg) !important;
border: 1px solid var(--metal-border) !important;
color: var(--metal-text) !important;
border-radius: 4px;
transition: all 0.3s ease;
}
.form-control:focus, .custom-select:focus {
background-color: rgba(0,0,0,0.5) !important;
border-color: var(--metal-highlight) !important;
box-shadow: 0 0 10px rgba(0, 188, 212, 0.3) !important;
}
/* Buttons */
.btn-outline-primary {
color: var(--metal-highlight);
border-color: var(--metal-highlight);
}
.btn-outline-primary:hover {
background-color: var(--metal-highlight);
color: #000;
box-shadow: 0 0 15px var(--metal-highlight);
}
/* HR */
hr {
border-top: 1px solid rgba(255,255,255,0.1) !important;
}
/* Labels */
label, strong {
color: #cfcfcf;
font-weight: 500;
}
/* Description text */
em {
color: var(--metal-text-muted);
font-style: normal;
font-size: 0.9em;
}
</style>
<div class="container-fluid">
{{ macros.m_row_start('5') }}
{{ macros.m_row_end() }}
<!-- Header & Save Button -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h4>GDM 설정</h4>
{{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']]) }}
</div>
{{ macros.m_hr_head_bottom() }}
<form id="setting">
<!-- Basic Setting -->
{{ macros.setting_top_big('기본 설정') }}
{{ macros.setting_bottom() }}
{{ macros.setting_input_text('save_path', '저장 경로', value=arg['save_path'], desc='{PATH_DATA}는 실제 데이터 경로로 치환됩니다.') }}
{{ macros.setting_input_text('temp_path', '임시 경로', value=arg['temp_path'], desc='다운로드 중 임시 파일 저장 경로') }}
{{ macros.setting_input_text('max_concurrent', '동시 다운로드 수', value=arg['max_concurrent'], desc='동시에 진행할 최대 다운로드 수') }}
{{ macros.setting_select('max_download_rate', '속도 제한', [['0', '무제한'], ['1M', '1 MB/s'], ['3M', '3 MB/s'], ['5M', '5 MB/s'], ['10M', '10 MB/s']], value=arg['max_download_rate'], desc='다운로드 속도를 제한합니다.') }}
{{ macros.m_hr() }}
<!-- Downloader Setting -->
{{ macros.setting_top_big('다운로더 설정') }}
{{ macros.setting_bottom() }}
{{ macros.setting_input_text('aria2c_path', 'aria2c 경로', value=arg['aria2c_path'], desc='aria2c 실행 파일 경로 (고속 다운로드용)') }}
{{ macros.setting_input_text('aria2c_connections', 'aria2c 연결 수', value=arg['aria2c_connections'], desc='aria2c 동시 연결 수 (기본 16)') }}
{{ macros.setting_input_text('ffmpeg_path', 'ffmpeg 경로', value=arg['ffmpeg_path'], desc='ffmpeg 실행 파일 경로 (HLS 스트림용)') }}
{{ macros.setting_input_text('yt_dlp_path', 'yt-dlp 경로', value=arg['yt_dlp_path'], desc='비워두면 Python 모듈 사용') }}
{{ macros.m_hr() }}
<!-- Retry Setting -->
{{ macros.setting_top_big('재시도 설정') }}
{{ macros.setting_bottom() }}
{{ macros.setting_checkbox('auto_retry', '자동 재시도', value=arg['auto_retry'], desc='다운로드 실패 시 자동으로 재시도') }}
{{ macros.setting_input_text('max_retry', '최대 재시도 횟수', value=arg['max_retry'], desc='최대 재시도 횟수') }}
</form>
</div>
{% endblock %}
{% block tail_js %}
<script type="text/javascript">
var package_name = "{{arg['package_name'] }}";
var sub = "{{arg['module_name'] }}"; // sub usually is module name like 'queue'
// Save Button Logic (Standard FlaskFarm Plugin JS)
// Note: globalSettingSaveBtn logic is usually handled by framework's default plugin.js if available,
// OR we explicitly define it here.
// Gommi plugin loads '/package_name/static/package_name.js' ?
// I recall checking step 21445 it had `<script src="/{{package_name}}/static/{{package_name}}.js"></script>`
// I will explicitly add the save logic just in case the static JS relies on specific form IDs.
$(document).ready(function(){
// Nothing special needed
});
$("body").on('click', '#globalSettingSaveBtn', function(e){
e.preventDefault();
var formData = get_formdata('#setting');
$.ajax({
url: '/' + package_name + '/ajax/' + sub + '/setting_save',
type: "POST",
cache: false,
data: formData,
dataType: "json",
success: function(ret) {
if (ret.ret == 'success') {
$.notify('설정을 저장했습니다.', {type:'success'});
} else {
$.notify('저장 실패: ' + ret.msg, {type:'danger'});
}
}
});
});
</script>
{% endblock %}