Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10430037fe | |||
| 01657d4e1b | |||
| 0ad1fd4cd5 | |||
| 7ed8ca2848 | |||
| 5f59283bf0 |
+19
-13
@@ -70,8 +70,9 @@ function jobOf(r: Row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pressedMark(r: Row): string {
|
function pressedMark(r: Row): string {
|
||||||
if (!r.createdAt) return "Lyra";
|
const when = r.job?.updatedAt ?? r.createdAt; // when it was pressed, not when it was requested
|
||||||
const d = new Date(r.createdAt);
|
if (!when) return "Lyra";
|
||||||
|
const d = new Date(when);
|
||||||
return `Lyra · ${d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).toUpperCase()}`;
|
return `Lyra · ${d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).toUpperCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,16 +205,21 @@ export function Queue() {
|
|||||||
const activeTab: Tab = tab ?? defaultTab;
|
const activeTab: Tab = tab ?? defaultTab;
|
||||||
const shown = buckets[activeTab];
|
const shown = buckets[activeTab];
|
||||||
|
|
||||||
// Dedupe by artist+album — re-requesting/re-downloading the same album leaves several
|
// "Recently Pressed": imported albums ordered by when they were PRESSED (job.updatedAt ≈ import
|
||||||
// completed Requests; show each album once (rows arrive newest-first, so keep the first).
|
// time), newest first — an album requested long ago but imported just now is recently pressed
|
||||||
|
// even though its Request is old (rows arrive by request createdAt, which buried fresh imports).
|
||||||
|
// Dedupe by artist+album (re-downloads leave several completed Requests; keep the most recent).
|
||||||
const seenPressed = new Set<string>();
|
const seenPressed = new Set<string>();
|
||||||
const pressed = rows.filter((r) => {
|
const pressedAll = rows
|
||||||
if (jobOf(r).state !== "imported") return false;
|
.filter((r) => jobOf(r).state === "imported")
|
||||||
const key = `${r.artist} ${r.album}`.toLowerCase();
|
.sort((a, b) => new Date(jobOf(b).updatedAt ?? 0).getTime() - new Date(jobOf(a).updatedAt ?? 0).getTime())
|
||||||
if (seenPressed.has(key)) return false;
|
.filter((r) => {
|
||||||
seenPressed.add(key);
|
const key = `${r.artist} ${r.album}`.toLowerCase();
|
||||||
return true;
|
if (seenPressed.has(key)) return false;
|
||||||
});
|
seenPressed.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const pressed = pressedAll.slice(0, 10); // show only the 10 most recently pressed
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
function renderRow(r: Row) {
|
function renderRow(r: Row) {
|
||||||
@@ -310,7 +316,7 @@ export function Queue() {
|
|||||||
<StatTiles
|
<StatTiles
|
||||||
items={[
|
items={[
|
||||||
{ k: "On the press", v: active.length },
|
{ k: "On the press", v: active.length },
|
||||||
{ k: "Pressed", v: pressed.length, accent: true },
|
{ k: "Pressed", v: pressedAll.length, accent: true },
|
||||||
{ k: "Wanted", v: wanted },
|
{ k: "Wanted", v: wanted },
|
||||||
{ k: "Watching", v: watching, unit: "artists" },
|
{ k: "Watching", v: watching, unit: "artists" },
|
||||||
]}
|
]}
|
||||||
@@ -383,7 +389,7 @@ export function Queue() {
|
|||||||
|
|
||||||
{pressed.length > 0 ? (
|
{pressed.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<SectionHeader title="Recently Pressed" note={`${pressed.length} in library`} />
|
<SectionHeader title="Recently Pressed" note={`last ${pressed.length} · ${pressedAll.length} in library`} />
|
||||||
<PressedList
|
<PressedList
|
||||||
items={pressed.map((r) => ({ id: r.id, artist: r.artist, album: r.album, matrix: pressedMark(r), rgMbid: r.rgMbid ?? null }))}
|
items={pressed.map((r) => ({ id: r.id, artist: r.artist, album: r.album, matrix: pressedMark(r), rgMbid: r.rgMbid ?? null }))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ def _is_album(g: dict) -> bool:
|
|||||||
return (g.get("primary-type") or "").casefold() == "album"
|
return (g.get("primary-type") or "").casefold() == "album"
|
||||||
|
|
||||||
|
|
||||||
|
def _rg_year(g: dict) -> int:
|
||||||
|
"""First-release year of a release-group, or 9999 when unknown (so undated groups lose ties)."""
|
||||||
|
frd = (g.get("first-release-date") or "")[:4]
|
||||||
|
return int(frd) if frd.isdigit() else 9999
|
||||||
|
|
||||||
|
|
||||||
def _credited_artist(g: dict) -> str:
|
def _credited_artist(g: dict) -> str:
|
||||||
"""Primary credited artist name of a release-group search result, or '' if absent."""
|
"""Primary credited artist name of a release-group search result, or '' if absent."""
|
||||||
credit = g.get("artist-credit")
|
credit = g.get("artist-credit")
|
||||||
@@ -56,7 +62,12 @@ def _best_release_group(artist: str, album: str, groups: list) -> dict | None:
|
|||||||
* title similarity is next;
|
* title similarity is next;
|
||||||
* an Album primary-type only breaks ties *within* the same artist+title, so a famous
|
* an Album primary-type only breaks ties *within* the same artist+title, so a famous
|
||||||
album (Michael Jackson's "Off the Wall") still isn't resolved to its same-named
|
album (Michael Jackson's "Off the Wall") still isn't resolved to its same-named
|
||||||
single whose short tracklist would map positionally onto the album's files.
|
single whose short tracklist would map positionally onto the album's files;
|
||||||
|
* finally, the EARLIEST release-group wins — MusicBrainz lists the original album and its
|
||||||
|
later reissues/anniversary editions as separate same-titled groups (Linkin Park "Hybrid
|
||||||
|
Theory": a 2000 original and a 2023 13-track reissue), which tie on everything above, so
|
||||||
|
MB's arbitrary relevance order used to pick a reissue whose longer tracklist/runtime then
|
||||||
|
failed completeness checks against the standard album a source delivers. Prefer the original.
|
||||||
Artist is a ranking signal, never a hard filter — a low match sinks a candidate but
|
Artist is a ranking signal, never a hard filter — a low match sinks a candidate but
|
||||||
never drops the release, so credited-name variations (feat., punctuation) still resolve.
|
never drops the release, so credited-name variations (feat., punctuation) still resolve.
|
||||||
Returns None below the title threshold. Pure — no network I/O."""
|
Returns None below the title threshold. Pure — no network I/O."""
|
||||||
@@ -68,6 +79,7 @@ def _best_release_group(artist: str, album: str, groups: list) -> dict | None:
|
|||||||
_ratio(artist, _credited_artist(g)),
|
_ratio(artist, _credited_artist(g)),
|
||||||
_ratio(album, g.get("title", "")),
|
_ratio(album, g.get("title", "")),
|
||||||
_is_album(g),
|
_is_album(g),
|
||||||
|
-_rg_year(g), # tie-break: the original (earliest) group over later reissues
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if _ratio(album, best.get("title", "")) < _MIN_TITLE_RATIO:
|
if _ratio(album, best.get("title", "")) < _MIN_TITLE_RATIO:
|
||||||
@@ -75,6 +87,23 @@ def _best_release_group(artist: str, album: str, groups: list) -> dict | None:
|
|||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_release(releases: list) -> dict | None:
|
||||||
|
"""Choose which release of a release-group to read the tracklist from. MB returns them in an
|
||||||
|
arbitrary order and ``releases[0]`` is often a later reissue/deluxe/anniversary edition with
|
||||||
|
more tracks and a longer runtime than the standard album a source actually delivers — which
|
||||||
|
then makes a *complete* download fail the pipeline's track-count/duration completeness checks
|
||||||
|
and re-download forever (Linkin Park "Hybrid Theory": MB's arbitrary pick was a 2023 13-track
|
||||||
|
reissue at ~52 min vs the 2000 original's 12 tracks / ~38 min). Prefer the ORIGINAL standard
|
||||||
|
edition: an Official release, then the earliest date, then a dated one. Pure — unit-tested."""
|
||||||
|
if not releases:
|
||||||
|
return None
|
||||||
|
def _key(r: dict) -> tuple:
|
||||||
|
official = 0 if (r.get("status") == "Official") else 1 # official editions first
|
||||||
|
date = r.get("date") or ""
|
||||||
|
return (official, 0 if date else 1, date) # then dated, then earliest date
|
||||||
|
return min(releases, key=_key)
|
||||||
|
|
||||||
|
|
||||||
class MusicBrainzResolver:
|
class MusicBrainzResolver:
|
||||||
"""Real MbResolver using the MusicBrainz webservice via musicbrainzngs.
|
"""Real MbResolver using the MusicBrainz webservice via musicbrainzngs.
|
||||||
|
|
||||||
@@ -119,9 +148,10 @@ class MusicBrainzResolver:
|
|||||||
if rgid:
|
if rgid:
|
||||||
rgfull = _with_retry(lambda: musicbrainzngs.get_release_group_by_id(rgid, includes=["releases"]))
|
rgfull = _with_retry(lambda: musicbrainzngs.get_release_group_by_id(rgid, includes=["releases"]))
|
||||||
releases = rgfull.get("release-group", {}).get("release-list", [])
|
releases = rgfull.get("release-group", {}).get("release-list", [])
|
||||||
if releases:
|
chosen = _pick_release(releases)
|
||||||
|
if chosen:
|
||||||
rel = _with_retry(
|
rel = _with_retry(
|
||||||
lambda: musicbrainzngs.get_release_by_id(releases[0]["id"], includes=["recordings"])
|
lambda: musicbrainzngs.get_release_by_id(chosen["id"], includes=["recordings"])
|
||||||
).get("release", {})
|
).get("release", {})
|
||||||
tracks = []
|
tracks = []
|
||||||
for medium in rel.get("medium-list", []):
|
for medium in rel.get("medium-list", []):
|
||||||
|
|||||||
@@ -29,12 +29,17 @@ def _dirname(path: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
_ETA_CAP_SECONDS = 359_999 # ~100h; keeps the value well inside Job.downloadEtaSeconds (int4)
|
||||||
|
|
||||||
|
|
||||||
def _eta_seconds(total_bytes: int, bytes_now: int, speed_bps: float) -> int | None:
|
def _eta_seconds(total_bytes: int, bytes_now: int, speed_bps: float) -> int | None:
|
||||||
"""Seconds until the transfer finishes at the current measured rate, or None when it can't be
|
"""Seconds until the transfer finishes at the current measured rate, or None when it can't be
|
||||||
estimated (no throughput sample yet, unknown total, or already complete). Pure — unit-tested."""
|
estimated (no throughput sample yet, unknown total, or already complete). Pure — unit-tested.
|
||||||
|
A near-stalled peer drives speed_bps toward zero, so the estimate is capped: an uncapped value
|
||||||
|
can exceed int4 and overflow the downloadEtaSeconds column write (poisoning the pipeline txn)."""
|
||||||
if speed_bps <= 0 or total_bytes <= 0 or bytes_now >= total_bytes:
|
if speed_bps <= 0 or total_bytes <= 0 or bytes_now >= total_bytes:
|
||||||
return None
|
return None
|
||||||
return int((total_bytes - bytes_now) / speed_bps)
|
return min(int((total_bytes - bytes_now) / speed_bps), _ETA_CAP_SECONDS)
|
||||||
|
|
||||||
|
|
||||||
def _norm(s: str) -> str:
|
def _norm(s: str) -> str:
|
||||||
@@ -70,7 +75,7 @@ def _keep_matching(responses: list, needle: str) -> list:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _parse_search_responses(responses: list) -> list[dict]:
|
def _parse_search_responses(responses: list, artist: str = "") -> list[dict]:
|
||||||
"""Turn slskd search responses into album candidates (one per peer+directory), ordered so the
|
"""Turn slskd search responses into album candidates (one per peer+directory), ordered so the
|
||||||
peers that will FINISH first come first: a free upload slot, then the shortest estimated
|
peers that will FINISH first come first: a free upload slot, then the shortest estimated
|
||||||
transfer time (album bytes ÷ the peer's advertised speed), then a shorter queue. Ranking on
|
transfer time (album bytes ÷ the peer's advertised speed), then a shorter queue. Ranking on
|
||||||
@@ -104,7 +109,14 @@ def _parse_search_responses(responses: list) -> list[dict]:
|
|||||||
est_seconds = total_bytes / (speed if speed > 0 else 1)
|
est_seconds = total_bytes / (speed if speed > 0 else 1)
|
||||||
lossless = all(f["ext"] in _LOSSLESS_EXT for f in dfiles)
|
lossless = all(f["ext"] in _LOSSLESS_EXT for f in dfiles)
|
||||||
dirname = _basename(directory)
|
dirname = _basename(directory)
|
||||||
if " - " in dirname:
|
# Trust the REQUESTED artist when it appears anywhere in the path — many rips put the
|
||||||
|
# artist in a parent folder ("Rihanna\(2009) Rated R\…") or bury it ("2009 - Rihanna -
|
||||||
|
# Rated R"), which the "Artist - Album" folder parse gets wrong; that tanked the
|
||||||
|
# confidence score and lost real FLACs to clean-named MP3s. album stays the folder name
|
||||||
|
# (fuzzy-matched downstream); artist is what the parse gets wrong.
|
||||||
|
if artist and _path_matches(artist, dfiles[0]["filename"]):
|
||||||
|
guess_artist, guess_album = artist, dirname
|
||||||
|
elif " - " in dirname:
|
||||||
guess_artist, guess_album = (p.strip() for p in dirname.split(" - ", 1))
|
guess_artist, guess_album = (p.strip() for p in dirname.split(" - ", 1))
|
||||||
else:
|
else:
|
||||||
guess_artist, guess_album = "", dirname
|
guess_artist, guess_album = "", dirname
|
||||||
@@ -194,7 +206,7 @@ class SlskdClient:
|
|||||||
if alt:
|
if alt:
|
||||||
responses = alt
|
responses = alt
|
||||||
break
|
break
|
||||||
return _parse_search_responses(responses)
|
return _parse_search_responses(responses, artist)
|
||||||
|
|
||||||
def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
||||||
ref = json.loads(source_ref)
|
ref = json.loads(source_ref)
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ _AUDIO_EXT = {".flac", ".mp3", ".m4a", ".opus", ".ogg", ".aac", ".wav"}
|
|||||||
# Reject a download whose total playtime falls below this fraction of MusicBrainz's expected
|
# Reject a download whose total playtime falls below this fraction of MusicBrainz's expected
|
||||||
# total — catches truncated files / preview clips substituted for real tracks that keep the
|
# total — catches truncated files / preview clips substituted for real tracks that keep the
|
||||||
# track COUNT right. Generous, so edition/encoding differences don't false-positive; bonus
|
# track COUNT right. Generous, so edition/encoding differences don't false-positive; bonus
|
||||||
# tracks (over-long) are always fine.
|
# tracks (over-long) are always fine. Kept well below 1.0 because a source legitimately delivers a
|
||||||
_DURATION_MIN_RATIO = 0.85
|
# different edition than MB's resolved release (shorter radio edits, no bonus/live tracks), and only
|
||||||
|
# a genuinely truncated/preview-filled download (playtime a fraction of expected) should be caught.
|
||||||
|
_DURATION_MIN_RATIO = 0.65
|
||||||
# Cap how many ranked candidates a single job will actually download+verify before giving up.
|
# Cap how many ranked candidates a single job will actually download+verify before giving up.
|
||||||
# Without this, the incomplete-download fall-through can grind through dozens of sources (a popular
|
# Without this, the incomplete-download fall-through can grind through dozens of sources (a popular
|
||||||
# album returns ~200 Soulseek candidates), each a slow attempt that also leaves an abandoned slskd
|
# album returns ~200 Soulseek candidates), each a slow attempt that also leaves an abandoned slskd
|
||||||
@@ -189,6 +191,12 @@ def _make_on_progress(conn: psycopg.Connection, job_id: str, reports: "threading
|
|||||||
try:
|
try:
|
||||||
_set_download_progress(conn, job_id, frac, eta_seconds)
|
_set_download_progress(conn, job_id, frac, eta_seconds)
|
||||||
except Exception as e: # a progress write must never fail the download
|
except Exception as e: # a progress write must never fail the download
|
||||||
|
# Roll back so a failed write can't leave the shared pipeline conn in an aborted
|
||||||
|
# txn (which would cascade "current transaction is aborted" into every later query).
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
print(f"pipeline: on_progress write failed: {e}", flush=True)
|
print(f"pipeline: on_progress write failed: {e}", flush=True)
|
||||||
|
|
||||||
return on_progress
|
return on_progress
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from lyra_worker._musicbrainz import _best_release_group, _with_retry
|
from lyra_worker._musicbrainz import _best_release_group, _pick_release, _with_retry
|
||||||
|
|
||||||
# Offline unit tests for the pure release-group selection logic. The live resolve()
|
# Offline unit tests for the pure release-group selection logic. The live resolve()
|
||||||
# path (network) is covered by test_musicbrainz_live.py.
|
# path (network) is covered by test_musicbrainz_live.py.
|
||||||
@@ -51,6 +51,21 @@ def test_empty_groups_returns_none():
|
|||||||
assert _best_release_group("Michael Jackson", "Off the Wall", []) is None
|
assert _best_release_group("Michael Jackson", "Off the Wall", []) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefers_original_group_over_later_reissue():
|
||||||
|
# Real "Hybrid Theory" bug: MB lists a 2000 original and a 2023 reissue as separate same-titled
|
||||||
|
# Album groups (tie on artist/title/type). The reissue's longer tracklist fails completeness
|
||||||
|
# checks, so the original (earliest) must win regardless of MB's relevance order.
|
||||||
|
groups = [
|
||||||
|
{"id": "reissue", "title": "Hybrid Theory", "primary-type": "Album",
|
||||||
|
"first-release-date": "2023-07-21",
|
||||||
|
"artist-credit": [{"artist": {"name": "Linkin Park"}}]},
|
||||||
|
{"id": "original", "title": "Hybrid Theory", "primary-type": "Album",
|
||||||
|
"first-release-date": "2000-10-24",
|
||||||
|
"artist-credit": [{"artist": {"name": "Linkin Park"}}]},
|
||||||
|
]
|
||||||
|
assert _best_release_group("Linkin Park", "Hybrid Theory", groups)["id"] == "original"
|
||||||
|
|
||||||
|
|
||||||
def test_prefers_credited_artist_over_same_titled_other_artist_album():
|
def test_prefers_credited_artist_over_same_titled_other_artist_album():
|
||||||
# Real-world "Fun Machine" bug: Lake Street Dive's EP is what we followed, but two
|
# Real-world "Fun Machine" bug: Lake Street Dive's EP is what we followed, but two
|
||||||
# unrelated bands also have a release group literally titled "Fun Machine" as an Album.
|
# unrelated bands also have a release group literally titled "Fun Machine" as an Album.
|
||||||
@@ -68,6 +83,30 @@ def test_prefers_credited_artist_over_same_titled_other_artist_album():
|
|||||||
# --- _with_retry: transient MusicBrainz failures (SSL EOF, 503 rate-limit) ---
|
# --- _with_retry: transient MusicBrainz failures (SSL EOF, 503 rate-limit) ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_release_prefers_original_official_over_later_reissue():
|
||||||
|
# Real "Hybrid Theory" bug: MB's arbitrary releases[0] was a 2023 13-track reissue; the 2000
|
||||||
|
# original is what sources deliver, so completeness checks must reference it.
|
||||||
|
releases = [
|
||||||
|
{"id": "reissue", "status": "Official", "date": "2023-06-01"},
|
||||||
|
{"id": "original", "status": "Official", "date": "2000-10-24"},
|
||||||
|
{"id": "deluxe", "status": "Official", "date": "2020-10-09"},
|
||||||
|
]
|
||||||
|
assert _pick_release(releases)["id"] == "original"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_release_skips_non_official_and_undated():
|
||||||
|
releases = [
|
||||||
|
{"id": "promo", "status": "Promotion", "date": "1999-01-01"}, # earliest but not official
|
||||||
|
{"id": "undated", "status": "Official", "date": ""},
|
||||||
|
{"id": "official", "status": "Official", "date": "2001-03-03"},
|
||||||
|
]
|
||||||
|
assert _pick_release(releases)["id"] == "official"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_release_empty_is_none():
|
||||||
|
assert _pick_release([]) is None
|
||||||
|
|
||||||
|
|
||||||
def test_retry_returns_value_without_backoff_when_first_call_succeeds():
|
def test_retry_returns_value_without_backoff_when_first_call_succeeds():
|
||||||
sleeps: list[float] = []
|
sleeps: list[float] = []
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import lyra_worker.adapters._slskd as slskd_mod
|
import lyra_worker.adapters._slskd as slskd_mod
|
||||||
from lyra_worker.adapters._slskd import SlskdClient, _eta_seconds, _parse_search_responses
|
from lyra_worker.adapters._slskd import (
|
||||||
|
_ETA_CAP_SECONDS,
|
||||||
|
SlskdClient,
|
||||||
|
_eta_seconds,
|
||||||
|
_parse_search_responses,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _Resp:
|
class _Resp:
|
||||||
@@ -110,6 +115,27 @@ def test_parse_ranks_free_and_fast_peers_first():
|
|||||||
assert peers == ["fast", "slow", "queued"] # free+fast first; no-slot peer last despite speed
|
assert peers == ["fast", "slow", "queued"] # free+fast first; no-slot peer last despite speed
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_uses_requested_artist_when_present_in_path():
|
||||||
|
# Real "Rated R" MP3-over-FLAC bug: the artist is in a PARENT folder ("Rihanna\(2009) Rated
|
||||||
|
# R\…"), so the "Artist - Album" folder parse yields no artist and confidence tanks. When the
|
||||||
|
# requested artist is in the path, trust it so the candidate scores as Rihanna.
|
||||||
|
responses = [
|
||||||
|
{"username": "p", "hasFreeUploadSlot": True, "uploadSpeed": 1000, "queueLength": 0,
|
||||||
|
"files": [{"filename": r"@@x\Music\Rihanna\(2009) Rated R\01. Mad House.flac", "size": 5}]},
|
||||||
|
]
|
||||||
|
c = _parse_search_responses(responses, artist="Rihanna")[0]
|
||||||
|
assert c["artist"] == "Rihanna"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_without_artist_falls_back_to_folder_parse():
|
||||||
|
responses = [
|
||||||
|
{"username": "p", "hasFreeUploadSlot": True, "uploadSpeed": 1000, "queueLength": 0,
|
||||||
|
"files": [{"filename": r"x\Some Band - Some Album\01.flac", "size": 5}]},
|
||||||
|
]
|
||||||
|
c = _parse_search_responses(responses)[0] # no artist arg → folder parse
|
||||||
|
assert c["artist"] == "Some Band"
|
||||||
|
|
||||||
|
|
||||||
def test_parse_prefers_shorter_transfer_over_raw_speed():
|
def test_parse_prefers_shorter_transfer_over_raw_speed():
|
||||||
# The Hybrid Theory case: a peer advertising higher speed but serving a ~2x-larger rip (24-bit
|
# The Hybrid Theory case: a peer advertising higher speed but serving a ~2x-larger rip (24-bit
|
||||||
# / bloated FLAC) should lose to a peer with lower speed but a smaller standard rip that
|
# / bloated FLAC) should lose to a peer with lower speed but a smaller standard rip that
|
||||||
@@ -136,6 +162,14 @@ def test_eta_seconds_unknown_cases():
|
|||||||
assert _eta_seconds(1000, 1000, 100.0) is None # already complete
|
assert _eta_seconds(1000, 1000, 100.0) is None # already complete
|
||||||
|
|
||||||
|
|
||||||
|
def test_eta_seconds_capped_when_peer_nearly_stalls():
|
||||||
|
# A near-stalled peer (tiny speed) would yield an ETA far past int4, overflowing the
|
||||||
|
# downloadEtaSeconds column write. The estimate must be clamped to a safe ceiling.
|
||||||
|
eta = _eta_seconds(30_000_000, 0, 0.001) # 30MB / 0.001 B/s ~= 3e10s uncapped
|
||||||
|
assert eta == _ETA_CAP_SECONDS
|
||||||
|
assert eta < 2_147_483_647 # fits in int4
|
||||||
|
|
||||||
|
|
||||||
def test_download_reports_shrinking_eta(tmp_path, monkeypatch):
|
def test_download_reports_shrinking_eta(tmp_path, monkeypatch):
|
||||||
# A 900-byte file arriving 300 bytes per 3s poll → measured speed 100 B/s → ETA counts down
|
# A 900-byte file arriving 300 bytes per 3s poll → measured speed 100 B/s → ETA counts down
|
||||||
# (6s, 3s) then None on completion. on_progress gets (fraction, eta_seconds).
|
# (6s, 3s) then None on completion. on_progress gets (fraction, eta_seconds).
|
||||||
|
|||||||
Reference in New Issue
Block a user