first commit
This commit is contained in:
1
pytorrent/static/js/api.js
Normal file
1
pytorrent/static/js/api.js
Normal file
@@ -0,0 +1 @@
|
||||
export const apiSource = " async function post(url,data,method='POST'){\n const res=await fetch(url,{method,headers:{'Content-Type':'application/json','Accept':'application/json'},body:JSON.stringify(data||{})});\n const text=await res.text();\n let json;\n try{ json=JSON.parse(text); }\n catch(e){\n const clean=(text||'').replace(/<[^>]+>/g,' ').replace(/\\s+/g,' ').trim().slice(0,180);\n throw new Error(clean?`Invalid server response (${res.status}): ${clean}`:`Invalid server response (${res.status})`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`Operation failed (${res.status})`);\n return json;\n }\n\n async function runAction(action, extra={}){ const hashes=selectedHashes(); if(!hashes.length) return toast('No torrents selected','warning'); let payload={hashes,...extra}; if(action==='move'){ openPathPicker('move'); return; } setBusy(true); try{ const j=await post(`/api/torrents/${action}`,payload); markQueuedJobs(j, hashes, action); if(action==='recheck'){ hashes.forEach(h=>{ const t=torrents.get(h); if(t) torrents.set(h,{...t,status:'Checking',hashing:1,message:'Force recheck queued'}); }); scheduleRender(true); } const parts=Number(j.bulk_parts||1); toast(parts>1?`${action} queued in ${parts} bulk parts`:`${action} queued`,'success'); if(action==='set_label') await loadLabels(); }catch(e){toast(e.message,'danger');} finally{setBusy(false);} }\n function flag(iso){ const code=String(iso||'').toLowerCase(); return code?`<span class=\"fi fi-${esc(code)}\"></span> <span>${esc(code.toUpperCase())}</span>`:'-'; }\n function table(headers,rows,extraClass=''){ const cls=extraClass?` ${extraClass}`:''; return `<table class=\"table table-sm detail-table${cls}\"><thead><tr>${headers.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.map(r=>`<tr>${r.map(c=>`<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`; }\n function responsiveTable(headers,rows,extraClass=''){ return `<div class=\"responsive-table-wrap\">${table(headers,rows,extraClass)}</div>`; }\n function downloadJson(filename, data){ const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),500); }\n function filenameFromResponse(res, fallback){ const cd=res.headers.get('Content-Disposition')||''; const m=cd.match(/filename\\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?/i); try{ return decodeURIComponent(m?.[1]||m?.[2]||fallback); }catch(e){ return m?.[1]||m?.[2]||fallback; } }\n async function downloadResponse(url, options={}, fallback='download.bin', label='Preparing download...'){\n setBusy(true,label);\n try{\n const res=await fetch(url,options);\n if(!res.ok){ const j=await res.json().catch(()=>({})); throw new Error(j.error||`Download failed: HTTP ${res.status}`); }\n const total=Number(res.headers.get('Content-Length')||0);\n const name=filenameFromResponse(res,fallback);\n let blob;\n if(res.body){\n const reader=res.body.getReader();\n const chunks=[]; let received=0;\n while(true){\n const {done,value}=await reader.read();\n if(done) break;\n chunks.push(value); received += value.length;\n const loader=$('globalLoader');\n const span=loader?.querySelector('span:last-child');\n if(span){\n if(total){\n const pct=Math.max(0,Math.min(100,Math.round((received/total)*100)));\n span.textContent=`Downloading ${pct}%`;\n } else {\n span.textContent=`Downloading ${(received/1024/1024).toFixed(1)} MB`;\n }\n }\n }\n blob=new Blob(chunks);\n } else {\n blob=await res.blob();\n }\n const obj=URL.createObjectURL(blob);\n const a=document.createElement('a'); a.href=obj; a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(obj),1000);\n toast('Download started','success');\n } finally { setBusy(false); }\n }\n async function downloadTorrentFiles(hashes=null){\n const list=hashes||selectedHashes();\n if(!list.length) return toast('No torrents selected','warning');\n if(list.length===1) return downloadResponse(`/api/torrents/${encodeURIComponent(list[0])}/torrent-file`,{},`${list[0]}.torrent`,'Preparing .torrent...').catch(e=>toast(e.message,'danger'));\n return downloadResponse('/api/torrents/torrent-files.zip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({hashes:list})},'pytorrent-torrents.zip','Preparing torrent ZIP...').catch(e=>toast(e.message,'danger'));\n }\n";
|
||||
44
pytorrent/static/js/app.js
Normal file
44
pytorrent/static/js/app.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { stateSource } from './state.js';
|
||||
import { torrentsSource } from './torrents.js';
|
||||
import { apiSource } from './api.js';
|
||||
import { createTorrentSource } from './createTorrent.js';
|
||||
import { torrentDetailsSource } from './torrentDetails.js';
|
||||
import { modalsSource } from './modals.js';
|
||||
import { rssSource } from './rss.js';
|
||||
import { smartQueueSource } from './smartQueue.js';
|
||||
import { plannerSource } from './planner.js';
|
||||
import { pollerSource } from './poller.js';
|
||||
import { dashboardSource } from './dashboard.js';
|
||||
import { chartsSource } from './charts.js';
|
||||
import { bootstrapSource } from './bootstrap.js';
|
||||
|
||||
export const moduleSources = [
|
||||
stateSource,
|
||||
torrentsSource,
|
||||
apiSource,
|
||||
createTorrentSource,
|
||||
torrentDetailsSource,
|
||||
modalsSource,
|
||||
rssSource,
|
||||
smartQueueSource,
|
||||
plannerSource,
|
||||
dashboardSource,
|
||||
pollerSource,
|
||||
chartsSource,
|
||||
bootstrapSource,
|
||||
];
|
||||
|
||||
export function buildRuntimeSource(){
|
||||
return `(() => {\n${moduleSources.join('\n')}\n})();\n`;
|
||||
}
|
||||
|
||||
export function startApp(){
|
||||
const runtimeSource = buildRuntimeSource();
|
||||
// Keep the original shared lexical scope while loading the source from smaller ES modules.
|
||||
// `io` is passed explicitly so Socket.IO remains available inside the generated runtime.
|
||||
return Function('io', runtimeSource)(window.io);
|
||||
}
|
||||
|
||||
if(typeof window !== 'undefined' && !window.PYTORRENT_DISABLE_AUTOSTART){
|
||||
startApp();
|
||||
}
|
||||
1
pytorrent/static/js/bootstrap.js
vendored
Normal file
1
pytorrent/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/charts.js
Normal file
1
pytorrent/static/js/charts.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/createTorrent.js
Normal file
1
pytorrent/static/js/createTorrent.js
Normal file
@@ -0,0 +1 @@
|
||||
export const createTorrentSource = " function isCreateTorrentTabActive(){\n return $('createTorrentPane')?.classList.contains('active');\n }\n function syncAddAndCreateActions(){\n const createActive = isCreateTorrentTabActive();\n $('addBtn')?.classList.toggle('d-none', !!createActive);\n $('createTorrentBtn')?.classList.toggle('d-none', !createActive);\n }\n function createTorrentPayload(){\n const fd = new FormData();\n fd.append('source_path', $('createSourcePath')?.value || '');\n fd.append('trackers', $('createTrackers')?.value || '');\n fd.append('comment', $('createComment')?.value || '');\n fd.append('source', $('createSourceName')?.value || '');\n fd.append('piece_size_kib', $('createPieceSize')?.value || '256');\n fd.append('private', $('createPrivate')?.checked ? '1' : '0');\n fd.append('share', $('createShare')?.checked ? '1' : '0');\n fd.append('label', $('createLabel')?.value || '');\n return fd;\n }\n function downloadCreatedTorrent(blob,name){\n const obj = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = obj;\n a.download = name;\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(()=>URL.revokeObjectURL(obj), 1000);\n }\n async function createTorrentFromModal(){\n const btn = $('createTorrentBtn');\n const info = $('createTorrentInfo');\n buttonBusy(btn, true);\n setBusy(true, 'Creating torrent...');\n if(info) info.textContent = 'Creating .torrent file...';\n try{\n const res = await fetch('/api/torrents/create', {method: 'POST', body: createTorrentPayload()});\n if(!res.ok){\n const j = await res.json().catch(()=>({}));\n throw new Error(j.error || `Create failed (${res.status})`);\n }\n const name = filenameFromResponse(res, 'created.torrent');\n const message = res.headers.get('X-PyTorrent-Create-Message') || 'Torrent created';\n const blob = await res.blob();\n downloadCreatedTorrent(blob, name);\n if(info) info.textContent = message;\n toast(message, 'success');\n }catch(e){\n if(info) info.textContent = e.message;\n toast(e.message, 'danger');\n }finally{\n setBusy(false);\n buttonBusy(btn, false);\n }\n }\n $('addModal')?.addEventListener('shown.bs.modal', syncAddAndCreateActions);\n document.querySelectorAll('#addModal [data-bs-toggle=\"pill\"]').forEach(tab => tab.addEventListener('shown.bs.tab', syncAddAndCreateActions));\n $('createTorrentBtn')?.addEventListener('click', createTorrentFromModal);\n";
|
||||
1
pytorrent/static/js/dashboard.js
Normal file
1
pytorrent/static/js/dashboard.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/modals.js
Normal file
1
pytorrent/static/js/modals.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/planner.js
Normal file
1
pytorrent/static/js/planner.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/poller.js
Normal file
1
pytorrent/static/js/poller.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/rss.js
Normal file
1
pytorrent/static/js/rss.js
Normal file
@@ -0,0 +1 @@
|
||||
export const rssSource = " async function loadRss(){ const j=await (await fetch('/api/rss')).json(); const feeds=j.feeds||[], rules=j.rules||[], history=j.history||[]; if($('rssManager')) $('rssManager').innerHTML=`<h6>Feeds</h6>${table(['Name','URL','Interval','Last check','Last error','Actions'],feeds.map(f=>[esc(f.name),esc(f.url),esc(f.interval_minutes||30)+' min',humanDateCell(f.last_checked_at),esc(f.last_error||''),`<button class=\"btn btn-xs btn-outline-primary rss-edit-feed\" data-feed='${esc(JSON.stringify(f))}'><i class=\"fa-solid fa-pen-to-square\"></i> Edit</button> <button class=\"btn btn-xs btn-outline-danger rss-delete-feed\" data-id=\"${esc(f.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button>`]))}<h6 class=\"mt-3\">Rules</h6>${table(['Name','Include','Exclude','Filters','Path','Label','Actions'],rules.map(r=>[esc(r.name),esc(r.pattern),esc(r.exclude_pattern||''),esc([r.min_size_mb?`min ${r.min_size_mb}MB`:'',r.max_size_mb?`max ${r.max_size_mb}MB`:'',r.category,r.quality,r.season?`S${r.season}`:'',r.episode?`E${r.episode}`:''].filter(Boolean).join(', ')),esc(r.save_path),esc(r.label),`<button class=\"btn btn-xs btn-outline-primary rss-edit-rule\" data-rule='${esc(JSON.stringify(r))}'><i class=\"fa-solid fa-pen-to-square\"></i> Edit</button> <button class=\"btn btn-xs btn-outline-danger rss-delete-rule\" data-id=\"${esc(r.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button>`]))}<h6 class=\"mt-3\">RSS log</h6>${table(['Time','Title','Status','Message'],history.map(h=>[humanDateCell(h.created_at),esc(h.title||h.link||''),esc(h.status),esc(h.message||'')]))}`; }\n \n\n function fillBackupSettings(settings={}){\n if($('backupAutoEnabled')) $('backupAutoEnabled').checked=!!settings.enabled;\n if($('backupAutoInterval')) $('backupAutoInterval').value=settings.interval_hours||24;\n if($('backupRetentionDays')) $('backupRetentionDays').value=settings.retention_days||30;\n }\n function backupPreviewDetails(table={}){\n const sample=table.sample||[];\n if(!sample.length) return '<div class=\"backup-preview-empty\">No saved rows in this table.</div>';\n const keys=[...new Set(sample.flatMap(row=>Object.keys(row||{})))].slice(0,8);\n return responsiveTable(keys.map(esc), sample.map(row=>keys.map(key=>esc(row?.[key] ?? ''))), 'backup-preview-sample-table');\n }\n function backupPreviewTable(preview={}){\n const tables=preview.tables||[];\n const rows=tables.map(t=>`<details class=\"backup-preview-table-details\"><summary><span><b>${esc(t.name)}</b><small>${esc(t.rows)} row(s) \u00b7 ${(t.columns||[]).length} column(s)</small></span></summary>${backupPreviewDetails(t)}</details>`).join('');\n return `<div class=\"surface-section backup-preview-card\"><div class=\"section-title\"><i class=\"fa-solid fa-eye\"></i> Backup preview</div><div class=\"small text-muted mb-2\">Created: ${esc(preview.created_at||'-')} \u00b7 ${preview.automatic?'automatic':'manual'} \u00b7 sensitive values hidden</div>${rows || '<div class=\"empty-mini\">Backup has no previewable settings.</div>'}</div>`;\n }\n async function loadBackup(){\n const j=await (await fetch('/api/backup')).json();\n const rows=j.backups||[];\n fillBackupSettings(j.auto||{});\n if($('backupManager')) $('backupManager').innerHTML=responsiveTable(['Name','Created','Type','Actions'],rows.map(b=>[esc(b.name),humanDateCell(b.created_at),b.automatic?'Auto':'Manual',`<div class=\"table-action-group backup-actions\"><button class=\"btn btn-xs btn-outline-info backup-preview-btn\" data-id=\"${esc(b.id)}\"><i class=\"fa-solid fa-eye\"></i> Preview</button><a class=\"btn btn-xs btn-outline-secondary\" href=\"/api/backup/${esc(b.id)}/download\"><i class=\"fa-solid fa-download\"></i> Download</a><button class=\"btn btn-xs btn-outline-warning backup-restore\" data-id=\"${esc(b.id)}\"><i class=\"fa-solid fa-rotate-left\"></i> Restore</button><button class=\"btn btn-xs btn-outline-danger backup-delete\" data-id=\"${esc(b.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button></div>`]),'backup-table');\n }\n\n";
|
||||
1
pytorrent/static/js/smartQueue.js
Normal file
1
pytorrent/static/js/smartQueue.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/smartQueue.js.orig
Normal file
1
pytorrent/static/js/smartQueue.js.orig
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/state.js
Normal file
1
pytorrent/static/js/state.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/torrentDetails.js
Normal file
1
pytorrent/static/js/torrentDetails.js
Normal file
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/torrents.js
Normal file
1
pytorrent/static/js/torrents.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user