eaf-base-api 2.4.7__tar.gz → 2.4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eaf_base_api-2.4.7/eaf_base_api.egg-info → eaf_base_api-2.4.8}/PKG-INFO +1 -1
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/base.py +145 -109
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8/eaf_base_api.egg-info}/PKG-INFO +1 -1
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/pyproject.toml +1 -1
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/LICENSE +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/MANIFEST.in +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/README.md +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/__init__.py +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/modules/config.py +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/modules/errors.py +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/eaf_base_api.egg-info/SOURCES.txt +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/eaf_base_api.egg-info/requires.txt +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.4.7 → eaf_base_api-2.4.8}/setup.cfg +0 -0
|
@@ -369,7 +369,7 @@ class Helper:
|
|
|
369
369
|
):
|
|
370
370
|
"""
|
|
371
371
|
Yields Video/ErrorVideo in deterministic (page_idx, vid_idx) order while fetching concurrently.
|
|
372
|
-
|
|
372
|
+
Stops scraping pages once a 404 page is encountered (self.core.fetch returns an httpx.Response).
|
|
373
373
|
"""
|
|
374
374
|
logger = self.logger
|
|
375
375
|
run_id = uuid.uuid4().hex[:8] # correlate all logs for this iterator call
|
|
@@ -395,6 +395,10 @@ class Helper:
|
|
|
395
395
|
next_page_idx = 0 # ordering cursor
|
|
396
396
|
next_video_idx = 0
|
|
397
397
|
|
|
398
|
+
# NEW: stop paging after first 404
|
|
399
|
+
stop_after_404 = False
|
|
400
|
+
stop_at_page_idx: Optional[int] = None
|
|
401
|
+
|
|
398
402
|
def flush_ready():
|
|
399
403
|
nonlocal next_page_idx, next_video_idx
|
|
400
404
|
flushed = 0
|
|
@@ -444,17 +448,15 @@ class Helper:
|
|
|
444
448
|
# queue of discovered videos we haven't submitted yet
|
|
445
449
|
pending_videos: deque[Tuple[int, int, str]] = deque()
|
|
446
450
|
|
|
447
|
-
# helper to respect videos_concurrency strictly
|
|
451
|
+
# helper to respect videos_concurrency strictly
|
|
448
452
|
def schedule_videos():
|
|
449
453
|
scheduled = 0
|
|
450
454
|
while pending_videos and len(video_in_flight) < videos_concurrency:
|
|
451
455
|
pidx, vid_idx, vurl = pending_videos.popleft()
|
|
452
456
|
if other_return:
|
|
453
457
|
vf = video_executor.submit(self._other_return, vurl)
|
|
454
|
-
|
|
455
458
|
else:
|
|
456
459
|
vf = video_executor.submit(self._make_video_safe, vurl)
|
|
457
|
-
|
|
458
460
|
video_in_flight[vf] = (pidx, vid_idx)
|
|
459
461
|
scheduled += 1
|
|
460
462
|
logger.debug(
|
|
@@ -491,7 +493,6 @@ class Helper:
|
|
|
491
493
|
|
|
492
494
|
# If nothing in flight but items queued (shouldn't happen often), try to schedule then continue
|
|
493
495
|
if not waiting_on:
|
|
494
|
-
# No futures to wait on; try to schedule (covers corner cases)
|
|
495
496
|
schedule_videos()
|
|
496
497
|
if not (page_in_flight or video_in_flight):
|
|
497
498
|
break
|
|
@@ -504,7 +505,7 @@ class Helper:
|
|
|
504
505
|
if fut in page_in_flight:
|
|
505
506
|
pidx, url = page_in_flight.pop(fut)
|
|
506
507
|
try:
|
|
507
|
-
|
|
508
|
+
html_or_resp = fut.result()
|
|
508
509
|
logger.info(
|
|
509
510
|
"[%s] PAGE done pidx=%d url=%s (inflight_page=%d inflight_video=%d)",
|
|
510
511
|
run_id, pidx, url, len(page_in_flight), len(video_in_flight)
|
|
@@ -518,20 +519,56 @@ class Helper:
|
|
|
518
519
|
page_counts[pidx] = 0
|
|
519
520
|
# now-ordered items may be ready
|
|
520
521
|
yield from flush_ready()
|
|
521
|
-
# schedule next page if available
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
522
|
+
# schedule next page if available (unless we've already stopped)
|
|
523
|
+
if not stop_after_404:
|
|
524
|
+
try:
|
|
525
|
+
npidx, nurl = next(page_iter)
|
|
526
|
+
nfut = page_executor.submit(self.core.fetch, nurl)
|
|
527
|
+
page_in_flight[nfut] = (npidx, nurl)
|
|
528
|
+
logger.debug(
|
|
529
|
+
"[%s] scheduled NEXT PAGE pidx=%d url=%s after failure",
|
|
530
|
+
run_id, npidx, nurl
|
|
531
|
+
)
|
|
532
|
+
except StopIteration:
|
|
533
|
+
pass
|
|
532
534
|
continue
|
|
533
535
|
|
|
534
|
-
#
|
|
536
|
+
# Detect a 404 "page" (httpx.Response returned)
|
|
537
|
+
is_404 = False
|
|
538
|
+
try:
|
|
539
|
+
if isinstance(html_or_resp, httpx.Response) and html_or_resp.status_code == 404:
|
|
540
|
+
is_404 = True
|
|
541
|
+
except Exception:
|
|
542
|
+
# Fallback duck-typing if httpx import fails for some reason
|
|
543
|
+
if getattr(html_or_resp, "status_code", None) == 404 and hasattr(html_or_resp, "headers"):
|
|
544
|
+
is_404 = True
|
|
545
|
+
|
|
546
|
+
if is_404:
|
|
547
|
+
logger.warning(
|
|
548
|
+
"[%s] PAGE 404 pidx=%d url=%s -> stopping further page scheduling",
|
|
549
|
+
run_id, pidx, url
|
|
550
|
+
)
|
|
551
|
+
stop_after_404 = True
|
|
552
|
+
stop_at_page_idx = pidx
|
|
553
|
+
# 404 page has 0 videos
|
|
554
|
+
page_counts[pidx] = 0
|
|
555
|
+
# Flush anything now ready
|
|
556
|
+
yield from flush_ready()
|
|
557
|
+
# Do NOT schedule any more pages
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
# If we've already hit a 404 and this page index is after it, ignore this page
|
|
561
|
+
if stop_after_404 and stop_at_page_idx is not None and pidx > stop_at_page_idx:
|
|
562
|
+
logger.info(
|
|
563
|
+
"[%s] ignoring PAGE pidx=%d url=%s (after first 404 at pidx=%d)",
|
|
564
|
+
run_id, pidx, url, stop_at_page_idx
|
|
565
|
+
)
|
|
566
|
+
page_counts[pidx] = 0
|
|
567
|
+
yield from flush_ready()
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
# Normal path: Extract video URLs
|
|
571
|
+
html = html_or_resp
|
|
535
572
|
try:
|
|
536
573
|
video_urls = extractor(html) or []
|
|
537
574
|
logger.debug(
|
|
@@ -551,26 +588,24 @@ class Helper:
|
|
|
551
588
|
run_id, pidx, page_counts[pidx]
|
|
552
589
|
)
|
|
553
590
|
|
|
554
|
-
# NEW: queue videos instead of submitting all at once
|
|
555
591
|
for vid_idx, vurl in enumerate(video_urls):
|
|
556
592
|
pending_videos.append((pidx, vid_idx, vurl))
|
|
557
|
-
# Fill available slots respecting videos_concurrency
|
|
558
593
|
schedule_videos()
|
|
559
594
|
|
|
560
|
-
# Try flushing any now-ready items
|
|
561
595
|
yield from flush_ready()
|
|
562
596
|
|
|
563
|
-
# Fetch next page
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
597
|
+
# Fetch next page only if we haven't seen a 404
|
|
598
|
+
if not stop_after_404:
|
|
599
|
+
try:
|
|
600
|
+
npidx, nurl = next(page_iter)
|
|
601
|
+
nfut = page_executor.submit(self.core.fetch, nurl)
|
|
602
|
+
page_in_flight[nfut] = (npidx, nurl)
|
|
603
|
+
logger.debug(
|
|
604
|
+
"[%s] scheduled NEXT PAGE pidx=%d url=%s (inflight_page=%d)",
|
|
605
|
+
run_id, npidx, nurl, len(page_in_flight)
|
|
606
|
+
)
|
|
607
|
+
except StopIteration:
|
|
608
|
+
logger.debug("[%s] no more pages to schedule", run_id)
|
|
574
609
|
|
|
575
610
|
# VIDEO completed
|
|
576
611
|
elif fut in video_in_flight:
|
|
@@ -858,11 +893,7 @@ class BaseCore:
|
|
|
858
893
|
|
|
859
894
|
# 404: usually not recoverable, but we try once (if attempt==0) in case of transient CDN
|
|
860
895
|
if status == 404:
|
|
861
|
-
|
|
862
|
-
self.logger.info(f"Got 404 once for {url}, retrying in case of transient edge/CDN issue.")
|
|
863
|
-
continue
|
|
864
|
-
self.logger.error(f"Resource not found (404) for URL: {url}")
|
|
865
|
-
return response
|
|
896
|
+
return response # Immediately returning response, because 404 usually means no more content when searching
|
|
866
897
|
|
|
867
898
|
# 410: permanent gone
|
|
868
899
|
if status == 410:
|
|
@@ -1279,85 +1310,90 @@ class BaseCore:
|
|
|
1279
1310
|
raise ModuleNotFoundError(f"PyAV is required for remuxing. Install with pip install av. Not supported on Termux! {e}")
|
|
1280
1311
|
|
|
1281
1312
|
input_ = av_open(input_path)
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1313
|
+
if input_.format.name.lower() == "mpegts":
|
|
1314
|
+
output = av_open(output_path, mode="w", format="mp4", options={"movflags": "faststart"})
|
|
1315
|
+
|
|
1316
|
+
# --- VIDEO: keep your exact remux approach ---
|
|
1317
|
+
in_video = input_.streams.video[0]
|
|
1318
|
+
out_video = output.add_stream_from_template(template=in_video)
|
|
1319
|
+
|
|
1320
|
+
# --- AUDIO: copy if MP4-compatible; else transcode to AAC ---
|
|
1321
|
+
in_audio = next((s for s in input_.streams if s.type == "audio"), None)
|
|
1322
|
+
out_audio = None
|
|
1323
|
+
transcode_audio = False
|
|
1324
|
+
resampler = None
|
|
1325
|
+
|
|
1326
|
+
if in_audio:
|
|
1327
|
+
# Common MP4-safe audio codecs to copy without transcoding.
|
|
1328
|
+
copy_ok = {"aac", "alac", "mp3"}
|
|
1329
|
+
codec_name = (in_audio.codec_context.name or "").lower()
|
|
1330
|
+
|
|
1331
|
+
if codec_name in copy_ok:
|
|
1332
|
+
out_audio = output.add_stream_from_template(template=in_audio)
|
|
1333
|
+
else:
|
|
1334
|
+
# Build an AAC encoder stream. Match input rate/layout where possible.
|
|
1335
|
+
transcode_audio = True
|
|
1336
|
+
sample_rate = in_audio.codec_context.sample_rate or 48000
|
|
1337
|
+
layout = (
|
|
1338
|
+
in_audio.codec_context.layout.name
|
|
1339
|
+
if getattr(in_audio.codec_context, "layout", None)
|
|
1340
|
+
else "stereo"
|
|
1341
|
+
)
|
|
1342
|
+
out_audio = output.add_stream("aac", rate=sample_rate)
|
|
1343
|
+
# Setting layout/channels helps encoder pick sensible defaults.
|
|
1344
|
+
try:
|
|
1345
|
+
out_audio.layout = layout
|
|
1346
|
+
except Exception:
|
|
1347
|
+
pass
|
|
1348
|
+
|
|
1349
|
+
# Ensure frames handed to the AAC encoder are in a compatible sample format.
|
|
1350
|
+
# ('fltp' is the usual format for AAC encoders.)
|
|
1351
|
+
resampler = AudioResampler(format="fltp", layout=layout, rate=sample_rate)
|
|
1352
|
+
|
|
1353
|
+
# --- DEMUX both streams so audio is included ---
|
|
1354
|
+
demux_streams = [in_video] + ([in_audio] if in_audio else [])
|
|
1355
|
+
packets = list(input_.demux(demux_streams)) # keeps your progress logic
|
|
1356
|
+
total = len(packets)
|
|
1357
|
+
|
|
1358
|
+
for idx, packet in enumerate(packets):
|
|
1359
|
+
if packet.dts is None:
|
|
1360
|
+
if callback:
|
|
1361
|
+
callback(idx + 1, total)
|
|
1362
|
+
continue
|
|
1316
1363
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1364
|
+
if packet.stream == in_video:
|
|
1365
|
+
# Your original video remux path (no explicit rescale).
|
|
1366
|
+
packet.stream = out_video
|
|
1367
|
+
output.mux(packet)
|
|
1320
1368
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1369
|
+
elif in_audio and packet.stream == in_audio:
|
|
1370
|
+
if not transcode_audio:
|
|
1371
|
+
# Audio is already MP4-compatible; just remux.
|
|
1372
|
+
packet.stream = out_audio
|
|
1373
|
+
output.mux(packet)
|
|
1374
|
+
else:
|
|
1375
|
+
# Decode -> (optionally resample) -> encode AAC -> mux.
|
|
1376
|
+
for frame in packet.decode():
|
|
1377
|
+
# Resample to match encoder expectations.
|
|
1378
|
+
frames = resampler.resample(frame) if resampler else [frame]
|
|
1379
|
+
for f in frames:
|
|
1380
|
+
for enc_pkt in out_audio.encode(f):
|
|
1381
|
+
output.mux(enc_pkt)
|
|
1325
1382
|
|
|
1326
|
-
for idx, packet in enumerate(packets):
|
|
1327
|
-
if packet.dts is None:
|
|
1328
1383
|
if callback:
|
|
1329
1384
|
callback(idx + 1, total)
|
|
1330
|
-
continue
|
|
1331
1385
|
|
|
1332
|
-
if
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1386
|
+
# Flush audio encoder if we transcoded.
|
|
1387
|
+
if transcode_audio and out_audio:
|
|
1388
|
+
for enc_pkt in out_audio.encode(None):
|
|
1389
|
+
output.mux(enc_pkt)
|
|
1336
1390
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
# Decode -> (optionally resample) -> encode AAC -> mux.
|
|
1344
|
-
for frame in packet.decode():
|
|
1345
|
-
# Resample to match encoder expectations.
|
|
1346
|
-
frames = resampler.resample(frame) if resampler else [frame]
|
|
1347
|
-
for f in frames:
|
|
1348
|
-
for enc_pkt in out_audio.encode(f):
|
|
1349
|
-
output.mux(enc_pkt)
|
|
1350
|
-
|
|
1351
|
-
if callback:
|
|
1352
|
-
callback(idx + 1, total)
|
|
1353
|
-
|
|
1354
|
-
# Flush audio encoder if we transcoded.
|
|
1355
|
-
if transcode_audio and out_audio:
|
|
1356
|
-
for enc_pkt in out_audio.encode(None):
|
|
1357
|
-
output.mux(enc_pkt)
|
|
1358
|
-
|
|
1359
|
-
input_.close()
|
|
1360
|
-
output.close()
|
|
1391
|
+
input_.close()
|
|
1392
|
+
output.close()
|
|
1393
|
+
|
|
1394
|
+
else:
|
|
1395
|
+
self.logger.info("Stream seems to be already in MP4! Skipping remux...")
|
|
1396
|
+
os.rename(input_path, output_path)
|
|
1361
1397
|
|
|
1362
1398
|
def FFMPEG(self, video, quality: str, callback, path: str) -> bool:
|
|
1363
1399
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|