mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Dynamic app version from git tag + file size display in queue
Version: - New app/__version__.py with 'dev' fallback for local dev - Dockerfile injects APP_VERSION build arg from CI tag - Health endpoint and footer now show actual release version - Test updated to accept 'dev' in non-Docker environments File size: - Capture filesize/filesize_approx from yt-dlp extract_info - Write to DB via update_job_progress and broadcast via SSE - New 'Size' column in download table (hidden on mobile) - formatSize helper: bytes → human-readable (KB/MB/GB) - Frontend store picks up filesize from SSE events
This commit is contained in:
parent
6f20d29151
commit
04f7fd09f3
8 changed files with 65 additions and 13 deletions
|
|
@ -45,6 +45,10 @@ RUN useradd --create-home --shell /bin/bash mediarip
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
|
|
||||||
|
# Inject version from build arg (set by CI from git tag)
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
RUN echo "__version__ = \"${APP_VERSION}\"" > app/__version__.py
|
||||||
|
|
||||||
# Copy built frontend into backend static dir
|
# Copy built frontend into backend static dir
|
||||||
COPY --from=frontend-builder /build/frontend/dist ./static
|
COPY --from=frontend-builder /build/frontend/dist ./static
|
||||||
|
|
||||||
|
|
|
||||||
2
backend/app/__version__.py
Normal file
2
backend/app/__version__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Auto-generated at build time. Fallback for local dev.
|
||||||
|
__version__ = "dev"
|
||||||
|
|
@ -289,16 +289,27 @@ async def update_job_progress(
|
||||||
speed: str | None = None,
|
speed: str | None = None,
|
||||||
eta: str | None = None,
|
eta: str | None = None,
|
||||||
filename: str | None = None,
|
filename: str | None = None,
|
||||||
|
filesize: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update live progress fields for a running download."""
|
"""Update live progress fields for a running download."""
|
||||||
await db.execute(
|
if filesize is not None:
|
||||||
"""
|
await db.execute(
|
||||||
UPDATE jobs
|
"""
|
||||||
SET progress_percent = ?, speed = ?, eta = ?, filename = ?
|
UPDATE jobs
|
||||||
WHERE id = ?
|
SET progress_percent = ?, speed = ?, eta = ?, filename = ?, filesize = ?
|
||||||
""",
|
WHERE id = ?
|
||||||
(progress_percent, speed, eta, filename, job_id),
|
""",
|
||||||
)
|
(progress_percent, speed, eta, filename, filesize, job_id),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE jobs
|
||||||
|
SET progress_percent = ?, speed = ?, eta = ?, filename = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(progress_percent, speed, eta, filename, job_id),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ try:
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
_yt_dlp_version = "unknown"
|
_yt_dlp_version = "unknown"
|
||||||
|
|
||||||
_APP_VERSION = "0.1.0"
|
try:
|
||||||
|
from app.__version__ import __version__ as _APP_VERSION
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
_APP_VERSION = "dev"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
@router.get("/health")
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,23 @@ class DownloadService:
|
||||||
relative_fn = str(abs_path.relative_to(out_dir))
|
relative_fn = str(abs_path.relative_to(out_dir))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
relative_fn = abs_path.name
|
relative_fn = abs_path.name
|
||||||
|
|
||||||
|
# Capture filesize from metadata
|
||||||
|
file_size = info.get("filesize") or info.get("filesize_approx")
|
||||||
|
if file_size:
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
update_job_progress(
|
||||||
|
self._db, job_id, 0, None, None, relative_fn,
|
||||||
|
filesize=int(file_size),
|
||||||
|
),
|
||||||
|
self._loop,
|
||||||
|
).result(timeout=10)
|
||||||
|
self._broker.publish(session_id, {
|
||||||
|
"event": "job_update",
|
||||||
|
"data": {"job_id": job_id, "status": "downloading",
|
||||||
|
"percent": 0, "filename": relative_fn,
|
||||||
|
"filesize": int(file_size)},
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
relative_fn = None
|
relative_fn = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,11 @@ class TestHealthEndpoint:
|
||||||
assert isinstance(data["queue_depth"], int) and data["queue_depth"] >= 0
|
assert isinstance(data["queue_depth"], int) and data["queue_depth"] >= 0
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_health_version_is_semver(self, client):
|
async def test_health_version_format(self, client):
|
||||||
resp = await client.get("/api/health")
|
resp = await client.get("/api/health")
|
||||||
version = resp.json()["version"]
|
version = resp.json()["version"]
|
||||||
parts = version.split(".")
|
# In Docker: semver (e.g. "1.1.4"). Locally: "dev".
|
||||||
assert len(parts) == 3, f"Expected semver, got {version}"
|
assert version == "dev" or len(version.split(".")) == 3, f"Unexpected version: {version}"
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_health_queue_depth_reflects_active_jobs(self, db):
|
async def test_health_queue_depth_reflects_active_jobs(self, db):
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,13 @@ function isCompleted(job: Job): boolean {
|
||||||
return job.status === 'completed'
|
return job.status === 'completed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
||||||
|
}
|
||||||
|
|
||||||
/** Infer whether the job is audio or video from quality/filename. */
|
/** Infer whether the job is audio or video from quality/filename. */
|
||||||
function isAudioJob(job: Job): boolean {
|
function isAudioJob(job: Job): boolean {
|
||||||
if (job.quality === 'bestaudio') return true
|
if (job.quality === 'bestaudio') return true
|
||||||
|
|
@ -184,6 +191,9 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
<th class="col-speed sortable hide-mobile" @click="toggleSort('speed')">
|
<th class="col-speed sortable hide-mobile" @click="toggleSort('speed')">
|
||||||
Speed{{ sortIndicator('speed') }}
|
Speed{{ sortIndicator('speed') }}
|
||||||
</th>
|
</th>
|
||||||
|
<th class="col-size hide-mobile">
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
<th class="col-eta sortable hide-mobile" @click="toggleSort('eta')">
|
<th class="col-eta sortable hide-mobile" @click="toggleSort('eta')">
|
||||||
ETA{{ sortIndicator('eta') }}
|
ETA{{ sortIndicator('eta') }}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -227,6 +237,10 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
<span v-if="job.speed" class="mono">{{ job.speed }}</span>
|
<span v-if="job.speed" class="mono">{{ job.speed }}</span>
|
||||||
<span v-else class="text-muted">—</span>
|
<span v-else class="text-muted">—</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="col-size hide-mobile">
|
||||||
|
<span v-if="job.filesize" class="mono">{{ formatSize(job.filesize) }}</span>
|
||||||
|
<span v-else class="text-muted">—</span>
|
||||||
|
</td>
|
||||||
<td class="col-eta hide-mobile">
|
<td class="col-eta hide-mobile">
|
||||||
<span v-if="job.eta" class="mono">{{ job.eta }}</span>
|
<span v-if="job.eta" class="mono">{{ job.eta }}</span>
|
||||||
<span v-else class="text-muted">—</span>
|
<span v-else class="text-muted">—</span>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ export const useDownloadsStore = defineStore('downloads', () => {
|
||||||
if (event.speed !== null) existing.speed = event.speed
|
if (event.speed !== null) existing.speed = event.speed
|
||||||
if (event.eta !== null) existing.eta = event.eta
|
if (event.eta !== null) existing.eta = event.eta
|
||||||
if (event.filename !== null) existing.filename = event.filename
|
if (event.filename !== null) existing.filename = event.filename
|
||||||
|
if (event.filesize) existing.filesize = event.filesize
|
||||||
if (event.error_message) existing.error_message = event.error_message
|
if (event.error_message) existing.error_message = event.error_message
|
||||||
// Trigger reactivity by re-setting the map entry
|
// Trigger reactivity by re-setting the map entry
|
||||||
jobs.value.set(event.job_id, { ...existing })
|
jobs.value.set(event.job_id, { ...existing })
|
||||||
|
|
@ -111,7 +112,7 @@ export const useDownloadsStore = defineStore('downloads', () => {
|
||||||
quality: null,
|
quality: null,
|
||||||
output_template: null,
|
output_template: null,
|
||||||
filename: event.filename ?? null,
|
filename: event.filename ?? null,
|
||||||
filesize: null,
|
filesize: event.filesize ?? null,
|
||||||
progress_percent: event.percent,
|
progress_percent: event.percent,
|
||||||
speed: event.speed ?? null,
|
speed: event.speed ?? null,
|
||||||
eta: event.eta ?? null,
|
eta: event.eta ?? null,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue