Compare commits

..

6 Commits

Author SHA1 Message Date
4ec95d8ba9 chore: bump version 2026-07-31 09:53:41 +02:00
d52b9a55f4 fix: exclude downloaded episodes from episodeDict and GetMissingEpisode
Prevent fully-downloaded series from being queued by auto-download:

- SerieList.GetMissingEpisode(): filter by is_downloaded instead of
  checking if episodeDict is non-empty. episodeDict from the DB
  relationship includes all episodes (including downloaded), so a
  series with only downloaded episodes still had a truthy episodeDict.

- AnimeSeries.episodeDict property: skip episodes where is_downloaded=True
  when building the dict from the DB relationship. This makes the
  property consistent with the is_downloaded filtering already done
  manually in list_series_with_filters(), and ensures that calling
  code anywhere in the codebase gets the correct missing-episode view.

Fixes hana-kimi (and any other fully-downloaded series) incorrectly
appearing in the auto-download queue after a rescan.
2026-07-31 09:50:59 +02:00
12681720e9 fix: process image loading batch sequentially to avoid concurrent AsyncSession use
SQLAlchemy async sessions are not safe for concurrent operations. load_series_images_batch was using asyncio.gather to run multiple load_series_images calls concurrently, all sharing the same db session. This caused 'session is provisioning a new connection; concurrent operations are not permitted' errors for every series in the batch.

Fix by processing each batch sequentially instead of concurrently. BATCH_SIZE=10 still paces TMDB requests as intended.
2026-07-31 09:09:04 +02:00
084488a88c chore: bump version 2026-07-31 08:45:27 +02:00
270da18543 fix: emit download_progress events during direct stream downloads
When _try_direct_stream() succeeded, it streamed chunks directly via
requests.get() without firing any progress events. This caused the WebSocket
frontend to only see 'started' (0%) and 'completed' (100%) — no incremental
updates.

Add per-chunk progress events to _try_direct_stream that mirror yt-dlp's
hook format (downloaded_bytes, total_bytes, speed, eta, status), emitted at
~1% intervals. These flow through the existing download_progress handler chain
(SeriesApp → anime_service → progress_service → WebSocket → frontend).
2026-07-31 08:42:59 +02:00
163532b1ef fix: use data.key instead of data.data in series_updated handler
The websocket-client strips the outer {type, data, ...} wrapper before
emitting to handlers, so handlers receive the inner series data object
directly (e.g. {key, name, missing_episodes}) — not {type, data: {...}}.

The series_updated handler was checking data.data which always failed,
causing every update to fall back to a full loadSeries() call instead of
calling updateSingleSeries() directly. This prevented the missing
episodes count from updating in real-time after a download completed.

Fix: check data.key directly and pass data (not data.data) to
updateSingleSeries().
2026-07-31 08:37:44 +02:00
8 changed files with 56 additions and 14 deletions

View File

@@ -1 +1 @@
v1.5.2
v1.5.4

View File

@@ -1,6 +1,6 @@
{
"name": "aniworld-web",
"version": "1.5.2",
"version": "1.5.4",
"description": "Aniworld Anime Download Manager - Web Frontend",
"type": "module",
"scripts": {

View File

@@ -134,7 +134,7 @@ class SerieList:
"""Return all series that still contain missing episodes."""
return [
anime for anime in self.keyDict.values()
if anime.episodeDict
if any(not ep.is_downloaded for ep in (anime.episodes or []))
]
def get_missing_episodes(self) -> List[AnimeSeries]:

View File

@@ -205,6 +205,8 @@ class AnimeSeries(Base, TimestampMixin):
try:
if self.episodes:
for ep in self.episodes:
if ep.is_downloaded:
continue
season = ep.season or 1
if season not in episode_dict:
episode_dict[season] = []

View File

@@ -5,6 +5,7 @@ import logging
import os
import re
import shutil
import time
import threading
from pathlib import Path
from urllib.parse import quote
@@ -383,6 +384,12 @@ class AniworldLoader(Loader):
"Direct stream download starting (type=%s)",
content_type
)
total_size = int(response.headers.get(
"Content-Length", 0
))
received = 0
last_emit = 0
start_time = time.monotonic()
with open(output_path, "wb") as fh:
for chunk in response.iter_content(chunk_size=1024 * 1024):
if self._cancel_flag.is_set():
@@ -391,7 +398,33 @@ class AniworldLoader(Loader):
)
return False
if chunk:
received += len(chunk)
fh.write(chunk)
# Emit progress events at ~1% intervals
if total_size > 0:
pct = (received / total_size) * 100
if pct - last_emit >= 1.0 or received == total_size:
elapsed = time.monotonic() - start_time
speed_bps = (
received / elapsed
if elapsed > 0 else 0
)
eta = (
int((total_size - received) / speed_bps)
if speed_bps > 0 else None
)
self.events.download_progress({
"downloaded_bytes": received,
"total_bytes": total_size,
"speed": speed_bps,
"eta": eta,
"status": (
"finished"
if received >= total_size
else "downloading"
),
})
last_emit = pct
return True
except requests.RequestException as exc:
logger.warning("Direct stream download failed: %s", exc)

View File

@@ -162,17 +162,18 @@ class ImageLoadingService:
for i in range(0, len(series_list), self.BATCH_SIZE):
batch = series_list[i : i + self.BATCH_SIZE]
tasks = [
self.load_series_images(
# Process each series sequentially to avoid concurrent use of the
# same AsyncSession (SQLAlchemy async sessions are not thread-safe
# for concurrent operations). BATCH_SIZE still paces TMDB requests.
results: List[Dict[str, Any] | Exception] = []
for series in batch:
result = await self.load_series_images(
key=series["key"],
folder=series["folder"],
anime_directory=anime_directory,
db=db,
)
for series in batch
]
results = await asyncio.gather(*tasks, return_exceptions=True)
results.append(result)
for series, result in zip(batch, results):
if isinstance(result, Exception):

View File

@@ -136,13 +136,16 @@ AniWorld.IndexSocketHandler = (function() {
// Series events
socket.on(WS_EVENTS.SERIES_UPDATED, function(data) {
console.log('Series updated:', data);
// Use the data directly to update the series instead of full refresh
if (data && data.data && AniWorld.SeriesManager && AniWorld.SeriesManager.updateSingleSeries) {
AniWorld.SeriesManager.updateSingleSeries(data.data);
// NOTE: websocket-client.js strips the outer {type, data, ...} wrapper
// before emitting, so `data` here is the inner series data object
// (e.g. {key, name, missing_episodes, ...}) — NOT {type, data, ...}.
// AniWorld.SeriesManager.updateSingleSeries() expects this flat object.
if (data && data.key && AniWorld.SeriesManager && AniWorld.SeriesManager.updateSingleSeries) {
AniWorld.SeriesManager.updateSingleSeries(data);
} else {
// Fallback to full reload if data is incomplete
console.warn('Incomplete series update data, falling back to full reload');
console.warn('Incomplete series update data, falling back to full reload', data);
if (AniWorld.SeriesManager && AniWorld.SeriesManager.loadSeries) {
AniWorld.SeriesManager.loadSeries();
}

3
uv.lock generated Normal file
View File

@@ -0,0 +1,3 @@
version = 1
revision = 3
requires-python = ">=3.12"